diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 744aed32a11e0d..22d302d789ddad 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -23,4 +23,4 @@ - [ ] I've tested my changes with keyboard and screen readers. - [ ] My code has proper inline documentation. - [ ] I've included developer documentation if appropriate. -- [ ] I've updated all React Native files affected by any refactorings/renamings in this PR. +- [ ] I've updated all React Native files affected by any refactorings/renamings in this PR. diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 7b4c161ff3b5e5..9d5eadacb40153 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -2,22 +2,113 @@ name: Build Gutenberg Plugin Zip on: pull_request: - paths-ignore: - - '**.md' push: branches: [trunk] - tags: - - 'v*' - paths-ignore: - - '**.md' + workflow_dispatch: + inputs: + version: + description: 'rc or stable?' + required: true jobs: + bump-version: + name: Bump version + runs-on: ubuntu-latest + if: | + github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/trunk' && ( + github.event.inputs.version == 'rc' || + github.event.inputs.version == 'stable' + ) + outputs: + old_version: ${{ steps.get_version.outputs.old_version }} + new_version: ${{ steps.get_version.outputs.new_version }} + release_branch: ${{ steps.get_version.outputs.release_branch }} + steps: + - name: Checkout code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + token: ${{ secrets.GUTENBERG_TOKEN }} + + - name: Compute old and new version + id: get_version + run: | + OLD_VERSION=$(jq --raw-output '.version' package.json) + echo "::set-output name=old_version::$(echo $OLD_VERSION)" + if [[ ${{ github.event.inputs.version }} == 'stable' ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i patch) + else + if [[ $OLD_VERSION == *"rc"* ]]; then + NEW_VERSION=$(npx semver $OLD_VERSION -i prerelease) + else + # WordPress version guidelines: If minor is 9, bump major instead. + IFS='.' read -r -a OLD_VERSION_ARRAY <<< "$OLD_VERSION" + if [[ ${OLD_VERSION_ARRAY[1]} == "9" ]]; then + NEW_VERSION="$(npx semver $OLD_VERSION -i major)-rc.1" + else + NEW_VERSION="$(npx semver $OLD_VERSION -i minor)-rc.1" + fi + fi + fi + echo "::set-output name=new_version::$(echo $NEW_VERSION)" + IFS='.' read -r -a NEW_VERSION_ARRAY <<< "$NEW_VERSION" + RELEASE_BRANCH="release/${NEW_VERSION_ARRAY[0]}.${NEW_VERSION_ARRAY[1]}" + echo "::set-output name=release_branch::$(echo $RELEASE_BRANCH)" + + - name: Configure git user name and email + run: | + git config user.name "Gutenberg Repository Automation" + git config user.email gutenberg@wordpress.org + + - name: Create and switch to release branch + if: | + github.event.inputs.version == 'rc' && + ! contains( steps.get_version.outputs.old_version, 'rc' ) + run: git checkout -b "${{ steps.get_version.outputs.release_branch }}" + + - name: Switch to release branch + if: | + github.event.inputs.version == 'stable' || + contains( steps.get_version.outputs.old_version, 'rc' ) + run: | + git fetch --depth=1 origin "${{ steps.get_version.outputs.release_branch }}" + git checkout "${{ steps.get_version.outputs.release_branch }}" + + - name: Update plugin version + env: + VERSION: ${{ steps.get_version.outputs.new_version }} + run: | + cat <<< $(jq --tab --arg version "${VERSION}" '.version = $version' package.json) > package.json + cat <<< $(jq --tab --arg version "${VERSION}" '.version = $version' package-lock.json) > package-lock.json + sed -i "s/${{ steps.get_version.outputs.old_version }}/${VERSION}/g" gutenberg.php + sed -i "s/${{ steps.get_version.outputs.old_version }}/${VERSION}/g" readme.txt + + - name: Commit the version bump + run: | + git add gutenberg.php package.json package-lock.json readme.txt + git commit -m "Bump plugin version to ${{ steps.get_version.outputs.new_version }}" + git push --set-upstream origin "${{ steps.get_version.outputs.release_branch }}" + + - name: Cherry-pick to trunk + run: | + git checkout trunk + TRUNK_VERSION=$(jq --raw-output '.version' package.json) + if [[ ${{ steps.get_version.outputs.old_version }} == "$TRUNK_VERSION" ]]; then + git cherry-pick "${{ steps.get_version.outputs.release_branch }}" + git push + fi + + build: name: Build Release Artifact runs-on: ubuntu-latest + needs: bump-version + if: always() steps: - name: Checkout code uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + ref: ${{ needs.bump-version.outputs.release_branch || github.ref }} - name: Cache node modules uses: actions/cache@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v2.1.4 @@ -48,26 +139,49 @@ jobs: name: gutenberg-plugin path: ./gutenberg.zip + - name: Build release notes draft + if: ${{ needs.bump-version.outputs.new_version }} + env: + VERSION: ${{ needs.bump-version.outputs.new_version }} + run: | + IFS='.' read -r -a VERSION_ARRAY <<< "${VERSION}" + MILESTONE="Gutenberg ${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}" + npm run changelog -- --milestone="$MILESTONE" --unreleased > release-notes.txt + sed -ie '1,6d' release-notes.txt + if [[ ${{ needs.bump-version.outputs.new_version }} != *"rc"* ]]; then + # Include previous RCs' release notes, if any + CHANGELOG_REGEX="=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=" + RC_REGEX="=\s${VERSION}(-rc\.[0-9]+)?\s=" + awk "/${RC_REGEX}/ {found=1;print;next} /${CHANGELOG_REGEX}/ {found=0} found" changelog.txt >> release-notes.txt + fi + + - name: Upload release notes artifact + if: ${{ needs.bump-version.outputs.new_version }} + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + with: + name: release-notes + path: ./release-notes.txt + create-release: name: Create Release Draft and Attach Asset - needs: build + needs: [bump-version, build] runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/v') steps: - name: Set Release Version id: get_release_version - run: echo ::set-output name=version::$(echo $GITHUB_REF | cut -d / -f 3 | sed s/^v// | sed 's/-rc./ RC/' ) + env: + VERSION: ${{ needs.bump-version.outputs.new_version }} + run: echo ::set-output name=version::$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' ) - name: Download Plugin Zip Artifact uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 with: name: gutenberg-plugin - - name: Extract Changelog for Release - run: | - unzip gutenberg.zip changelog.txt - CHANGELOG_REGEX="/=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=/" - awk -i inplace "$CHANGELOG_REGEX"'{p++;next} p==2{exit} p>=1' changelog.txt + - name: Download Release Notes Artifact + uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 + with: + name: release-notes - name: Create Release Draft id: create_release @@ -75,11 +189,12 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - tag_name: ${{ github.ref }} + tag_name: "v${{ needs.bump-version.outputs.new_version }}" release_name: ${{ steps.get_release_version.outputs.version }} + commitish: ${{ needs.bump-version.outputs.release_branch || github.ref }} draft: true - prerelease: ${{ contains(github.ref, 'rc') }} - body_path: changelog.txt + prerelease: ${{ contains(needs.bump-version.outputs.new_version, 'rc') }} + body_path: release-notes.txt - name: Upload Release Asset id: upload-release-asset diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 4bff464420313d..e27b9a83686769 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -2,16 +2,8 @@ name: Create Block on: pull_request: - paths: - - 'packages/**' - - '!packages/**/test/**' - - '!packages/**/*.md' push: branches: [trunk, wp/trunk] - paths: - - 'packages/**' - - '!packages/**/test/**' - - '!packages/**/*.md' jobs: checks: diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index dc10ffe7a12ab8..4d0bbe41c5b27f 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -2,14 +2,10 @@ name: End-to-End Tests on: pull_request: - paths-ignore: - - '**.md' push: branches: - trunk - 'wp/**' - paths-ignore: - - '**.md' jobs: admin: diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 857bed9849def8..e3adb12f173846 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -2,8 +2,6 @@ name: Performances Tests on: pull_request: - paths-ignore: - - '**.md' release: types: [created] diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index aa1156da286d07..76128bb84a6742 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -2,12 +2,8 @@ name: React Native E2E Tests (Android) on: pull_request: - paths-ignore: - - '**.md' push: branches: [trunk] - paths-ignore: - - '**.md' jobs: test: diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index 60b119ff1ee0c2..33b3a8ab72172e 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -2,12 +2,8 @@ name: React Native E2E Tests (iOS) on: pull_request: - paths-ignore: - - '**.md' push: branches: [trunk] - paths-ignore: - - '**.md' jobs: test: @@ -38,7 +34,9 @@ jobs: - name: Restore build cache uses: actions/cache@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v2.1.4 with: - path: packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app + path: | + packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app + packages/react-native-editor/ios/build/WDA key: ${{ runner.os }}-ios-build-${{ matrix.xcode }}-${{ hashFiles('ios-checksums.txt') }} - name: Restore pods cache @@ -65,6 +63,9 @@ jobs: - name: Build (if needed) run: test -e packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/GutenbergDemo || npm run native test:e2e:build-app:ios + - name: Build Web Driver Agent (if needed) + run: test -d packages/react-native-editor/ios/build/WDA || npm run native test:e2e:build-wda + - name: Run iOS Device Tests run: TEST_RN_PLATFORM=ios npm run native device-tests:local ${{ matrix.native-test-name }} diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 57df6897e9553c..0f77e22b1844eb 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -1,18 +1,100 @@ +name: Update Changelog and upload Gutenberg plugin to WordPress.org plugin repo + on: release: - types: [released] - -name: Upload Gutenberg plugin to WordPress.org plugin repo + types: [published] jobs: + get-release-branch: + name: Get release branch name + runs-on: ubuntu-latest + if: github.event.release.assets[0] + outputs: + release_branch: ${{ steps.get_release_branch.outputs.release_branch }} + steps: + - name: Compute release branch name + id: get_release_branch + env: + TAG: ${{ github.event.release.tag_name }} + run: | + IFS='.' read -r -a VERSION_ARRAY <<< "${TAG#v}" + RELEASE_BRANCH="release/${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}" + echo "::set-output name=release_branch::$(echo $RELEASE_BRANCH)" + + update-changelog: + name: Update Changelog on ${{ matrix.branch }} branch + runs-on: ubuntu-latest + if: github.event.release.assets[0] + needs: get-release-branch + env: + TAG: ${{ github.event.release.tag_name }} + strategy: + matrix: + include: + - branch: trunk + label: trunk + - branch: ${{ needs.get-release-branch.outputs.release_branch }} + label: release + steps: + - name: Checkout code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 + with: + ref: ${{ matrix.branch }} + token: ${{ secrets.GUTENBERG_TOKEN }} + + - name: Update the Changelog to include the release notes + run: | + # First, determine where to insert the new Changelog entry. + SERIES="${RELEASE_BRANCH#release/}" + SERIES_REGEX="=\s${SERIES}\.[0-9]+\s=" + CUT_MARKS=$( grep -nP -m 1 "${SERIES_REGEX}" changelog.txt | cut -d: -f1 ) + if [[ -z "${CUT_MARKS}" ]]; then + CHANGELOG_REGEX="=\s[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?\s=" + RC_REGEX="=\s${TAG#v}(-rc\.[0-9]+)?\s=" + CUT_MARKS=$( awk "/${RC_REGEX}/ {print NR; next}; /${CHANGELOG_REGEX}/ {print NR; exit}" changelog.txt ) + fi + BEFORE=$( echo "$CUT_MARKS" | head -n 1 ) + AFTER=$( echo "$CUT_MARKS" | tail -n 1 ) + # Okay, we have all we need to build the new Changelog. + head -n $(( "${BEFORE}" - 1 )) changelog.txt > new_changelog.txt + printf '= %s =\n\n' "${TAG#v}" >> new_changelog.txt + # Need to use a heredoc in order to preserve special characters. + cat <<- "EOF" > release_notes.txt + ${{ github.event.release.body }} + EOF + # Normalize empty lines: Trim them from beginning and end of file... + awk 'NF {p=1} p' <<< "$(< release_notes.txt)" >> new_changelog.txt + # ...then add two empty lines at the end. + printf '\n\n' >> new_changelog.txt + tail -n +"${AFTER}" changelog.txt >> new_changelog.txt + mv new_changelog.txt changelog.txt + + - name: Configure git user name and email + run: | + git config user.name "Gutenberg Repository Automation" + git config user.email gutenberg@wordpress.org + + - name: Commit the Changelog update + run: | + git add changelog.txt + git commit -m "Update Changelog for ${TAG#v}" + git push --set-upstream origin "${{ matrix.branch }}" + + - name: Upload Changelog artifact + uses: actions/upload-artifact@e448a9b857ee2131e752b06002bf0e093c65e571 # v2.2.2 + with: + name: changelog ${{ matrix.label }} + path: ./changelog.txt + upload: name: Upload Gutenberg Plugin runs-on: ubuntu-latest environment: wp.org plugin - if: github.event.release.assets[0] + needs: update-changelog + if: ${{ !github.event.release.prerelease && github.event.release.assets[0] }} env: PLUGIN_REPO_URL: 'https://plugins.svn.wordpress.org/gutenberg' - STABLE_TAG_REGEX: 'Stable tag: [0-9]\+\.[0-9]\+\.[0-9]\+\s*' + STABLE_VERSION_REGEX: '[0-9]\+\.[0-9]\+\.[0-9]\+\s*' SVN_USERNAME: ${{ secrets.svn_username }} SVN_PASSWORD: ${{ secrets.svn_password }} VERSION: ${{ github.event.release.name }} @@ -20,9 +102,11 @@ jobs: - name: Check out Gutenberg trunk from WP.org plugin repo run: svn checkout "$PLUGIN_REPO_URL/trunk" - - name: Get previous stable tag - id: get_previous_stable_tag - run: echo ::set-output name=stable_tag::$(grep "$STABLE_TAG_REGEX" ./trunk/readme.txt) + - name: Get previous stable version + id: get_previous_stable_version + env: + STABLE_TAG_REGEX: 'Stable tag: \K${{ env.STABLE_VERSION_REGEX }}' + run: echo ::set-output name=stable_version::$(grep -oP "${STABLE_TAG_REGEX}" ./trunk/readme.txt) - name: Delete everything working-directory: ./trunk @@ -39,9 +123,15 @@ jobs: - name: Replace the stable tag placeholder with the existing stable tag on the SVN repository env: STABLE_TAG_PLACEHOLDER: 'Stable tag: V\.V\.V' - STABLE_TAG: ${{ steps.get_previous_stable_tag.outputs.stable_tag }} + STABLE_TAG: 'Stable tag: ${{ steps.get_previous_stable_version.outputs.stable_version }}' run: sed -i "s/${STABLE_TAG_PLACEHOLDER}/${STABLE_TAG}/g" ./trunk/readme.txt + - name: Download Changelog Artifact + uses: actions/download-artifact@4a7a711286f30c025902c28b541c10e147a9b843 # v2.0.8 + with: + name: changelog trunk + path: trunk + - name: Commit the content changes working-directory: ./trunk run: | @@ -59,6 +149,6 @@ jobs: - name: Update the plugin's stable version working-directory: ./trunk run: | - sed -i "s/${STABLE_TAG_REGEX}/Stable tag: ${VERSION}/g" ./readme.txt + sed -i "s/Stable tag: ${STABLE_VERSION_REGEX}/Stable tag: ${VERSION}/g" ./readme.txt svn commit -m "Releasing version $VERSION" \ --no-auth-cache --non-interactive --username "$SVN_USERNAME" --password "$SVN_PASSWORD" diff --git a/README.md b/README.md index 3f6d7d36199325..dd556dbd03eb23 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,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/tutorials/create-block/README.md) for the fastest way to get started extending the block editor. See the [Developer Documentation](https://developer.wordpress.org/block-editor/developers/) for extensive tutorials, documentation, and API references. +Review the [Create a Block tutorial](/docs/getting-started/tutorials/create-block/README.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. ### Contribute to Gutenberg diff --git a/bin/packages/build-worker.js b/bin/packages/build-worker.js index b9c7e96f21d7bb..d3b5254c32a9c5 100644 --- a/bin/packages/build-worker.js +++ b/bin/packages/build-worker.js @@ -105,8 +105,7 @@ async function buildCSS( file ) { // Editor styles should be excluded from the default CSS vars output. .concat( file.includes( 'common.scss' ) || - ( ! file.includes( 'block-library' ) && - ! file.includes( 'editor-styles.scss' ) ) + ! file.includes( 'block-library' ) ? [ 'default-custom-properties' ] : [] ) diff --git a/bin/plugin/cli.js b/bin/plugin/cli.js index c1a978c33b131d..e3a22ebd3d603c 100755 --- a/bin/plugin/cli.js +++ b/bin/plugin/cli.js @@ -19,7 +19,6 @@ const catchException = ( command ) => { /** * Internal dependencies */ -const { releaseRC, releaseStable } = require( './commands/release' ); const { publishNpmLatestDistTag, publishNpmNextDistTag, @@ -27,18 +26,6 @@ const { const { getReleaseChangelog } = require( './commands/changelog' ); const { runPerformanceTests } = require( './commands/performance' ); -program - .command( 'release-plugin-rc' ) - .alias( 'rc' ) - .description( 'Release an RC version of the plugin' ) - .action( catchException( releaseRC ) ); - -program - .command( 'release-plugin-stable' ) - .alias( 'stable' ) - .description( 'Release a stable version of the plugin' ) - .action( catchException( releaseStable ) ); - program .command( 'publish-npm-packages-latest' ) .alias( 'npm-latest' ) diff --git a/bin/plugin/commands/packages.js b/bin/plugin/commands/packages.js index 525e91de084d1f..2b9a46ab3f758d 100644 --- a/bin/plugin/commands/packages.js +++ b/bin/plugin/commands/packages.js @@ -121,6 +121,10 @@ async function updatePackages( } ); + const productionPackageNames = Object.keys( + require( '../../../package.json' ).dependencies + ); + const processedPackages = await Promise.all( changelogFilesPublicPackages.map( async ( changelogPath ) => { const fileStream = fs.createReadStream( changelogPath ); @@ -133,13 +137,23 @@ async function updatePackages( lines.push( line ); } - const versionBump = calculateVersionBumpFromChangelog( + let versionBump = calculateVersionBumpFromChangelog( lines, minimumVersionBump ); const packageName = `@wordpress/${ changelogPath.split( '/' ).reverse()[ 1 ] }`; + // Enforce version bump for production packages when + // the stable minor or major version bump requested. + if ( + ! versionBump && + ! isPrerelease && + minimumVersionBump !== 'patch' && + productionPackageNames.includes( packageName ) + ) { + versionBump = minimumVersionBump; + } const packageJSONPath = changelogPath.replace( 'CHANGELOG.md', 'package.json' diff --git a/bin/plugin/commands/release.js b/bin/plugin/commands/release.js deleted file mode 100644 index a554ac42f1288d..00000000000000 --- a/bin/plugin/commands/release.js +++ /dev/null @@ -1,542 +0,0 @@ -/** - * External dependencies - */ -const inquirer = require( 'inquirer' ); -const fs = require( 'fs' ); -const semver = require( 'semver' ); -const Octokit = require( '@octokit/rest' ); -const { sprintf } = require( 'sprintf-js' ); - -/** - * Internal dependencies - */ -const { log, formats } = require( '../lib/logger' ); -const { askForConfirmation, runStep, readJSONFile } = require( '../lib/utils' ); -const git = require( '../lib/git' ); -const { getNextMajorVersion } = require( '../lib/version' ); -const { - getIssuesByMilestone, - getMilestoneByTitle, -} = require( '../lib/milestone' ); -const config = require( '../config' ); -const { - runGitRepositoryCloneStep, - runCleanLocalFoldersStep, - findReleaseBranchName, -} = require( './common' ); - -/** - * Creates a new release branch based on the last package.json version - * and chooses the next version number. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} abortMessage Abort Message. - * @param {string} version the new version. - * @param {string} releaseBranch The release branch to push to. - */ -async function runReleaseBranchCreationStep( - gitWorkingDirectoryPath, - abortMessage, - version, - releaseBranch -) { - await runStep( 'Creating the release branch', abortMessage, async () => { - await askForConfirmation( - 'The Plugin version to be used is ' + - formats.success( version ) + - '. Proceed with the creation of the release branch?', - true, - abortMessage - ); - - // Creates the release branch - await git.createLocalBranch( gitWorkingDirectoryPath, releaseBranch ); - - log( - '>> The local release branch ' + - formats.success( releaseBranch ) + - ' has been successfully created.' - ); - } ); -} - -/** - * Checkouts out the release branch. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} abortMessage Abort Message. - * @param {string} version The new version. - * @param {string} releaseBranch The release branch to checkout. - */ -async function runReleaseBranchCheckoutStep( - gitWorkingDirectoryPath, - abortMessage, - version, - releaseBranch -) { - await runStep( - 'Getting into the release branch', - abortMessage, - async () => { - await git.checkoutRemoteBranch( - gitWorkingDirectoryPath, - releaseBranch - ); - - log( - '>> The local release branch ' + - formats.success( releaseBranch ) + - ' has been successfully checked out.' - ); - - await askForConfirmation( - 'The Version to release is ' + - formats.success( version ) + - '. Proceed?', - true, - abortMessage - ); - } - ); -} - -/** - * Bump the version in the different files (package.json, package-lock.json...) - * and commit the changes. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} version Version to use. - * @param {string} changelog Changelog to use. - * @param {string} abortMessage Abort message. - * - * @return {Promise} hash of the version bump commit. - */ -async function runBumpPluginVersionUpdateChangelogAndCommitStep( - gitWorkingDirectoryPath, - version, - changelog, - abortMessage -) { - let commitHash; - await runStep( 'Updating the plugin version', abortMessage, async () => { - const packageJsonPath = gitWorkingDirectoryPath + '/package.json'; - const packageLockPath = gitWorkingDirectoryPath + '/package-lock.json'; - const pluginFilePath = - gitWorkingDirectoryPath + '/' + config.pluginEntryPoint; - const packageJson = readJSONFile( packageJsonPath ); - const packageLock = readJSONFile( packageLockPath ); - const newPackageJson = { - ...packageJson, - version, - }; - fs.writeFileSync( - packageJsonPath, - JSON.stringify( newPackageJson, null, '\t' ) + '\n' - ); - const newPackageLock = { - ...packageLock, - version, - }; - fs.writeFileSync( - packageLockPath, - JSON.stringify( newPackageLock, null, '\t' ) + '\n' - ); - const content = fs.readFileSync( pluginFilePath, 'utf8' ); - fs.writeFileSync( - pluginFilePath, - content.replace( - ' * Version: ' + packageJson.version, - ' * Version: ' + version - ) - ); - - // Update the content of the readme.txt file - const readmePath = gitWorkingDirectoryPath + '/readme.txt'; - const readmeFileContent = fs.readFileSync( readmePath, 'utf8' ); - const newReadmeContent = - readmeFileContent.substr( - 0, - readmeFileContent.indexOf( '== Changelog ==' ) - ) + - '== Changelog ==\n\n' + - `To read the changelog for ${ config.name } ${ version }, please navigate to the release page.` + - '\n'; - fs.writeFileSync( readmePath, newReadmeContent ); - - // Update the content of the changelog.txt file - const stableVersion = version.match( /[0-9]+\.[0-9]+\.[0-9]+/ )[ 0 ]; - const changelogPath = gitWorkingDirectoryPath + '/changelog.txt'; - const changelogFileContent = fs.readFileSync( changelogPath, 'utf8' ); - const versionHeader = '= ' + version + ' =\n\n'; - const regexToSearch = /=\s([0-9]+\.[0-9]+\.[0-9]+)(-rc\.[0-9]+)?\s=\n\n/g; - let lastDifferentVersionMatch = regexToSearch.exec( - changelogFileContent - ); - if ( lastDifferentVersionMatch[ 1 ] === stableVersion ) { - lastDifferentVersionMatch = regexToSearch.exec( - changelogFileContent - ); - } - const newChangelogContent = - '== Changelog ==\n\n' + - versionHeader + - changelog + - '\n\n' + - changelogFileContent.substr( lastDifferentVersionMatch.index ); - fs.writeFileSync( changelogPath, newChangelogContent ); - - log( - '>> The plugin version and changelog have been updated successfully.' - ); - - // Commit the version bump - await askForConfirmation( - 'Please check the diff. Proceed with the version bump commit?', - true, - abortMessage - ); - commitHash = await git.commit( - gitWorkingDirectoryPath, - 'Bump plugin version to ' + version, - [ - packageJsonPath, - packageLockPath, - pluginFilePath, - readmePath, - changelogPath, - ] - ); - log( - '>> The plugin version bump and changelog update have been committed successfully.' - ); - } ); - - return commitHash; -} - -/** - * Create a local Git Tag. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} version Version to use. - * @param {string} abortMessage Abort message. - */ -async function runCreateGitTagStep( - gitWorkingDirectoryPath, - version, - abortMessage -) { - await runStep( 'Creating the git tag', abortMessage, async () => { - await askForConfirmation( - 'Proceed with the creation of the git tag?', - true, - abortMessage - ); - - await git.createLocalTag( gitWorkingDirectoryPath, 'v' + version ); - - log( - '>> The ' + - formats.success( 'v' + version ) + - ' tag has been created successfully.' - ); - } ); -} - -/** - * Push the local Git Changes and Tags to the remote repository. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} releaseBranch Release branch name. - * @param {string} abortMessage Abort message. - */ -async function runPushGitChangesStep( - gitWorkingDirectoryPath, - releaseBranch, - abortMessage -) { - await runStep( - 'Pushing the release branch and the tag', - abortMessage, - async () => { - await askForConfirmation( - 'The release branch and the tag are going to be pushed to the remote repository. Continue?', - true, - abortMessage - ); - - await git.pushBranchToOrigin( - gitWorkingDirectoryPath, - releaseBranch - ); - - await git.pushTagsToOrigin( gitWorkingDirectoryPath ); - } - ); -} - -/** - * Cherry-picks the version bump commit into trunk. - * - * @param {string} gitWorkingDirectoryPath Git Working Directory Path. - * @param {string} commitHash Commit to cherry-pick. - * @param {string} abortMessage Abort message. - */ -async function runCherrypickBumpCommitIntoTrunkStep( - gitWorkingDirectoryPath, - commitHash, - abortMessage -) { - await runStep( - 'Cherry-picking the bump commit into trunk', - abortMessage, - async () => { - await askForConfirmation( - 'The plugin is now released. Proceed with the version bump in the trunk branch?', - true, - abortMessage - ); - await git.discardLocalChanges( gitWorkingDirectoryPath ); - await git.resetLocalBranchAgainstOrigin( - gitWorkingDirectoryPath, - 'trunk' - ); - await git.cherrypickCommitIntoBranch( - gitWorkingDirectoryPath, - commitHash, - 'trunk' - ); - await git.pushBranchToOrigin( gitWorkingDirectoryPath, 'trunk' ); - } - ); -} - -/** - * Checks whether the milestone associated with the release has open issues or - * pull requests. Returns a promise resolving to true if there are no open issue - * or pull requests, or false otherwise. - * - * @param {string} version Release version. - * - * @return {Promise} Promise resolving to boolean indicating whether - * the milestone is clear of open issues. - */ -async function isMilestoneClear( version ) { - const { githubRepositoryOwner: owner, githubRepositoryName: name } = config; - const octokit = new Octokit(); - const milestone = await getMilestoneByTitle( - octokit, - owner, - name, - // Disable reason: valid-sprintf applies to `@wordpress/i18n` where - // strings are expected to need to be extracted, and thus variables are - // not allowed. This string will not need to be extracted. - // eslint-disable-next-line @wordpress/valid-sprintf - sprintf( config.versionMilestoneFormat, { - ...config, - ...semver.parse( version ), - } ) - ); - - if ( ! milestone ) { - // If milestone can't be found, it's not especially actionable to warn - // that the milestone isn't clear. `true` is the sensible fallback. - return true; - } - - const issues = await getIssuesByMilestone( - new Octokit(), - owner, - name, - milestone.number, - 'open' - ); - - return issues.length === 0; -} - -/** - * Release a new version. - * - * @param {boolean} isRC Whether it's an RC release or not. - * - * @return {string} The new version. - */ -async function releasePlugin( isRC = true ) { - // This is a variable that contains the abort message shown when the script is aborted. - let abortMessage = 'Aborting!'; - let version, releaseBranch; - - const temporaryFolders = []; - - await askForConfirmation( 'Ready to go? ' ); - - const { changelog } = await inquirer.prompt( [ - { - type: 'editor', - name: 'changelog', - message: 'Please provide the CHANGELOG of the release (markdown)', - }, - ] ); - - // Cloning the Git repository - const gitWorkingDirectory = await runGitRepositoryCloneStep( abortMessage ); - temporaryFolders.push( gitWorkingDirectory ); - - const packageJsonPath = gitWorkingDirectory + '/package.json'; - const packageJson = readJSONFile( packageJsonPath ); - const packageVersion = packageJson.version; - const parsedPackagedVersion = semver.parse( packageJson.version ); - const isPackageVersionRC = parsedPackagedVersion.prerelease.length > 0; - - // Are we going to release an RC? - if ( isRC ) { - // We are releasing an RC. - // If packageVersion is stable, then generate new branch and RC1. - // If packageVersion is RC, then checkout branch and inc RC. - if ( ! isPackageVersionRC ) { - version = getNextMajorVersion( packageVersion ) + '-rc.1'; - const parsedVersion = semver.parse( version ); - - releaseBranch = - 'release/' + parsedVersion.major + '.' + parsedVersion.minor; - - await runReleaseBranchCreationStep( - gitWorkingDirectory, - abortMessage, - version, - releaseBranch - ); - } else { - version = semver.inc( packageVersion, 'prerelease', 'rc' ); - releaseBranch = findReleaseBranchName( packageJsonPath ); - - await runReleaseBranchCheckoutStep( - gitWorkingDirectory, - abortMessage, - version, - releaseBranch - ); - } - } else { - // We are releasing a stable version. - // If packageVersion is stable, then checkout the branch and inc patch. - // If packageVersion is RC, then checkout the branch and inc patch, effectively removing the RC. - version = semver.inc( packageVersion, 'patch' ); - releaseBranch = findReleaseBranchName( packageJsonPath ); - - await runReleaseBranchCheckoutStep( - gitWorkingDirectory, - abortMessage, - version, - findReleaseBranchName( packageJsonPath ) - ); - } - - if ( ! ( await isMilestoneClear( version ) ) ) { - await askForConfirmation( - 'There are still open issues in the milestone. Are you sure you want to proceed?', - false, - abortMessage - ); - } - - // Bumping the version and commit. - const commitHash = await runBumpPluginVersionUpdateChangelogAndCommitStep( - gitWorkingDirectory, - version, - changelog, - abortMessage - ); - - // Creating the git tag - await runCreateGitTagStep( gitWorkingDirectory, version, abortMessage ); - - // Push the local changes - await runPushGitChangesStep( - gitWorkingDirectory, - releaseBranch, - abortMessage - ); - - abortMessage = - 'Aborting! Make sure to manually cherry-pick the ' + - formats.success( commitHash ) + - ' commit to the trunk branch.'; - - // Cherry-picking the bump commit into trunk - await runCherrypickBumpCommitIntoTrunkStep( - gitWorkingDirectory, - commitHash, - abortMessage - ); - - abortMessage = 'Aborting! The release is finished though.'; - await runCleanLocalFoldersStep( temporaryFolders, abortMessage ); - - return version; -} - -async function releaseRC() { - log( - formats.title( '\n๐Ÿ’ƒ Time to release ' + config.name + ' ๐Ÿ•บ\n\n' ), - 'Welcome! This tool is going to help you release a new RC version of the Plugin.\n', - 'It goes through different steps: creating the release branch, bumping the plugin version, tagging the release, and pushing the tag to GitHub.\n', - 'Once the tag is pushed to GitHub, GitHub will build the plugin ZIP, and attach it to a release draft.\n' - ); - - const version = await releasePlugin( true ); - - log( - '\n>> ๐ŸŽ‰ The ' + - config.name + - ' version ' + - formats.success( version ) + - ' has been successfully tagged.\n', - "In a few minutes, you'll be able to find the GitHub release draft here: " + - formats.success( config.wpRepositoryReleasesURL ) + - '\n', - "Don't forget to publish the release once the draft is available!\n", - 'Thanks for performing the release!\n' - ); -} - -async function releaseStable() { - log( - formats.title( '\n๐Ÿ’ƒ Time to release ' + config.name + ' ๐Ÿ•บ\n\n' ), - 'Welcome! This tool is going to help you release a new stable version of the Plugin.\n', - 'It goes through different steps: bumping the plugin version, tagging the release, and pushing the tag to GitHub.\n', - 'Once the tag is pushed to GitHub, GitHub will build the plugin ZIP, and attach it to a release draft.\n', - 'To have the release uploaded to the WP.org plugin repository SVN, you need approval from a member of the ' + - config.team + - ' Team.\n' - ); - - const version = await releasePlugin( false ); - - log( - '\n>> ๐ŸŽ‰ ' + - config.name + - ' ' + - formats.success( version ) + - ' has been successfully tagged.\n', - "In a few minutes, you'll be able to find the GitHub release draft here: " + - formats.success( config.wpRepositoryReleasesURL ) + - '\n', - "Don't forget to publish the release once the draft is available!\n", - 'Once published, the upload to the WP.org plugin repository needs approval from a member of the ' + - config.team + - ' Team at ' + - formats.success( - config.githubRepositoryURL + - 'actions/workflows/upload-release-to-plugin-repo.yml ' - ) + - '.\n', - "Thanks for performing the release! and don't forget to publish the release post.\n" - ); -} - -module.exports = { - releaseRC, - releaseStable, -}; diff --git a/changelog.txt b/changelog.txt index 98900ed96f8597..2fb596dc5c402c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,247 @@ == Changelog == -= 10.2.0-rc.1 = += 10.3.1 = + +### Bug Fixes + +- Restore color preset CSS variables for custom links for themes with the `experimental-link-color` support flag enabled. ([30452](https://github.com/WordPress/gutenberg/pull/30452)) +- Restore the inner `
` of the Group block in the editor for classic themes. ([30453](https://github.com/WordPress/gutenberg/pull/30453)) +- Fix content loss for Group block when used with lots of nesting. ([30460](https://github.com/WordPress/gutenberg/pull/30460)) + + += 10.3.0 = + +### Enhancements + +- Add `since` versions to the deprecated features. ([30072](https://github.com/WordPress/gutenberg/pull/30072)) +- Blocks: Add "theme" category and better present Template Parts in the inserter. ([30020](https://github.com/WordPress/gutenberg/pull/30020)) +- Block Editor: + - Add drag handle to select mode. ([28815](https://github.com/WordPress/gutenberg/pull/28815)) + - Improve block inserter keyboard navigation. ([26938](https://github.com/WordPress/gutenberg/pull/26938)) + - Open admin sidebar menu over editor on small screens. ([29955](https://github.com/WordPress/gutenberg/pull/29955)) +- Block Library: + - Cover: Allow drag and drop media replacement. ([29813](https://github.com/WordPress/gutenberg/pull/29813)) + - File: Make the editor markup match the frontend. ([30148](https://github.com/WordPress/gutenberg/pull/30148)) + - Social Links: Improve selected state of empty block. ([29756](https://github.com/WordPress/gutenberg/pull/29756)) + - Standardize the groups in the block toolbar. ([30012](https://github.com/WordPress/gutenberg/pull/30012), [29247](https://github.com/WordPress/gutenberg/pull/29247), [29863](https://github.com/WordPress/gutenberg/pull/29863)) + - Verse block: Add support for the padding to the verse block. ([29820](https://github.com/WordPress/gutenberg/pull/29820)) +- Components: Allow multiple words in the autocomplete phrase matcher. ([29939](https://github.com/WordPress/gutenberg/pull/29939)) +- Gutenberg Plugin: Improved cache bust without `filemtime` for assets. ([29775](https://github.com/WordPress/gutenberg/pull/29775)) +- Icons: Hint the lowercase icon by 0.15px to correct the font weight appearance. ([29754](https://github.com/WordPress/gutenberg/pull/29754)) +- Media: Use image default size from settings. ([29966](https://github.com/WordPress/gutenberg/pull/29966)) + +### New APIs + +- Compose: Add new `useCopyToClipboard` hook. ([29643](https://github.com/WordPress/gutenberg/pull/29643)) +- Deprecated: Add `since` option to `deprecated` function. ([30017](https://github.com/WordPress/gutenberg/pull/30017)) + +### Bug Fixes + +- Block Editor: + - Ensure that uncategorized block types are properly handled. ([30125](https://github.com/WordPress/gutenberg/pull/30125)) + - Fix mover width/size regressions. ([29889](https://github.com/WordPress/gutenberg/pull/29889)) + - Fix navigation mode focus. ([30126](https://github.com/WordPress/gutenberg/pull/30126)) + - Fix regression with multi select style. ([30128](https://github.com/WordPress/gutenberg/pull/30128)) + - Fix the issue with block style preview when example missing. ([29894](https://github.com/WordPress/gutenberg/pull/29894)) + - Fix sibling block inserter displaying at end of block list. ([29920](https://github.com/WordPress/gutenberg/pull/29920)) + - Revert showing empty paragraphs on fronted. ([29809](https://github.com/WordPress/gutenberg/pull/29809)) + - Show the active block variation's icon in Select mode. ([30143](https://github.com/WordPress/gutenberg/pull/30143)) +- Blocks: Adding onRemove event to verse block. ([30104](https://github.com/WordPress/gutenberg/pull/30104)) +- Block Library: + - Cover: Improve disabled media buttons check for placeholder. ([29858](https://github.com/WordPress/gutenberg/pull/29858)) + - Embed: + - Fix overzealous aspect ratio scaling for embeds. ([29510](https://github.com/WordPress/gutenberg/pull/29510)) + - Embed: Fix select on focus. ([29431](https://github.com/WordPress/gutenberg/pull/29431)) + - Gallery: Fix gallery item clicking. ([29860](https://github.com/WordPress/gutenberg/pull/29860)) + - Image: + - Fix block reset sizes on external URL change. ([26879](https://github.com/WordPress/gutenberg/pull/26879)) + - Fix undo step with temporary URL. ([30114](https://github.com/WordPress/gutenberg/pull/30114)) + - Social Link: More accessible labels. ([29659](https://github.com/WordPress/gutenberg/pull/29659)) + - Video: Fix kind attribute missing subtitle value in video text track. ([30040](https://github.com/WordPress/gutenberg/pull/30040)) +- Components: + - Don't display Guide's page control if there is only one page. ([29629](https://github.com/WordPress/gutenberg/pull/29629)) + - Prevent PanelBody title from being overlapped by arrow. ([29914](https://github.com/WordPress/gutenberg/pull/29914)) +- Compose: Call `useMergeRefs` when dependency changes after ref change. ([29892](https://github.com/WordPress/gutenberg/pull/29892)) +- Copy: + - Restore dot at the end of a sentence. ([29897](https://github.com/WordPress/gutenberg/pull/29897)) + - Update the layout alignment description for better clarity. ([29974](https://github.com/WordPress/gutenberg/pull/29974)) +- Gutenberg Plugin: Update "requires at least" value to 5.6. ([29646](https://github.com/WordPress/gutenberg/pull/29646)) +- E2E Tests: Stabilize randomly failing tests in trunk. ([29836](https://github.com/WordPress/gutenberg/pull/29836)) +- Navigation Component: Align item text to the left/right. ([30083](https://github.com/WordPress/gutenberg/pull/30083)) +- Post Editor: + - Fix post editor layout regression. ([30093](https://github.com/WordPress/gutenberg/pull/30093)) + - Keep post publishing popover open when a date is clicked. ([29738](https://github.com/WordPress/gutenberg/pull/29738), [29893](https://github.com/WordPress/gutenberg/pull/29893)) +- RichText: Fix inline display warning. ([30193](https://github.com/WordPress/gutenberg/pull/30193)) +- Themes: Restore the default editor font for the non FSE themes. ([30080](https://github.com/WordPress/gutenberg/pull/30080)) +- Raw Handling: Fix pasting special spaces. ([28077](https://github.com/WordPress/gutenberg/pull/28077)) +- Storybook: Fix block editor shortcuts. ([29750](https://github.com/WordPress/gutenberg/pull/29750)) +- Writing Flow: + - Fix `caretRangeFromPoint`. ([30031](https://github.com/WordPress/gutenberg/pull/30031)) + - Fix tab behavior. ([30000](https://github.com/WordPress/gutenberg/pull/30000)) + - Remove arrow nav limitations. ([30057](https://github.com/WordPress/gutenberg/pull/30057)) + +### Performance + +- Block Editor: + - Optimise multi-selection select calls. ([30140](https://github.com/WordPress/gutenberg/pull/30140)) + - When inserting Block Patterns they get parsed when the browser is idle. ([29444](https://github.com/WordPress/gutenberg/pull/29444)) +- Block Library: Use early return in the Button block to optimize save.js. ([29781](https://github.com/WordPress/gutenberg/pull/29781)) + +### Experiments + +- Components: + - Add Heading. ([29592](https://github.com/WordPress/gutenberg/pull/29592)) + - Button: Add a default type of button. ([29900](https://github.com/WordPress/gutenberg/pull/29900)) +- Customizer: Add widgets customize inspector. ([29755](https://github.com/WordPress/gutenberg/pull/29755)) +- Full-Site Editing: + - Add a layout configuration to the Group block and `theme.json` and make alignments declarative. ([29335](https://github.com/WordPress/gutenberg/pull/29335)) + - Add client ID trees selectors in block navigation. ([29902](https://github.com/WordPress/gutenberg/pull/29902)) + - Add description field to Post Content block. ([29971](https://github.com/WordPress/gutenberg/pull/29971)) + - Add Log In/Out block. ([29766](https://github.com/WordPress/gutenberg/pull/29766)) + - Add Query Title block and Archive Title variation. ([29428](https://github.com/WordPress/gutenberg/pull/29428)) + - Add Term Description block. ([29613](https://github.com/WordPress/gutenberg/pull/29613)) + - Add preload_paths filter for widgets screen and full site editing. ([28701](https://github.com/WordPress/gutenberg/pull/28701)) + - Add support for experimental layout in Post Content block. ([29982](https://github.com/WordPress/gutenberg/pull/29982)) + - Add layout support to the Template Part block. ([30077](https://github.com/WordPress/gutenberg/pull/30077)) + - Add link color option in Site Title block. ([29924](https://github.com/WordPress/gutenberg/pull/29924)) + - Always use full screen mode. ([29489](https://github.com/WordPress/gutenberg/pull/29489)) + - Automatically open the sidebar to the appropriate menu. ([26964](https://github.com/WordPress/gutenberg/pull/26964), [30098](https://github.com/WordPress/gutenberg/pull/30098)) + - Close navigation panel after template selection. ([29956](https://github.com/WordPress/gutenberg/pull/29956)) + - Expose Template Part block variations to the Inserter. ([30032](https://github.com/WordPress/gutenberg/pull/30032)) + - First step towards hybrid themes โ€“ fallback to PHP templates. ([29026](https://github.com/WordPress/gutenberg/pull/29026)) + - Fix block toolbar from overlapping navigation panel. ([29918](https://github.com/WordPress/gutenberg/pull/29918)) + - Fix different markup in the editor and on the frontend for the Site Title block. ([29021](https://github.com/WordPress/gutenberg/pull/29021)) + - Fix edge case where the default layout could be undefined. ([30024](https://github.com/WordPress/gutenberg/pull/30024)) + - Fix persistence of Preferences in site editor. ([30019](https://github.com/WordPress/gutenberg/pull/30019)) + - Fix Post Comment Count block attribute. ([30056](https://github.com/WordPress/gutenberg/pull/30056)) + - Fix Query Loop block margin. ([30078](https://github.com/WordPress/gutenberg/pull/30078)) + - Fix Template Part alignments behavior. ([30099](https://github.com/WordPress/gutenberg/pull/30099)) + - Fix template saving issue after switching themes. ([29842](https://github.com/WordPress/gutenberg/pull/29842)) + - Polish site button focus/hover styles in post and site editor. ([29888](https://github.com/WordPress/gutenberg/pull/29888)) + - Prevent navigation panel focus when hidden. ([29600](https://github.com/WordPress/gutenberg/pull/29600)) + - Refactor the Post Content block. ([29898](https://github.com/WordPress/gutenberg/pull/29898)) + - Remove alignments from the root level of the site editor. ([30079](https://github.com/WordPress/gutenberg/pull/30079)) + - Remove header toolbar transition in reduced-motion mode. ([29764](https://github.com/WordPress/gutenberg/pull/29764)) + - Remove unused QueryProvider in Query block. ([29947](https://github.com/WordPress/gutenberg/pull/29947)) + - Template Part: Identify template parts in error messages. ([28398](https://github.com/WordPress/gutenberg/pull/28398)) + - Update Post Content icon, unuse justify. ([29867](https://github.com/WordPress/gutenberg/pull/29867)) + - Update Post Title markup so that editor and front match. ([29824](https://github.com/WordPress/gutenberg/pull/29824)) + - Update template details popover. ([29439](https://github.com/WordPress/gutenberg/pull/29439)) +- Global Styles: + - Allow themes to use any styles in the `theme.json` whether or not the block supports it. ([29941](https://github.com/WordPress/gutenberg/pull/29941)) + - Better CSS reset style loader order. ([30034](https://github.com/WordPress/gutenberg/pull/30034)) + - Block Supports: Allow skipping serialization of border. ([30035](https://github.com/WordPress/gutenberg/pull/30035)) + - Optimistically continue with empty data when user data for global styles is not a JSON. ([30088](https://github.com/WordPress/gutenberg/pull/30088)) + - Remove kebab-case camelCase transformations. ([29986](https://github.com/WordPress/gutenberg/pull/29986)) + - Skip `null` when translating settings. ([30171](https://github.com/WordPress/gutenberg/pull/30171)) + - Translate custom templates in `theme.json`. ([29828](https://github.com/WordPress/gutenberg/pull/29828)) +- Navigation Editor and Block: + - Add line-height to Navigation block. ([30010](https://github.com/WordPress/gutenberg/pull/30010)) + - Add padding to Navigation Link placeholder. ([29832](https://github.com/WordPress/gutenberg/pull/29832)) + - Allow vertical inserter in the Navigation block. ([28833](https://github.com/WordPress/gutenberg/pull/28833)) + - Consistently provide fallback variations for the block. ([30117](https://github.com/WordPress/gutenberg/pull/30117)) + - Enable list view. ([29936](https://github.com/WordPress/gutenberg/pull/29936)) + - Fix flyout background color in Page List block. ([29932](https://github.com/WordPress/gutenberg/pull/29932)) + - Fix link items in navigation screen. ([30009](https://github.com/WordPress/gutenberg/pull/30009)) + - Fix minor styling issues with nav editor. ([30129](https://github.com/WordPress/gutenberg/pull/30129)) + - Fix Navigation block styles in the navigation editor. ([29748](https://github.com/WordPress/gutenberg/pull/29748)) + - Fix navigation editor link search suggestions. ([29707](https://github.com/WordPress/gutenberg/pull/29707)) + - Fix navigation editor saving. ([29749](https://github.com/WordPress/gutenberg/pull/29749)) + - Fix navigation screen font. ([30085](https://github.com/WordPress/gutenberg/pull/30085)) + - Fix navigation screen inserter horizontal scrollbar. ([29930](https://github.com/WordPress/gutenberg/pull/29930)) + - Fix navigation editor block toolbar not visible on small screens. ([29967](https://github.com/WordPress/gutenberg/pull/29967)) + - Fix padding issues with nav screen. ([30183](https://github.com/WordPress/gutenberg/pull/30183)) + - Fix paragraph margin specificity inside layout containers. ([30038](https://github.com/WordPress/gutenberg/pull/30038)) + - Fix popover anchor in Navigation Link block. ([30173](https://github.com/WordPress/gutenberg/pull/30173)) + - Improve default label of location select. ([29908](https://github.com/WordPress/gutenberg/pull/29908)) + - Increase importance of submenus staying open. ([30169](https://github.com/WordPress/gutenberg/pull/30169)) + - Keep submenus open on select in the editor. ([29713](https://github.com/WordPress/gutenberg/pull/29713)) + - Match editor markup to rendered in Navigation Link block. ([29935](https://github.com/WordPress/gutenberg/pull/29935)) + - Move theme location settings to navigation editor sidebar. ([29458](https://github.com/WordPress/gutenberg/pull/29458)) + - Navigation Menu: Show submenus only on select in the editor. ([29869](https://github.com/WordPress/gutenberg/pull/29869)) + - Polish navigation screen. ([29926](https://github.com/WordPress/gutenberg/pull/29926), [30168](https://github.com/WordPress/gutenberg/pull/30168)) + - Simplify focus style in Site Icon block. ([29872](https://github.com/WordPress/gutenberg/pull/29872)) + - Show all menus in manage locations. ([29906](https://github.com/WordPress/gutenberg/pull/29906)) + - Unset font weight and text decoration inheritance in Navigation block. ([30011](https://github.com/WordPress/gutenberg/pull/30011)) + - Use the interface package for the navigation screen. ([30013](https://github.com/WordPress/gutenberg/pull/30013)) + - Visual and design improvements for List View. ([29769](https://github.com/WordPress/gutenberg/pull/29769)) +- Widgets Editor: + - Fix warning when widgets block editor is disabled. ([30318](https://github.com/WordPress/gutenberg/pull/30318)) + - Iterate on widgets REST API endpoints. ([29649](https://github.com/WordPress/gutenberg/pull/29649)) + - Load block editor assets in the navigation and widget editors. ([30076](https://github.com/WordPress/gutenberg/pull/30076)) + - Unify menu item styles for Navigation Block and Page List blocks. ([29975](https://github.com/WordPress/gutenberg/pull/29975)) + - Use a default sans serif font for the widget screen. ([30084](https://github.com/WordPress/gutenberg/pull/30084)) + +### Documentation + +- Block Editor: Fix `renderAppender` documentation. ([29925](https://github.com/WordPress/gutenberg/pull/29925)) +- Handbook: + - Fix broken image link in the documentation main README. ([29857](https://github.com/WordPress/gutenberg/pull/29857)) + - Fix broken link to developer resources in README.md. (#29795). ([29796](https://github.com/WordPress/gutenberg/pull/29796)) + - Fix link to native-mobile.md in pull request template. ([29923](https://github.com/WordPress/gutenberg/pull/29923)) + - Fix rebase error. ([29753](https://github.com/WordPress/gutenberg/pull/29753)) + - Remove superfluous sentence in create block tutorial. ([30062](https://github.com/WordPress/gutenberg/pull/30062)) + - Update block design principles with a new section on how to group controls. ([29816](https://github.com/WordPress/gutenberg/pull/29816)) + - Update broken link to Getting Started for the React Native based Mobile Gutenberg. ([30162](https://github.com/WordPress/gutenberg/pull/30162)) + - Update the quick view image on the documentation homepage. ([29808](https://github.com/WordPress/gutenberg/pull/29808)) +- Editor: Clarify the purpose of the `@wordpress/editor` package. ([30136](https://github.com/WordPress/gutenberg/pull/30136)) +- I18n: Replace dead link in README.md. ([29699](https://github.com/WordPress/gutenberg/pull/29699)) +- Interface: Fix typos in interface package. ([29740](https://github.com/WordPress/gutenberg/pull/29740)) + +### Code Quality + +- API Fetch: + - Type several of the middlewares. ([29719](https://github.com/WordPress/gutenberg/pull/29719), [30150](https://github.com/WordPress/gutenberg/pull/30150), [29901](https://github.com/WordPress/gutenberg/pull/29901)) + - Type the rest of the package. ([30161](https://github.com/WordPress/gutenberg/pull/30161)) +- Block Editor: + - Avoid `isInsideRootBlock` (DOM query) in `useFocusFirstElement`. ([30178](https://github.com/WordPress/gutenberg/pull/30178)) + - Focus mode: Fix opacity for inner blocks, move classes. ([30130](https://github.com/WordPress/gutenberg/pull/30130)) + - Move class for navigation mode. ([30181](https://github.com/WordPress/gutenberg/pull/30181)) + - Move `is-typing` and `is-outline-mode` classes up the tree. ([30106](https://github.com/WordPress/gutenberg/pull/30106)) + - Move nav mode exit from writing flow to block props. ([30175](https://github.com/WordPress/gutenberg/pull/30175)) +- Block Library: + - Refactor ServerSideRender to use React hooks. ([28297](https://github.com/WordPress/gutenberg/pull/28297)) + - Remove obsolete editor styles for List block. ([30094](https://github.com/WordPress/gutenberg/pull/30094)) + - Rename `loginOut` variable to `logInOut`. ([29979](https://github.com/WordPress/gutenberg/pull/29979)) +- Blocks: + - Ensure theme category is only added when not provided. ([30089](https://github.com/WordPress/gutenberg/pull/30089)) + - Rename getBlockContent to getBlockInnerHTML internally. ([29949](https://github.com/WordPress/gutenberg/pull/29949)) +- Components: Fix React warning in Text Control. ([29724](https://github.com/WordPress/gutenberg/pull/29724)) +- Date: Add types. ([29789](https://github.com/WordPress/gutenberg/pull/29789)) +- DOM: + - Add types to `focusable`. ([29787](https://github.com/WordPress/gutenberg/pull/29787), [30030](https://github.com/WordPress/gutenberg/pull/30030)) + - Split into smaller modules to facilitate typing. ([30044](https://github.com/WordPress/gutenberg/pull/30044)) +- Gutenberg Plugin: + - Cleanup the blocks.php file. ([29964](https://github.com/WordPress/gutenberg/pull/29964)) + - Fix PHPCS warnings. ([30022](https://github.com/WordPress/gutenberg/pull/30022)) +- Packages: Add types directive to api-fetch and date packages. ([30252](https://github.com/WordPress/gutenberg/pull/30252)) +- RichText: Remove dead and deprecated `setFocusedElement`. ([29877](https://github.com/WordPress/gutenberg/pull/29877)) + +### Tools + +- Babel Preset: Update Babel to v7.13.x. ([30018](https://github.com/WordPress/gutenberg/pull/30018)) +- Create block: Require WordPress 5.7 by default and source it from the main plugin file. ([29757](https://github.com/WordPress/gutenberg/pull/29757)) +- E2E Tests: + - Cover the case when using multiple words in the inserter. ([29978](https://github.com/WordPress/gutenberg/pull/29978)) + - Fix test plugin clash. ([29744](https://github.com/WordPress/gutenberg/pull/29744), [29745](https://github.com/WordPress/gutenberg/pull/29745)) + - Set delay to zero in the reduce-motion mixin and tests. ([29762](https://github.com/WordPress/gutenberg/pull/29762)) +- Eslint Plugin: Add TypeScript as peer dependency and make it optional. ([29942](https://github.com/WordPress/gutenberg/pull/29942)) +- GitHub Workflows: + - Release: Allow triggering manually. ([28138](https://github.com/WordPress/gutenberg/pull/28138)) + - Remove path ignore configs from CI. ([30090](https://github.com/WordPress/gutenberg/pull/30090)) + - Use Gutenberg token for version bump, changelog commits. ([30212](https://github.com/WordPress/gutenberg/pull/30212)) +- Packages: Enforce version bump for production packages after WP major. ([29903](https://github.com/WordPress/gutenberg/pull/29903)) +- Unit Testing: Allow TypeScript modules for transpiled packages. ([29873](https://github.com/WordPress/gutenberg/pull/29873)) + + += 10.2.1 = + +### Bug Fixes + +- Button: Restore gradients support. ([29980](https://github.com/WordPress/gutenberg/pull/29980)) + + += 10.2.0 = ### Features @@ -23,33 +264,41 @@ ### Bug Fixes +- Reusable Blocks: + - Fix editor crash when converting block with visible styles to reusable (after a save and page reload). ([29059](https://github.com/WordPress/gutenberg/pull/29059)) + - Fix reusable block crash when converting a just created reusable block to blocks. ([29292](https://github.com/WordPress/gutenberg/pull/29292)) +- Buttons Block: + - Buttons: Fix links inside links. ([29273](https://github.com/WordPress/gutenberg/pull/29273)) + - Fix legacy button center alignments inside the buttons block. ([29281](https://github.com/WordPress/gutenberg/pull/29281)) +- Cover Block: + - Fix cover block content position not migrating correctly from deprecated version. ([29542](https://github.com/WordPress/gutenberg/pull/29542)) + - Fix solid-color only cover has small gray border in the editor only. ([29499](https://github.com/WordPress/gutenberg/pull/29499)) +- Social Icons and Links: + - Fix social icons vertical spacing issue. ([29657](https://github.com/WordPress/gutenberg/pull/29657)) + - Yelp: Fix foreground color, make background transparent. ([29660](https://github.com/WordPress/gutenberg/pull/29660)) + - Social Links: Replace CSS variables with block context approach. ([29330](https://github.com/WordPress/gutenberg/pull/29330)) +- Table of Contents block: + - Fix links when in archive loop or when using "Plain" permalink structure. ([29394](https://github.com/WordPress/gutenberg/pull/29394)) + - Fix class attribute. ([29317](https://github.com/WordPress/gutenberg/pull/29317)) - Add theme styles in the site editor. ([29704](https://github.com/WordPress/gutenberg/pull/29704)) - Fix broken links to the block editor developer handbook. ([29663](https://github.com/WordPress/gutenberg/pull/29663)) -- Yelp. ([29660](https://github.com/WordPress/gutenberg/pull/29660)) -- Fix social icons vertical spacing issue. ([29657](https://github.com/WordPress/gutenberg/pull/29657)) - Fix in between inserter edge case. ([29625](https://github.com/WordPress/gutenberg/pull/29625)) - Fix the button component styles when used with a dashicon. ([29614](https://github.com/WordPress/gutenberg/pull/29614)) - Revert moving is-typing class. ([29608](https://github.com/WordPress/gutenberg/pull/29608)) - Fix inline block styles minification issues with calc(). ([29554](https://github.com/WordPress/gutenberg/pull/29554)) -- Fix cover block content position not migrating correctly from deprecated version. ([29542](https://github.com/WordPress/gutenberg/pull/29542)) -- Fix solid-color only cover has small gray border in the editor only. ([29499](https://github.com/WordPress/gutenberg/pull/29499)) -- Table of Contents block: Fix links when in archive loop or when using "Plain" permalink structure. ([29394](https://github.com/WordPress/gutenberg/pull/29394)) - Packages: Update the publishing command for npm with next dist tag. ([29379](https://github.com/WordPress/gutenberg/pull/29379)) - Ignore build folders when native unit tests. ([29371](https://github.com/WordPress/gutenberg/pull/29371)) - Fix mobile issue template label. ([29344](https://github.com/WordPress/gutenberg/pull/29344)) - Interface: Fix React warnings triggered in ActionItem component. ([29340](https://github.com/WordPress/gutenberg/pull/29340)) -- Social Links: Replace CSS variables with block context approach. ([29330](https://github.com/WordPress/gutenberg/pull/29330)) -- Table of contents: Fix class attribute. ([29317](https://github.com/WordPress/gutenberg/pull/29317)) - Search block: Add missing space to provide valid HTML. ([29314](https://github.com/WordPress/gutenberg/pull/29314)) - Blocks: Ensure that metadata registered on the server for core block is preserved on the client (try 2). ([29302](https://github.com/WordPress/gutenberg/pull/29302)) -- Fix reusable block crash when converting a just created reusable block to blocks. ([29292](https://github.com/WordPress/gutenberg/pull/29292)) - Fix off-center appender in some themes. ([29290](https://github.com/WordPress/gutenberg/pull/29290)) -- Fix legacy button center alignments inside the buttons block. ([29281](https://github.com/WordPress/gutenberg/pull/29281)) - Add enableCustomSpacing to block editor settings. ([29277](https://github.com/WordPress/gutenberg/pull/29277)) -- Buttons: Fix links inside links. ([29273](https://github.com/WordPress/gutenberg/pull/29273)) -- Fix editor crash when converting block with visible styles to reusable (after a save and page reload). ([29059](https://github.com/WordPress/gutenberg/pull/29059)) - Border Radius Support: Fix application of zero radius values. ([28998](https://github.com/WordPress/gutenberg/pull/28998)) - Fix Document Outline mouse click. ([28589](https://github.com/WordPress/gutenberg/pull/28589)) +- Fix typos in template part area tags. ([29937](https://github.com/WordPress/gutenberg/pull/29937)) +- Gallery Block: Fix the crop images setting. ([29823](https://github.com/WordPress/gutenberg/pull/29823)) +- Fix Error: Could not process the 'wp-config.php' transformation. ([29800](https://github.com/WordPress/gutenberg/pull/29800)) ### Performance @@ -57,38 +306,41 @@ ### Experiments -- Global Styles: Do not add padding sub-properties if there's no values in theme.json. ([29712](https://github.com/WordPress/gutenberg/pull/29712)) -- Site Title: Add text decoration and text transform controls. ([29622](https://github.com/WordPress/gutenberg/pull/29622)) -- Make border work on the site editor. ([29618](https://github.com/WordPress/gutenberg/pull/29618)) -- i18n: Fix the template area unassigned type string. ([29617](https://github.com/WordPress/gutenberg/pull/29617)) +- Global Styles: + - Do not add padding sub-properties if there's no values in theme.json. ([29712](https://github.com/WordPress/gutenberg/pull/29712)) + - Fix specificity conflict of blocks with single classes as selectors. ([29378](https://github.com/WordPress/gutenberg/pull/29378)) + - Fix specificity issue between theme and user styles. ([29533](https://github.com/WordPress/gutenberg/pull/29533)) + - Custom Link Color: Do not apply to buttons. ([29557](https://github.com/WordPress/gutenberg/pull/29557)) + - Implement skip serialization for color key in style att. ([29253](https://github.com/WordPress/gutenberg/pull/29253)) +- Full-Site Editing: + - Site Title: Add text decoration and text transform controls. ([29622](https://github.com/WordPress/gutenberg/pull/29622)) + - Make border work on the site editor. ([29618](https://github.com/WordPress/gutenberg/pull/29618)) + - Prevent clicking on tag and category links in the site editor. ([29583](https://github.com/WordPress/gutenberg/pull/29583)) + - Print nothing in the front end if there are no results in Query block. ([29521](https://github.com/WordPress/gutenberg/pull/29521)) + - [Query block] Remove exclusion of current page id. ([29432](https://github.com/WordPress/gutenberg/pull/29432)) + - Handle missing categories/tags in Query block. ([29424](https://github.com/WordPress/gutenberg/pull/29424)) + - Query block setup with block patterns integration. ([28891](https://github.com/WordPress/gutenberg/pull/28891)) + - Update template descriptions for clarity and humanity. ([29531](https://github.com/WordPress/gutenberg/pull/29531)) + - i18n: Fix the template area unassigned type string. ([29617](https://github.com/WordPress/gutenberg/pull/29617)) + - Template Part: Prevent infinite recursion. ([28456](https://github.com/WordPress/gutenberg/pull/28456)) + - Update title, description, and icon of Post Categories. ([29400](https://github.com/WordPress/gutenberg/pull/29400)) + - Show Site Logo's block toolbar when selected, after the editor loads. ([29336](https://github.com/WordPress/gutenberg/pull/29336)) + - Remove delete toolbar option from Site Logo. ([29331](https://github.com/WordPress/gutenberg/pull/29331)) +- Navigation Editor and Block: + - Allow very thin menus. ([29555](https://github.com/WordPress/gutenberg/pull/29555)) + - Refactor and simplify navigation block CSS. ([29465](https://github.com/WordPress/gutenberg/pull/29465)) + - Make navigation placeholder state visible in dark themes. ([29366](https://github.com/WordPress/gutenberg/pull/29366)) + - Update navigation editor menu selection dropdown. ([29202](https://github.com/WordPress/gutenberg/pull/29202)) + - Make Spacer block width adjustable and add it to Navigation block. ([29133](https://github.com/WordPress/gutenberg/pull/29133)) + - Navigation: Try adding navigation link variants via server. ([29095](https://github.com/WordPress/gutenberg/pull/29095)) + - Navigation Editor: Allow menu renaming. ([29012](https://github.com/WordPress/gutenberg/pull/29012)) - Group Block: Add support for custom border settings. ([29591](https://github.com/WordPress/gutenberg/pull/29591)) -- Prevent clicking on tag and category links in the site editor. ([29583](https://github.com/WordPress/gutenberg/pull/29583)) -- Custom Link Color: Do not apply to buttons. ([29557](https://github.com/WordPress/gutenberg/pull/29557)) -- Navigation block: Allow very thin menus. ([29555](https://github.com/WordPress/gutenberg/pull/29555)) -- Fix specificity issue between theme and user styles. ([29533](https://github.com/WordPress/gutenberg/pull/29533)) -- Update template descriptions for clarity and humanity. ([29531](https://github.com/WordPress/gutenberg/pull/29531)) -- Print nothing in the front end if there are no results in Query block. ([29521](https://github.com/WordPress/gutenberg/pull/29521)) - Pass block settings to the client for all blocks. ([29474](https://github.com/WordPress/gutenberg/pull/29474)) -- Refactor and simplify navigation block CSS. ([29465](https://github.com/WordPress/gutenberg/pull/29465)) -- [Query block] Remove exclusion of current page id. ([29432](https://github.com/WordPress/gutenberg/pull/29432)) -- Handle missing categories/tags in Query block. ([29424](https://github.com/WordPress/gutenberg/pull/29424)) -- Update title, description, and icon of Post Categories. ([29400](https://github.com/WordPress/gutenberg/pull/29400)) - Button block: Add color support via block.json. ([29382](https://github.com/WordPress/gutenberg/pull/29382)) -- Global Styles: Fix specificity conflict of blocks with single classes as selectors. ([29378](https://github.com/WordPress/gutenberg/pull/29378)) - Add/new nav link icon. ([29369](https://github.com/WordPress/gutenberg/pull/29369)) -- Make navigation placeholder state visible in dark themes. ([29366](https://github.com/WordPress/gutenberg/pull/29366)) - Temporary hack to render blocks in customizer. ([29365](https://github.com/WordPress/gutenberg/pull/29365)) -- Show Site Logo's block toolbar when selected, after the editor loads. ([29336](https://github.com/WordPress/gutenberg/pull/29336)) -- Remove delete toolbar option from Site Logo. ([29331](https://github.com/WordPress/gutenberg/pull/29331)) - Fix shortcode not showing in the widgets screen. ([29282](https://github.com/WordPress/gutenberg/pull/29282)) -- Implement skip serialization for color key in style att. ([29253](https://github.com/WordPress/gutenberg/pull/29253)) -- Update navigation editor menu selection dropdown. ([29202](https://github.com/WordPress/gutenberg/pull/29202)) -- Make Spacer block width adjustable and add it to Navigation block. ([29133](https://github.com/WordPress/gutenberg/pull/29133)) -- Navigation: Try adding navigation link variants via server. ([29095](https://github.com/WordPress/gutenberg/pull/29095)) -- Navigation Editor: Allow menu renaming. ([29012](https://github.com/WordPress/gutenberg/pull/29012)) - Fix: More resilient appender CSS. ([28908](https://github.com/WordPress/gutenberg/pull/28908)) -- Query block setup with block patterns integration. ([28891](https://github.com/WordPress/gutenberg/pull/28891)) -- Template Part: Prevent infinite recursion. ([28456](https://github.com/WordPress/gutenberg/pull/28456)) ### Documentation @@ -118,37 +370,61 @@ - Include PHP: Replace `dirname( __FILE__ )` with `__DIR__`. ([29404](https://github.com/WordPress/gutenberg/pull/29404)) - Run phpcbf to fix PHP CS issues. ([29368](https://github.com/WordPress/gutenberg/pull/29368)) - Register style attribute when any color property is supported. ([29349](https://github.com/WordPress/gutenberg/pull/29349)) -- Block edit: Avoid memoized block context in favour of useSelect. ([29333](https://github.com/WordPress/gutenberg/pull/29333)) -- Remove unused onFocus block context. ([29318](https://github.com/WordPress/gutenberg/pull/29318)) -- Remove obsolete block context. ([29313](https://github.com/WordPress/gutenberg/pull/29313)) -- Reduce memoized block context: Class names. ([29186](https://github.com/WordPress/gutenberg/pull/29186)) +- Block context: + - Remove unused onFocus block context. ([29318](https://github.com/WordPress/gutenberg/pull/29318)) + - Reduce memoized block context: Class names. ([29186](https://github.com/WordPress/gutenberg/pull/29186)) + - Remove obsolete block context. ([29313](https://github.com/WordPress/gutenberg/pull/29313)) + - Block edit: Avoid memoized block context in favour of useSelect. ([29333](https://github.com/WordPress/gutenberg/pull/29333)) ### Tools -- Temporary skip flaky test. ([29601](https://github.com/WordPress/gutenberg/pull/29601)) -- Scripts: Fork jest-environment-puppeteer to use puppeteer-core directly. ([29418](https://github.com/WordPress/gutenberg/pull/29418)) +- Scripts: + - Fork jest-environment-puppeteer to use puppeteer-core directly. ([29418](https://github.com/WordPress/gutenberg/pull/29418)) + - Add TypeScript support to linting command. ([27143](https://github.com/WordPress/gutenberg/pull/27143)) +- Needs Info / Stale bot + - Add stale issues bot to help triage efforts. ([29321](https://github.com/WordPress/gutenberg/pull/29321)) + - Do not automatically close message, update stale message. ([29310](https://github.com/WordPress/gutenberg/pull/29310)) +- Tests: + - Temporary skip flaky test. ([29601](https://github.com/WordPress/gutenberg/pull/29601)) + - Paragraph block: Add test to ensure unwrapped editable paragraph. ([29299](https://github.com/WordPress/gutenberg/pull/29299)) + - Testing: Use snapshot-diff serializer to remove noise in snapshots. ([29270](https://github.com/WordPress/gutenberg/pull/29270)) + - Inserter: Add end-to-end test to make sure last inserted block is being focused. ([29187](https://github.com/WordPress/gutenberg/pull/29187)) - Blocks: Preprocess validation log with util.format instead of sprintf. ([29334](https://github.com/WordPress/gutenberg/pull/29334)) -- Add stale issues bot to help triage efforts. ([29321](https://github.com/WordPress/gutenberg/pull/29321)) -- Do not automatically close message, update stale message. ([29310](https://github.com/WordPress/gutenberg/pull/29310)) -- Paragraph block: Add test to ensure unwrapped editable paragraph. ([29299](https://github.com/WordPress/gutenberg/pull/29299)) -- Testing: Use snapshot-diff serializer to remove noise in snapshots. ([29270](https://github.com/WordPress/gutenberg/pull/29270)) -- Inserter: Add end-to-end test to make sure last inserted block is being focused. ([29187](https://github.com/WordPress/gutenberg/pull/29187)) - Docs: Update release.md. ([29091](https://github.com/WordPress/gutenberg/pull/29091)) - Docs/Tools/CI: Update references from `master` to `trunk`. ([28433](https://github.com/WordPress/gutenberg/pull/28433)) -- Scripts: Add TypeScript support to linting command. ([27143](https://github.com/WordPress/gutenberg/pull/27143)) ### Various -- Add Table of Contents block (dynamic rendering + hooks version). ([21234](https://github.com/WordPress/gutenberg/pull/21234)) -- Deregister TOC block until issues are resolved. ([29718](https://github.com/WordPress/gutenberg/pull/29718)) -- api-fetch: Add incremental type checking. ([29685](https://github.com/WordPress/gutenberg/pull/29685)) -- docgen: Incrementally add types. ([29684](https://github.com/WordPress/gutenberg/pull/29684)) -- Dom: Add type-checking to data-transfer. ([29682](https://github.com/WordPress/gutenberg/pull/29682)) -- Template part 'area' term - reword confusing 'type' terminology. ([29679](https://github.com/WordPress/gutenberg/pull/29679)) +- Full-Site Editing + - Site Editor: Browsing sidebar templates menu restructure. ([28291](https://github.com/WordPress/gutenberg/pull/28291)) + - Site Editor: Persistent List View. ([28637](https://github.com/WordPress/gutenberg/pull/28637)) + - Template part block: Add variations based on areas. ([29122](https://github.com/WordPress/gutenberg/pull/29122)) + - Template Part: Update switching trigger. ([29257](https://github.com/WordPress/gutenberg/pull/29257)) + - Template part 'area' term - reword confusing 'type' terminology. ([29679](https://github.com/WordPress/gutenberg/pull/29679)) + - Add i18n support for template part variations' descriptions. ([29612](https://github.com/WordPress/gutenberg/pull/29612)) + - Multi entity save panel - remove dynamic copy. ([29637](https://github.com/WordPress/gutenberg/pull/29637)) +- Table of Contents block + - Add Table of Contents block (dynamic rendering + hooks version). ([21234](https://github.com/WordPress/gutenberg/pull/21234)) + - Deregister TOC block until issues are resolved. ([29718](https://github.com/WordPress/gutenberg/pull/29718)) +- Components: + - Add next Button, ButtonGroup. ([29230](https://github.com/WordPress/gutenberg/pull/29230)) + - Add Card. ([29350](https://github.com/WordPress/gutenberg/pull/29350)) + - Add Divider. ([29433](https://github.com/WordPress/gutenberg/pull/29433)) + - Add Popover. ([29084](https://github.com/WordPress/gutenberg/pull/29084)) + - Add TooltipButton. ([29523](https://github.com/WordPress/gutenberg/pull/29523)) + - Add Tooltip and Shortcut. ([29385](https://github.com/WordPress/gutenberg/pull/29385)) + - Do not use ViewOwnProps for Portal. ([29345](https://github.com/WordPress/gutenberg/pull/29345)) + - Update Elevation story. ([29454](https://github.com/WordPress/gutenberg/pull/29454)) +- Component System: + - Add basic tests for style system. ([29320](https://github.com/WordPress/gutenberg/pull/29320)) + - Add tests for color utils. ([29301](https://github.com/WordPress/gutenberg/pull/29301)) +- Types + - api-fetch: Add incremental type checking. ([29685](https://github.com/WordPress/gutenberg/pull/29685)) + - docgen: Add TypeScript support. ([29189](https://github.com/WordPress/gutenberg/pull/29189)) + - docgen: Incrementally add types. ([29684](https://github.com/WordPress/gutenberg/pull/29684)) + - Dom: Add type-checking to data-transfer. ([29682](https://github.com/WordPress/gutenberg/pull/29682)) + - Components: Add types to Shortcut. ([29633](https://github.com/WordPress/gutenberg/pull/29633)) - Button Block: Removes "Link settings" panel. ([29664](https://github.com/WordPress/gutenberg/pull/29664)) -- Multi entity save panel - remove dynamic copy. ([29637](https://github.com/WordPress/gutenberg/pull/29637)) -- Components: Add types to Shortcut. ([29633](https://github.com/WordPress/gutenberg/pull/29633)) -- Add i18n support for template part variations' descriptions. ([29612](https://github.com/WordPress/gutenberg/pull/29612)) - Add regression test for editor JS crash caused by rtlcss parsing exception, take 2. ([29598](https://github.com/WordPress/gutenberg/pull/29598)) - Reset all WP Admin styles in the wrapper of the editor styles. ([29590](https://github.com/WordPress/gutenberg/pull/29590)) - Revert "[Mobile] - Fix splitting/merging of Paragraph and Heading". ([29587](https://github.com/WordPress/gutenberg/pull/29587)) @@ -160,39 +436,23 @@ - Remove base control negative help text margin. ([29550](https://github.com/WordPress/gutenberg/pull/29550)) - Navigation: Re-enable navigation block end-to-end tests. ([29543](https://github.com/WordPress/gutenberg/pull/29543)) - Accessibility improvement on #29530 issue. ([29534](https://github.com/WordPress/gutenberg/pull/29534)) -- Components: Add TooltipButton. ([29523](https://github.com/WordPress/gutenberg/pull/29523)) - Pin SHA values as version numbers for 3rd party GHAs. ([29485](https://github.com/WordPress/gutenberg/pull/29485)) - Update the visual design of the Sidebar Menu. ([29476](https://github.com/WordPress/gutenberg/pull/29476)) -- Components: Update Elevation story. ([29454](https://github.com/WordPress/gutenberg/pull/29454)) - Focus on block selection: Skip inner blocks. ([29434](https://github.com/WordPress/gutenberg/pull/29434)) -- Components: Add Divider. ([29433](https://github.com/WordPress/gutenberg/pull/29433)) -- Components: Add Tooltip and Shortcut. ([29385](https://github.com/WordPress/gutenberg/pull/29385)) - Use correct classname for nested Navigation Link container. ([29380](https://github.com/WordPress/gutenberg/pull/29380)) - Integrate AztecEditor-iOS 1.19.4. ([29355](https://github.com/WordPress/gutenberg/pull/29355)) -- Components: Add Card. ([29350](https://github.com/WordPress/gutenberg/pull/29350)) -- Components: Do not use ViewOwnProps for Portal. ([29345](https://github.com/WordPress/gutenberg/pull/29345)) -- Component System: Add basic tests for style system. ([29320](https://github.com/WordPress/gutenberg/pull/29320)) - Block context: Separate native context. ([29315](https://github.com/WordPress/gutenberg/pull/29315)) - Focus input when InputControl spinner arrows are pressed. ([29305](https://github.com/WordPress/gutenberg/pull/29305)) -- Component System: Add tests for color utils. ([29301](https://github.com/WordPress/gutenberg/pull/29301)) -- Template Part: Update switching trigger. ([29257](https://github.com/WordPress/gutenberg/pull/29257)) - WP Block Styles: Only load in the editor if a theme opts in. ([29252](https://github.com/WordPress/gutenberg/pull/29252)) -- Components: Add next Button, ButtonGroup. ([29230](https://github.com/WordPress/gutenberg/pull/29230)) - Add new overlay text icon, and use for image. ([29215](https://github.com/WordPress/gutenberg/pull/29215)) -- docgen: Add TypeScript support. ([29189](https://github.com/WordPress/gutenberg/pull/29189)) -- Template part block: Add variations based on areas. ([29122](https://github.com/WordPress/gutenberg/pull/29122)) -- Components: Add Popover. ([29084](https://github.com/WordPress/gutenberg/pull/29084)) - Add Missing URL state to Navigation Link Block. ([28861](https://github.com/WordPress/gutenberg/pull/28861)) - Improve dropcap behavior. ([28685](https://github.com/WordPress/gutenberg/pull/28685)) - Improve the block editor handbook table of content. ([28665](https://github.com/WordPress/gutenberg/pull/28665)) -- Site Editor: Persistent List View. ([28637](https://github.com/WordPress/gutenberg/pull/28637)) - RN: Add Bottom Sheet Select Control component. ([28543](https://github.com/WordPress/gutenberg/pull/28543)) -- Site Editor: Browsing sidebar templates menu restructure. ([28291](https://github.com/WordPress/gutenberg/pull/28291)) - RichText: Bypass paste filters for internal paste. ([27967](https://github.com/WordPress/gutenberg/pull/27967)) - Block Directory: Update search results list UI. ([25521](https://github.com/WordPress/gutenberg/pull/25521)) - = 10.1.1 = ### Bug Fixes diff --git a/docs/README.md b/docs/README.md index 2b4f4f9ec7c7a8..26cdbe393ad49e 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ **Gutenberg** is a codename for a whole new paradigm in WordPress site building and publishing, that aims to revolutionize the entire publishing experience as much as Gutenberg did the printed word. The project is right now in the second phase of a four-phase process that will touch every piece of WordPress -- Editing, **Customization** (which includes Full Site Editing, Block Patterns, Block Directory and Block based themes), Collaboration, and Multilingual -- and is focused on a new editing experience, the block editor (which is the topic of the current documentation). -![Quick view of the block editor](https://make.wordpress.org/core/files/2021/01/quick-view-of-the-block-editor.png) +![Quick view of the block editor](https://raw.githubusercontent.com/WordPress/gutenberg/trunk/docs/assets/quick-view-of-the-block-editor.png) **Legend :** diff --git a/docs/assets/quick-view-of-the-block-editor.png b/docs/assets/quick-view-of-the-block-editor.png new file mode 100644 index 00000000000000..1bab7e8cb36b4c Binary files /dev/null and b/docs/assets/quick-view-of-the-block-editor.png differ diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 23f16e0f740c5d..ada1952f38e02a 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -1,6 +1,6 @@ # Coding Guidelines -This living document serves to prescribe coding guidelines specific to the Gutenberg project. Base coding guidelines follow the [WordPress Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/). The following sections outline additional patterns and conventions used in the Gutenberg project. +This living document serves to prescribe coding guidelines specific to the Gutenberg project. Base coding guidelines follow the [WordPress Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/). The following sections outline additional patterns and conventions used in the Gutenberg project. ## CSS diff --git a/docs/contributors/code/getting-started-native-mobile.md b/docs/contributors/code/getting-started-native-mobile.md index b748c8880bc6d8..3ea4f91876b0b2 100644 --- a/docs/contributors/code/getting-started-native-mobile.md +++ b/docs/contributors/code/getting-started-native-mobile.md @@ -26,7 +26,7 @@ git clone https://github.com/WordPress/gutenberg.git Note that the commands described here should be run in the top-level directory of the cloned project. Before running the demo app, you need to download and install the project dependencies. This is done via the following command: ``` -nvm install --latest-npm +nvm install npm ci ``` diff --git a/docs/contributors/code/native-mobile.md b/docs/contributors/code/native-mobile.md index 91f4927a1fa6c2..3b38c2780ad4b2 100644 --- a/docs/contributors/code/native-mobile.md +++ b/docs/contributors/code/native-mobile.md @@ -4,7 +4,7 @@ Intertwined with the web codepaths, the Gutenberg repo also includes the [React ## Running Gutenberg Mobile on Android and iOS -For instructions on how to run the **Gutenberg Mobile Demo App** on Android or iOS, see [Getting Started for the React Native based Mobile Gutenberg](/docs/contributors/code/getting-started-with-code-contribution-native-mobile.md) +For instructions on how to run the **Gutenberg Mobile Demo App** on Android or iOS, see [Getting Started for the React Native based Mobile Gutenberg](/docs/contributors/code/getting-started-native-mobile.md) Also, the mobile client is packaged and released via the [official WordPress apps](https://wordpress.org/mobile/). Even though the build pipeline is slightly different then the mobile demo apps and lives in its own repo for now ([here's the native mobile repo](https://github.com/wordpress-mobile/gutenberg-mobile)), the source code itself is taken directly from this repo and the "web" side codepaths. diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index c08bf5cdde81c0..aba102136232c8 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -22,61 +22,35 @@ If critical bugs are discovered on stable versions of the plugin, patch versions > Note that at the time of writing, the tool doesn't support releasing consecutive RC releases. However, it is possible to use the tool for patch releases following the first stable release. -The plugin release process is entirely automated. To release the RC version of the plugin, run the following command and follow the instructions: +The plugin release process is entirely automated and happens solely on GitHub -- i.e. it doesn't require any steps to be run locally on your machine. -```bash -./bin/plugin/cli.js rc -``` - -To release a stable version, run: - -```bash -./bin/plugin/cli.js stable -``` - -During the release process, you'll be asked to provide: - -- A changelog: prepare one beforehand by following the instructions below. -- A [personal access token](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line): have one ready beforehand by visiting [this page](https://github.com/settings/tokens/new?scopes=repo,admin:org,write:packages), if you haven't got one yet. -- User and password for your GitHub account: if 2FA is enabled for your account (it should), you need to provide a personal access token instead of password (you can use the one necessary for the release). - -The release script will create a `git` tag for the release and push it to GitHub. This triggers a GitHub workflow that builds the plugin, creates a release draft (based on the Changelog), and attaches the plugin zip. This will take a couple of minutes. You will then find the release draft at https://github.com/WordPress/gutenberg/releases. You can edit it further (but note that the changes won't be propagated to `changelog.txt`). Once you're happy with it, press the 'Publish' button. +For your convenience, here's an [11-minute video walkthrough](https://youtu.be/TnSgJd3zpJY) that demonstrates the release process. It's recommended to watch this if you're unfamiliar with it. The process is also documented in the following paragraphs. -If you're releasing a stable version (rather than an RC), this will trigger a GitHub action that will upload the plugin to the WordPress.org plugin repository (SVN). This action needs approval by a member of the [`gutenberg-core` team](https://github.com/orgs/WordPress/teams/gutenberg-core). Locate the ["Upload Gutenberg plugin to WordPress.org plugin repo" workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) for the new version, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). +In order to start the release process, go to Gutenberg's GitHub repository's Actions tab, and locate the ["Build Gutenberg Plugin Zip" action](https://github.com/WordPress/gutenberg/actions/workflows/build-plugin-zip.yml). Note the blue banner that says "This workflow has a `workflow_dispatch` event trigger.", and expand the "Run workflow" dropdown on its right hand side. -### Manual Release Process +![Run workflow dropdown](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/contributors/code/workflow-dispatch-banner.png) -#### Creating the first Release Candidate +To release a release candidate (RC) version of the plugin, enter `rc`. To release a stable version, enter `stable`. In each case, press the green "Run workflow" button. -Releasing the first release candidate for this milestone (`x.x`) involves: +This will trigger a GitHub Actions (GHA) workflow that bumps the plugin version, builds the Gutenberg plugin .zip file, creates a release draft, and attaches the plugin .zip file to it. This part of the process typically takes a little under six minutes. You'll see that workflow appear at the top of the list, right under the blue banner. Once it's finished, it'll change its status icon from a yellow dot to a green checkmark. You can follow along in a more detailed view by clicking on the workflow. -1. writing a release blog post and changelog -2. creating the release branch -3. bumping the version and tagging the release -4. building the plugin -5. publishing the release to GitHub -6. publishing the call for testing +As soon as the workflow has finished, you'll find the release draft under https://github.com/WordPress/gutenberg/releases. The draft is pre-populated with changelog entries based on previous release candidates for this version, and any changes that have since been cherry-picked to the release branch. Thus, when releasing the first stable version of a series, make sure to delete any RC version headers (that are only there for your information), and to move the more recent changes to the corresponding sections below. Furthermore, take some time to edit the release notes into a more legible format, by grouping related entries under common bullet points. Don't rush this part -- it's important to bring the release notes into a nice shape. You don't have to do it all in one go -- you can save the draft and come back to it later. You can find some more tips on writing the release notes and post in the section below. -##### Writing the Release Post and Changelog +Only once you're happy with the shape of the release notes should you press the green "Publish release" button. This will create a `git` tag for the version, publish the release, and trigger [another GHA workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) that has a twofold purpose: -To generate a changelog for a release, use the changelog generator tool: +1. Use the release notes that you just edited to update `changelog.txt`, and +2. upload the new plugin version to the WordPress.org plugin repository (SVN) (only if you're releasing a stable version). -``` -npm run changelog -``` - -By default, this will search for and organize all pull requests associated with the milestone for the next version of the project. +The latter step needs approval by a member of the [`gutenberg-core` team](https://github.com/orgs/WordPress/teams/gutenberg-core). Locate the ["Upload Gutenberg plugin to WordPress.org plugin repo" workflow](https://github.com/WordPress/gutenberg/actions/workflows/upload-release-to-plugin-repo.yml) for the new version, and have it [approved](https://docs.github.com/en/actions/managing-workflow-runs/reviewing-deployments#approving-or-rejecting-a-job). -To override the default behavior, you can pass one or both of the following options. Remember to use `--` to let NPM pass the options to the script. +This will cause the new version to be available to users of WordPress all over the globe! ๐Ÿ’ƒ +You should check that folks are able to install the new version from their Dashboard. -- `--milestone `: Provide the title of the milestone for which the changelog should be generated. This should exactly match the title as shown on [the milestones page](https://github.com/WordPress/gutenberg/milestones). - - Example: `npm run changelog -- --milestone="Gutenberg 8.1"` -- `--token `: Provide a [GitHub personal access token](https://github.com/settings/tokens) for authenticating requests. This should only be necessary if you run the script frequently enough to been blocked by [rate limiting](https://developer.github.com/v3/#rate-limiting). - - Example: `npm run changelog -- --token="..."` -- `--unreleased`: Only list PRs that have been closed after the latest release in the milestone's series has been published. In other words, only list PRs that haven't been part of a release yet. - - Example: `npm run changelog -- --milestone="Gutenberg 9.8" --unreleased`. If the latest version in the 9.8 series is 9.8.3, only show PRs for the in the 9.8 series that were closed (merged) after 9.8.3 was published. +Once released, all that's left to do is writing a release post on [make.wordpress.org/core](https://make.wordpress.org/core/). You can find some tips on that below. +### Writing the Release Notes and Post -The script will output a generated changelog, grouped by pull request label. _Note that this is intended to be a starting point for release notes_. You will still want to manually review and curate the changelog entries. +The release notes draft is auto-generated by a script that looks for pull requests for the current milestone, and groups them by pull request label. +This is intended to be a starting point for release notes; you will still want to manually review and curate the changelog entries. Guidelines for proof-reading include: @@ -92,159 +66,17 @@ You should also include a performance audit at the end of the release post. You Compile this to a draft post on [make.wordpress.org/core](https://make.wordpress.org/core/); this post should be published after the actual release. -##### Creating the Release Branch - -For each milestone (let's assume it's `x.x` here), a release branch is used to release all RCs and minor releases. For the first RC of the milestone, a release branch is created from trunk. - -``` -git checkout trunk -git checkout -b release/x.x -git push origin release/x.x -``` - -##### Bumping the Version and Tagging the Release - -1. Checkout the `release/x.x` branch. -2. Create [a commit like this](https://github.com/WordPress/gutenberg/pull/13125/commits/13fa651dadc2472abb9b95f80db9d5f23e63ae9c), bumping the version number in `gutenberg.php`, `package.json`, and `package-lock.json` to `x.x.0-rc.1`. -3. Create a Pull Request from the release branch into `trunk` using the changelog as a description and ensure the tests pass properly. -4. Tag the RC version. `git tag vx.x.0-rc.1` from the release branch. -5. Push the tag `git push --tags`. -6. Merge the version bump pull request and avoid removing the release branch. - -##### Build the Plugin - -1. Run `git fetch --tags`. -2. Check out the tag for this release, you should run `git checkout vx.x.0-rc.1`. -3. Run `npm run build:plugin-zip` from the root of project. This packages a zip file with a release build of `gutenberg.zip`. - -##### Publish the Release on GitHub - -1. [Create a new release on GitHub](https://github.com/WordPress/gutenberg/releases/new). -2. If you were releasing the `x.x.0-rc.1` release candidate, label it `x.x.0-rc.1` and use the `vx.x.x-rc.1` as a tag. -3. Upload the `gutenberg.zip` file into the release. -4. Use the changelog as a description of the release. -5. Publish the release. - -Here's an example [release candidate page](https://github.com/WordPress/gutenberg/releases/tag/v4.6.0-rc.1); yours should look like that when you're finished. +If you don't have access to [make.wordpress.org/core](https://make.wordpress.org/core/), ping [someone on the Gutenberg Core team](https://github.com/orgs/WordPress/teams/gutenberg-core) in the [WordPress #core-editor Slack channel](https://wordpress.slack.com/messages/C02QB2JS7) to publish the post. -#### Creating Release Candidate Patches (done via `git cherry-pick`) +### Creating Release Candidate Patches (done via `git cherry-pick`) -If a bug is found in a release candidate and a fix is committed to `trunk`, we should include that fix in a new release candidate. To do this you'll need to use `git cherry-pick` to add these changes to the milestone's release branch. This way only fixes are added to the release candidate and not all the new code that has landed on `trunk` since tagging: +If a bug is found in a release candidate and a fix is committed to `trunk`, we should include that fix in the stable version (or optionally in another release candidate before that). To do this you'll need to use `git cherry-pick` to add these changes to the milestone's release branch. This way only the desired fixes are added rather than all the new code that has landed on `trunk` since tagging: 1. Checkout the corresponding release branch with: `git checkout release/x.x`. 2. Cherry-pick fix commits (in chronological order) with `git cherry-pick [SHA]`. -3. Create [a commit like this](https://github.com/WordPress/gutenberg/pull/13125/commits/13fa651dadc2472abb9b95f80db9d5f23e63ae9c), bumping the version number in `gutenberg.php`, `package.json`, and `package-lock.json` to `x.x.0-rc.2`. -4. Create a Pull Request from the release branch into `trunk` using the changelog as a description and ensure the tests pass properly. Note that if there there are merge conflicts, Travis CI will not run on the PR. Run tests locally using `npm run test` and `npm run test-e2e` if this happens. -5. Tag the RC version. `git tag vx.x.0-rc.2` from the release branch. -6. Push the tag `git push --tags`. -7. Create a branch for bumping the version number. `git checkout -b bump/x.x`. -8. Create a Pull Request from the `bump/x.x` branch into `trunk` using the - changelog as a description. -9. Merge the version bump pull request. -10. Follow the steps in [build the plugin](#build-the-plugin) and [publish the release on GitHub](#publish-the-release-on-github). - -You can copy the existing changelog from the previous release candidate. Let other contributors know that a new release candidate has been released in the [`#core-editor` channel](https://wordpress.slack.com/messages/C02QB2JS7) and the call for testing post. - -### Official Gutenberg Releasesโ„ข - -The process of releasing Gutenberg is similar to creating a release candidate, except we don't use the `-rc.X` in the `git` tag and we publish a new branch in the subversion repository. This updates the version available in the WordPress plugin repository and will cause WordPress sites around the world to prompt users to update to this new version. - -#### Creating a Release - -Creating a release involves: - -1. verifying the release blog post and changelog -2. bumping the version -3. building the plugin -4. publishing the new release to GitHub -5. committing to the [plugin repository] -6. publishing the release blog post - -##### Verifying the Release Post and Changelog - -1. Check the draft post on [make.wordpress.org/core](https://make.wordpress.org/core/); make sure the changelog reflects what's shipping in the release. - -##### Bumping the Version - -1. Checkout the release branch `git checkout release/x.x`. - -**Note:** This branch should never be removed or rebased. When we want to merge something from it to trunk and conflicts exist/may exist we use a temporary branch `bump/x.x`. - -2. Create [a commit like this](https://github.com/WordPress/gutenberg/commit/00d01049685f11f9bb721ad3437cb928814ab2a2#diff-b9cfc7f2cdf78a7f4b91a753d10865a2), removing the `-rc.X` from the version number in `gutenberg.php`, `package.json`, and `package-lock.json`. -3. Create a new branch called `bump/x.x` from `release/x.x` and switch to it: `git checkout -b bump/x.x`. -4. Create a pull request from `bump/x.x` to `trunk`. Verify the continuous integrations tests pass, before continuing to the next step even if conflicts exist. -5. Rebase `bump/x.x` against `origin/trunk` using `git fetch origin && git rebase origin/trunk`. -6. Force push the branch `bump/x.x` using `git push --force-with-lease`. -7. Switch to the `release/x.x` branch. Tag the version from the release branch `git tag vx.x.0`. -8. Push the tag `git push --tags`. -9. Merge the version bump pull request. - -##### Build the Plugin - -1. Run `git fetch --tags`. -2. Check out the tag for this release, you should run `git checkout vx.x.0`. -3. Run `npm run build:plugin-zip` from the root of project. This packages a zip file with a release build of `gutenberg.zip`. - -##### Publish the Release on GitHub - -1. [Create a new release on GitHub](https://github.com/WordPress/gutenberg/releases/new). -2. If you were releasing the `x.x.0` release candidate, label it `x.x.0` and use the `vx.x.x` as a tag. -3. Upload the a `gutenberg.zip` file into the release. -4. Use the changelog as a description of the release. -5. Publish the release. - -##### Commit to the Plugin Repository - -You'll need to use Subversion to publish the plugin to WordPress.org. - -1. Do an SVN checkout of `https://wordpress.org/plugins/gutenberg/trunk`: - -- If this is your first checkout, run: `svn checkout https://plugins.svn.wordpress.org/gutenberg/trunk` -- If you already have a copy, run: `svn up` - -2. Delete the contents except for the `readme.txt` and `changelog.txt` files (these files donโ€™t exist in the `git` repo, only in Subversion). -3. Extract the contents of the zip file. -4. Edit `readme.txt`, replacing the changelog for the previous version with the current release's changelog. -5. Add the changelog for the current release to `changelog.txt`. -6. Add new files/remove deleted files from the repository: - -```bash -# Add new files: -svn st | grep '^\?' | awk '{print $2}' | xargs svn add # add the -r option to xargs if you use a linux-based OS -# Delete old files: -svn st | grep '^!' | awk '{print $2}' | xargs svn rm # add the -r option to xargs if you use a linux-based OS -``` - -7. Commit the new version: - -```bash -# Replace X.X.X with your version: -svn ci -m "Committing Gutenberg version X.X.X" -``` - -8. Tag the new version: - -```bash -svn cp https://plugins.svn.wordpress.org/gutenberg/trunk https://plugins.svn.wordpress.org/gutenberg/tags/X.X.X -m "Tagging Gutenberg version X.X.X" -``` - -9. Edit `readme.txt` to point to the new tag. The **Stable version** header in `readme.txt` should be updated to match the new release version number. After updating and committing that, the new version should be released: - -```bash -svn ci -m "Releasing Gutenberg version X.X.X" -``` - -This will cause the new version to be available to users of WordPress all over the globe! ๐Ÿ’ƒ - -You should check that folks are able to install the new version from their Dashboard. - -### Publish the Release Blog Post - -1. Publish the [make/core](https://make.wordpress.org/core/) release blog post drafted earlier. -2. Pat yourself on the back! ๐Ÿ‘ - -If you don't have access to [make.wordpress.org/core](https://make.wordpress.org/core/), ping [someone on the Gutenberg Core team](https://github.com/orgs/WordPress/teams/gutenberg-core) in the [WordPress #core-editor Slack channel](https://wordpress.slack.com/messages/C02QB2JS7) to publish the post. +3. When done, push the changes to GitHub: `git push`. +If you decide that the fixes deserve another release candidate before the stable version is published, create one by following the instructions above. Let other contributors know that a new release candidate has been released in the [`#core-editor` channel](https://wordpress.slack.com/messages/C02QB2JS7). ## Packages Releases to npm and WordPress Core Updates The Gutenberg repository mirrors the [WordPress SVN repository](https://make.wordpress.org/core/handbook/about/release-cycle/) in terms of branching for each SVN branch, a corresponding Gutenberg `wp/*` branch is created: diff --git a/docs/contributors/code/workflow-dispatch-banner.png b/docs/contributors/code/workflow-dispatch-banner.png new file mode 100644 index 00000000000000..d05c88d753f2f8 Binary files /dev/null and b/docs/contributors/code/workflow-dispatch-banner.png differ diff --git a/docs/explanations/architecture/modularity.md b/docs/explanations/architecture/modularity.md index 0f80bc01a50b65..b3494910de2787 100644 --- a/docs/explanations/architecture/modularity.md +++ b/docs/explanations/architecture/modularity.md @@ -10,9 +10,9 @@ These packages are used to power the Block Editor, but they can be used to power Using a modular architecture has several benefits for all the actors involved: -* Each package is an independent unit and has a well defined public API that is used to interact with other packages and third-party code. This makes it easier for **Core Contributors** to reason about the codebase. They can focus on a single package at a time, understand it and make updates while knowing exactly how these changes could impact all the other parts relying on the given package. -* A module approach is also beneficial to the **end-user**. It allows to selectively load scripts on different WordPress Admin pages while keeping the bundle size contained. For instance, if we use the components package to power our plugin's settings page, there's no need to load the block-editor package on that page. -* This architecture also allows **third-party developers** to reuse these packages inside and outside the WordPress context by using these packages as npm or WordPress script dependencies. +- Each package is an independent unit and has a well defined public API that is used to interact with other packages and third-party code. This makes it easier for **Core Contributors** to reason about the codebase. They can focus on a single package at a time, understand it and make updates while knowing exactly how these changes could impact all the other parts relying on the given package. +- A module approach is also beneficial to the **end-user**. It allows to selectively load scripts on different WordPress Admin pages while keeping the bundle size contained. For instance, if we use the components package to power our plugin's settings page, there's no need to load the block-editor package on that page. +- This architecture also allows **third-party developers** to reuse these packages inside and outside the WordPress context by using these packages as npm or WordPress script dependencies. ## Types of packages @@ -24,23 +24,21 @@ These are the packages that ship in WordPress itself as JavaScript scripts. Thes Third-party developers can use these production packages in two different ways: -* If you're building a JavaScript application, website, page that runs outside of the context of WordPress, you can consume these packages like any other JavaScript package in the npm registry. +- If you're building a JavaScript application, website, page that runs outside of the context of WordPress, you can consume these packages like any other JavaScript package in the npm registry. ``` npm install @wordpress/components ``` ```js -import { Button }ย from '@wordpress/components'; +import { Button } from '@wordpress/components'; function MyApp() { - return ( - - ); + return ; } ``` -* If you're building a plugin that runs on WordPress, you'd probably prefer consuming the package that ships with WordPress itself. This allows multiple plugins to reuse the same packages and avoid code duplication. In WordPress, these packages are available as WordPress scripts with a handle following this format `wp-package-name` (e.g. `wp-components`). Once you add the script to your own WordPress plugin scripts dependencies, the package will be available on the `wp` global variable. +- If you're building a plugin that runs on WordPress, you'd probably prefer consuming the package that ships with WordPress itself. This allows multiple plugins to reuse the same packages and avoid code duplication. In WordPress, these packages are available as WordPress scripts with a handle following this format `wp-package-name` (e.g. `wp-components`). Once you add the script to your own WordPress plugin scripts dependencies, the package will be available on the `wp` global variable. ```php // myplugin.php @@ -53,9 +51,7 @@ wp_register_script( 'myscript', 'pathtomyscript.js', array ('wp-components', "wp const { Button } = wp.components; function MyApp() { - return ( - - ); + return ; } ``` @@ -65,8 +61,8 @@ Script dependencies definition can be a tedious task for developers. Mistakes an Some production packages provide stylesheets to function properly. -* If you're using the package as an npm dependency, the stylesheets will be available on the `build-style` folder of the package. Make sure to load this style file on your application. -* If you're working in the context of WordPress, you'll have to enqueue these stylesheets or add them to your stylesheets dependencies. The stylesheet handles are the same as the script handles. +- If you're using the package as an npm dependency, the stylesheets will be available on the `build-style` folder of the package. Make sure to load this style file on your application. +- If you're working in the context of WordPress, you'll have to enqueue these stylesheets or add them to your stylesheets dependencies. The stylesheet handles are the same as the script handles. In the context of existing WordPress pages, if you omit to define the scripts or styles dependencies properly, your plugin might still work properly if these scripts and styles are already loaded there by WordPress or by other plugins, but it's highly recommended to define all your dependencies exhaustively if you want to avoid potential breakage in future versions. @@ -88,16 +84,16 @@ It's often surprising to new contributors to discover that the post editor is co The above [Why?](#why) section should provide some context for how individual packages aim to satisfy specific requirements. That applies to these packages as well: -- `@wordpress/block-editor` provides components for implementing a block editor, operating on a primitive value of an array of block objects. It makes no assumptions for how this value is saved, and has no awareness (or requirement) of a WordPress site. -- `@wordpress/editor` utilizes components from `@wordpress/block-editor`, and associates the loading and saving mechanism of the blocks value to a post and post content. With the awareness of the concept of a WordPress post, it also provides various components relevant for working with a post object in the context of an editor (e.g. a post title input component). This package can support editing posts of any post type, and does not assume that it is rendered in any particular WordPress screen, or in any particular layout arrangement. -- `@wordpress/edit-post` is the implementation of the "New Post" ("Edit Post") screen in the WordPress admin. It is responsible for the layout of the various components provided by `@wordpress/editor` and `@wordpress/block-editor`, with full awareness of how it is presented in the specific screen in the WordPress administrative dashboard. +- `@wordpress/block-editor` provides components for implementing a block editor, operating on a primitive value of an array of block objects. It makes no assumptions for how this value is saved, and has no awareness (or requirement) of a WordPress site. +- `@wordpress/editor` is the enhanced version of the block editor for WordPress posts. It utilizes components from the `@wordpress/block-editor` package. Having an awareness of the concept of a WordPress post, it associates the loading and saving mechanism of the value representing blocks to a post and its content. It also provides various components relevant for working with a post object in the context of an editor (e.g., a post title input component). This package can support editing posts of any post type and does not assume that rendering happens in any particular WordPress screen or layout arrangement. +- `@wordpress/edit-post` is the implementation of the "New Post" ("Edit Post") screen in the WordPress admin. It is responsible for the layout of the various components provided by `@wordpress/editor` and `@wordpress/block-editor`, with full awareness of how it is presented in the specific screen in the WordPress administrative dashboard. Structured this way, these packages can be used in a variety of combinations outside the use-case of the "New Post" screen: -- A `@wordpress/edit-site` or `@wordpress/edit-widgets` package can serve as similar implementations of a "Site Editor" or "Widgets Editor", in much the same way as `@wordpress/edit-post`. -- `@wordpress/editor` could be used in the implementation of the "Reusable Block" block, since it is essentially a nested block editor associated with the post type `wp_block`. -- `@wordpress/block-editor` could be used independently from WordPress, or with a completely different save mechanism. For example, it could be used for a comments editor for posts of a site. +- A `@wordpress/edit-site` or `@wordpress/edit-widgets` package can serve as similar implementations of a "Site Editor" or "Widgets Editor", in much the same way as `@wordpress/edit-post`. +- `@wordpress/editor` could be used in the implementation of the "Reusable Block" block, since it is essentially a nested block editor associated with the post type `wp_block`. +- `@wordpress/block-editor` could be used independently from WordPress, or with a completely different save mechanism. For example, it could be used for a comments editor for posts of a site. ## Going further - - [Package Reference](/docs/reference-guides/packages.md) +- [Package Reference](/docs/reference-guides/packages.md) diff --git a/docs/getting-started/tutorials/create-block/block-anatomy.md b/docs/getting-started/tutorials/create-block/block-anatomy.md index 0e99d24a3c2c7d..12a4ebc4d2c080 100644 --- a/docs/getting-started/tutorials/create-block/block-anatomy.md +++ b/docs/getting-started/tutorials/create-block/block-anatomy.md @@ -2,7 +2,7 @@ At its simplest, a block in the WordPress block editor is a JavaScript object with a specific set of properties. -**Note:** Block development uses ESNext syntax, this refers to the latest JavaScript standard. If this is unfamiliar, I recommend reviewing the [ESNext syntax documentation](/docs/how-to-guides/javascript/esnext-js.md) to familiarize yourself with the newer syntax used in modern JavaScript development. +**Note:** Block development uses ESNext syntax, this refers to the latest JavaScript standard. If this is unfamiliar, review the [ESNext syntax documentation](/docs/how-to-guides/javascript/esnext-js.md) to familiarize yourself with the newer syntax used in modern JavaScript development. Here is the complete code for registering a block: @@ -41,7 +41,7 @@ The **title** is the title of the block shown in the Inserter. The **icon** is the icon shown in the Inserter. The icon property expects any Dashicon name as a string, see [list of available icons](https://developer.wordpress.org/resource/dashicons/). You can also provide an SVG object, but for now it's easiest to just pick a Dashicon name. -The **category** specified is a string and must be one of: "common, formatting, layout, widgets, or embed". You can create your own custom category name, [see documentation for details](/docs/reference-guides/filters/block-filters.md#managing-block-categories). For this tutorial, I specified "widgets" as the category. +The **category** specified is a string and must be one of: "common, formatting, layout, widgets, or embed". You can create your own custom category name, [see documentation for details](/docs/reference-guides/filters/block-filters.md#managing-block-categories). The last two block object properties are **edit** and **save**, these are the key parts of a block. Both properties should be defined as functions. diff --git a/docs/getting-started/tutorials/create-block/submitting-to-block-directory.md b/docs/getting-started/tutorials/create-block/submitting-to-block-directory.md index 220c30a174ea49..fc32814c364a3d 100644 --- a/docs/getting-started/tutorials/create-block/submitting-to-block-directory.md +++ b/docs/getting-started/tutorials/create-block/submitting-to-block-directory.md @@ -60,6 +60,7 @@ The Block Editor allows you to indicate the category your block belongs in, maki - media - design - widgets +- theme - embed [Read more about categories.](/docs/reference-guides/block-api/block-metadata.md#category) diff --git a/docs/how-to-guides/block-based-theme/README.md b/docs/how-to-guides/block-based-theme/README.md index b6b31b24d0fff7..6497fd437bf559 100644 --- a/docs/how-to-guides/block-based-theme/README.md +++ b/docs/how-to-guides/block-based-theme/README.md @@ -12,25 +12,11 @@ This tutorial is up to date as of Gutenberg version 9.1. ## Table of Contents -<<<<<<< HEAD -<<<<<<< HEAD 1. [What is needed to create a block-based theme?](/docs/how-to-guides/block-based-themes/README.md#what-is-needed-to-create-a-block-based-theme) 2. [Creating the theme](/docs/how-to-guides/block-based-themes/README.md#creating-the-theme) 3. [Creating the templates and template parts](/docs/how-to-guides/block-based-themes/README.md#creating-the-templates-and-template-parts) - 4. [Experimental-theme.json - Global styles](/docs/how-to-guides/block-based-themes/README.md#experimental-theme-json-global-styles) + 4. [experimental-theme.json - Global styles](/docs/how-to-guides/block-based-themes/README.md#experimental-theme-json-global-styles) 5. [Adding blocks](/docs/how-to-guides/block-based-themes/block-based-themes-2-adding-blocks.md) -======= -======= ->>>>>>> 1d0f49bf2967c2a055bf474e9a342df142d44a06 - 1. [What is needed to create a block-based theme?](/docs/how-to-guides/block-based-theme/README.md#what-is-needed-to-create-a-block-based-theme) - 2. [Creating the theme](/docs/how-to-guides/block-based-theme/README.md#creating-the-theme) - 3. [Creating the templates and template parts](/docs/how-to-guides/block-based-theme/README.md#creating-the-templates-and-template-parts) - 4. [Experimental-theme.json - Global styles](/docs/how-to-guides/block-based-theme/README.md#experimental-theme-json-global-styles) - 5. [Adding blocks](/docs/how-to-guides/block-based-theme/block-based-themes-2-adding-blocks.md) -<<<<<<< HEAD ->>>>>>> Rename how-to-guides/block-based-themes to how-to-guides/block-based-theme -======= ->>>>>>> 1d0f49bf2967c2a055bf474e9a342df142d44a06 ## What is needed to create a block-based theme? diff --git a/docs/how-to-guides/designers/block-design.md b/docs/how-to-guides/designers/block-design.md index 256516b77215a4..c054e34da1fa7b 100644 --- a/docs/how-to-guides/designers/block-design.md +++ b/docs/how-to-guides/designers/block-design.md @@ -15,6 +15,12 @@ Since the block itself represents what will actually appear on the site, interac Basic block settings wonโ€™t always make sense in the context of the placeholder/content UI. As a secondary option, options that are critical to the functionality of a block can live in the block toolbar. The Block Toolbar is still highly contextual and visible on all screen sizes. One notable constraint with the Block Toolbar is that it is icon-based UI, so any controls that live in the Block Toolbar need to be ones that can effectively be communicated via an icon or icon group. +### Group Block Toolbar controls with related items + +The Block Toolbar groups controls in segments, hierarchically. The first segment contains block type controls, such as the block switcher, the drag handle, and the mover control. The second group contains common and specific block tools that affect the entire block, followed by inline formatting, and the "More" menu. Optionally "Meta" or "Other" groups can separate some tools in their own segment. + +![A screenshot showing examples of block toolbar segment groupings.](https://make.wordpress.org/design/files/2021/03/docs_block-toolbar-structure.png) + ### The Settings Sidebar should only be used for advanced, tertiary controls The Settings Sidebar is not visible by default on a small / mobile screen, and may also be collapsed in a desktop view. Therefore, it should not be relied on for anything that is necessary for the basic operation of the block. Pick good defaults, make important actions available in the block toolbar, and think of the Settings Sidebar as something that most users should not need to open. @@ -27,7 +33,7 @@ Each Settings Sidebar comes with an "Advanced" section by default. This area hou Setup states, sometimes referred to as "placeholders", can be used to walk users through an initial process before showing the live preview state of the block. The setup process gathers information from the user that is needed to render the block. A blockโ€™s setup state is indicated with a grey background to provide clear differentiation for the user. Not all blocks have setup states โ€” for example, the Paragraph block. -![An example of a gallery blockโ€™s setup state on a grey background](https://make.wordpress.org/design/files/2018/12/gallery-setup.png) +![An example of an image blockโ€™s setup state on a grey background](https://make.wordpress.org/design/files/2021/03/docs__gallery-setup-state.png) A setup state is **not** necessary if: @@ -52,6 +58,12 @@ In most cases, a blockโ€™s setup state is only shown once and then further custo ## Do's and Don'ts +### Block Toolbar + +Group toolbar controls in logical segments. Don't add a segment for each. + +![A screenshot comparing a block toolbar with good vs. bad toolbar segment groupings.](https://make.wordpress.org/design/files/2021/03/docs__block-toolbar-do-dont.png) + ### Block Identification A block should have a straightforward, short name so users can easily find it in the block library. A block named "YouTube" is easy to find and understand. The same block, named "Embedded Video (YouTube)", would be less clear and harder to find in the block library. diff --git a/docs/how-to-guides/themes/block-based-themes.md b/docs/how-to-guides/themes/block-based-themes.md index dc931cc203b11c..3368d80fcfff33 100644 --- a/docs/how-to-guides/themes/block-based-themes.md +++ b/docs/how-to-guides/themes/block-based-themes.md @@ -31,7 +31,7 @@ theme |__ ... ``` -The difference with existing WordPress themes is that the different templates in the template hierarchy, and template parts, are block templates instead of php files. In addition, this example includes an [`experimental-theme.json`](/docs/how-to-guides/themes/theme-json.md) file for some styles. +The difference with existing WordPress themes is that the different templates in the template hierarchy, and template parts, are block templates instead of php files. In addition, this example includes an [`experimental-theme.json`](/docs/how-to-guides/themes/theme-json.md) file for some styles. ## What is a block template? @@ -50,20 +50,16 @@ Here's an example of a block template:
-
- - -
+ +
-
- -

Footer

- -
+ +

Footer

+
``` @@ -72,23 +68,23 @@ Here's an example of a block template: Ultimately, any WordPress user with the correct capabilities (example: `administrator` WordPress role) will be able to access these templates in the WordPress admin, edit them in dedicated views and potentially export them as a theme. -As of Gutenberg 8.5, there are two ways to create and edit templates within Gutenberg. +As of Gutenberg 8.5, there are two ways to create and edit templates within Gutenberg. ### Edit templates within The "Appearance" section of WP-Admin You can navigate to the temporary "Templates" admin menu under "Appearance" `wp-admin/edit.php?post_type=wp_template` and use this as a playground to edit your templates. Add blocks here and switch to the code editor mode to grab the HTML of the template when you are done. Afterwards, you can paste that markup into a file in your theme directory. -Please note that the "Templates" admin menu under "Appearance" will _not_ list templates that are bundled with your theme. It only lists new templates created by the specific WordPress site you're working on. +Please note that the "Templates" admin menu under "Appearance" will _not_ list templates that are bundled with your theme. It only lists new templates created by the specific WordPress site you're working on. ### Edit Templates within the Full-site Editor -To begin, create a blank template file within your theme. For example: `mytheme/block-templates/index.html`. Afterwards, open the Full-site editor. Your new template should appear as the active template, and should be blank. Add blocks as you normally would using Gutenberg. You can add and create template parts directly using the "Template Parts" block. +To begin, create a blank template file within your theme. For example: `mytheme/block-templates/index.html`. Afterwards, open the Full-site editor. Your new template should appear as the active template, and should be blank. Add blocks as you normally would using Gutenberg. You can add and create template parts directly using the "Template Parts" block. -Repeat for any additional templates you'd like to bundle with your theme. +Repeat for any additional templates you'd like to bundle with your theme. -When you're done, click the "Export Theme" option in the "Tools" (ellipsis) menu of the site editor. This will provide you with a ZIP download of all the templates and template parts you've created in the site editor. These new HTML files can be placed directly into your theme. +When you're done, click the "Export Theme" option in the "Tools" (ellipsis) menu of the site editor. This will provide you with a ZIP download of all the templates and template parts you've created in the site editor. These new HTML files can be placed directly into your theme. -Note that when you export template parts this way, the template part block markup will include a `postID` attribute that can be safely removed when distributing your theme. +Note that when you export template parts this way, the template part block markup will include a `postID` attribute that can be safely removed when distributing your theme. ## Templates CPT @@ -102,33 +98,33 @@ Note that it won't take precedence over any of your theme's templates with highe Some blocks have been made specifically for block-based themes. For example, you'll most likely use the **Site Title** block in your site's header while your **single** block template will most likely include a **Post Title** and a **Post Content** block. -As we're still early in the process, the number of blocks specifically dedicated to these block templates is relatively small but more will be added as we move forward with the project. As of Gutenberg 8.5, the following blocks are currently available: - -- Site Title -- Template Part -- Query -- Query Loop -- Query Pagination -- Post Title -- Post Content -- Post Author -- Post Comment -- Post Comment Author -- Post Comment Date -- Post Comments -- Post Comments Count -- Post Comments Form -- Post Date -- Post Excerpt -- Post Featured Image -- Post Hierarchical Terms -- Post Tags +As we're still early in the process, the number of blocks specifically dedicated to these block templates is relatively small but more will be added as we move forward with the project. As of Gutenberg 8.5, the following blocks are currently available: + +- Site Title +- Template Part +- Query +- Query Loop +- Query Pagination +- Post Title +- Post Content +- Post Author +- Post Comment +- Post Comment Author +- Post Comment Date +- Post Comments +- Post Comments Count +- Post Comments Form +- Post Date +- Post Excerpt +- Post Featured Image +- Post Hierarchical Terms +- Post Tags ## Styling -One of the most important aspects of themes (if not the most important) is the styling. While initially you'll be able to provide styles and enqueue them using the same hooks themes have always used, the [Global Styles](/docs/how-to-guides/themes/theme-json.md) effort will provide a scaffolding for adding many theme styles in the future. +One of the most important aspects of themes (if not the most important) is the styling. While initially you'll be able to provide styles and enqueue them using the same hooks themes have always used, the [Global Styles](/docs/how-to-guides/themes/theme-json.md) effort will provide a scaffolding for adding many theme styles in the future. ## Resources -- [Full Site Editing](https://github.com/WordPress/gutenberg/labels/%5BFeature%5D%20Full%20Site%20Editing) label. -- [Theme Experiments](https://github.com/WordPress/theme-experiments) repository, full of block-based theme examples created by the WordPress community. +- [Full Site Editing](https://github.com/WordPress/gutenberg/labels/%5BFeature%5D%20Full%20Site%20Editing) label. +- [Theme Experiments](https://github.com/WordPress/theme-experiments) repository, full of block-based theme examples created by the WordPress community. diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index 74831234b7f547..e9c6b4f3f1fb1d 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -6,18 +6,18 @@ This is documentation for the current direction and work in progress about how themes can hook into the various sub-systems that the Block Editor provides. -- Rationale - - Settings can be controlled per block - - Some block styles are managed - - CSS Custom Properties: presets & custom -- Specification - - Settings - - Styles - - Other theme metadata -- FAQ - - The naming schema of CSS Custom Properties - - Why using -- as a separator? - - How settings under "custom" create new CSS Custom Properties +- Rationale + - Settings can be controlled per block + - Some block styles are managed + - CSS Custom Properties: presets & custom +- Specification + - Settings + - Styles + - Other theme metadata +- FAQ + - The naming schema of CSS Custom Properties + - Why using -- as a separator? + - How settings under "custom" create new CSS Custom Properties ## Rationale @@ -25,15 +25,23 @@ The Block Editor API has evolved at different velocities and there are some grow This describes the current efforts to consolidate the various APIs related to styles into a single point โ€“ a `experimental-theme.json` file that should be located inside the root of the theme directory. +### Global settings for the block editor + +Instead of the proliferation of theme support flags or alternative methods, the `experimental-theme.json` files provides a canonical way to define the settings of the block editor. These settings includes things like: + +- What customization options should be made available or hidden from the user. +- What are the default colors, font sizes... available to the user. +- Defines the default layout of the editor. (widths and available alignments). + ### Settings can be controlled per block -The Block Editor already allows the control of specific settings such as alignment, drop cap, presets available, etc. All of these work at the block level. By using the `experimental-theme.json` we aim to allow themes to control these at a block level. +For more granularity, these settings also work at the block level in `experimental-theme.json`. Examples of what can be achieved are: -- Use a particular preset for a block (e.g.: table) but the common one for the rest of blocks. -- Enable font size UI controls for all blocks that support it but the headings block. -- etc. +- Use a particular preset for a block (e.g.: table) but the common one for the rest of blocks. +- Enable font size UI controls for all blocks but the headings block. +- etc. ### Some block styles are managed @@ -41,8 +49,8 @@ By using the `experimental-theme.json` file to set style properties in a structu Some of the advantages are: -- Reduce the amount of CSS enqueued. -- Prevent specificity wars. +- Reduce the amount of CSS enqueued. +- Prevent specificity wars. ### CSS Custom Properties @@ -50,66 +58,74 @@ There are some areas of styling that would benefit from having shared values tha To address this need, we've started to experiment with CSS Custom Properties, aka CSS Variables, in some places: -- **Presets**: [color palettes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes), [font sizes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-font-sizes), or [gradients](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets) declared by the theme are converted to CSS Custom Properties and enqueued both the front-end and the editors. +- **Presets**: [color palettes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-color-palettes), [font sizes](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-font-sizes), or [gradients](https://developer.wordpress.org/block-editor/developers/themes/theme-support/#block-gradient-presets) declared by the theme are converted to CSS Custom Properties and enqueued both the front-end and the editors. {% codetabs %} {% Input %} + ```json { - "settings": { - "defaults": { - "color": { - "palette": [ - { - "name": "Black", - "slug": "black", - "color": "#000000" - }, - { - "name": "White", - "slug": "white", - "color": "#ffffff" - } - ], - }, - }, - }, + "settings": { + "defaults": { + "color": { + "palette": [ + { + "name": "Black", + "slug": "black", + "color": "#000000" + }, + { + "name": "White", + "slug": "white", + "color": "#ffffff" + } + ] + } + } + } } ``` + {% Output %} + ```css :root { - --wp--preset--color--black: #000000; - --wp--preset--color--white: #ffffff; + --wp--preset--color--black: #000000; + --wp--preset--color--white: #ffffff; } ``` + {% end %} -- **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. +- **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. {% codetabs %} {% Input %} + ```json { - "settings": { - "defaults": { - "custom": { - "line-height": { - "body": 1.7, - "heading": 1.3 - }, - }, - }, - }, + "settings": { + "defaults": { + "custom": { + "line-height": { + "body": 1.7, + "heading": 1.3 + } + } + } + } } ``` + {% Output %} + ```css :root { - --wp--custom--line-height--body: 1.7; - --wp--custom--line-height--heading: 1.3; + --wp--custom--line-height--body: 1.7; + --wp--custom--line-height--heading: 1.3; } ``` + {% end %} ## Specification @@ -156,7 +172,11 @@ The settings section has the following structure and default values: ``` { "settings": { - "some/block": { + "defaults": { + "layout": { /* Default layout to be used in the post editor */ + "contentSize": "800px", + "wideSize": "1000px", + } "border": { "customRadius": false /* true to opt-in */ }, @@ -219,59 +239,72 @@ For example: {% codetabs %} {% Input %} + ```json { - "settings": { - "defaults": { - "color": { - "palette": [ - { - "slug": "strong-magenta", - "color": "#a156b4" - }, - { - "slug": "very-dark-grey", - "color": "rgb(131, 12, 8)" - } - ], - "gradients": [ - { - "slug": "blush-bordeaux", - "gradient": "linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%)" - }, - { - "slug": "blush-light-purple", - "gradient": "linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%)" - }, - ] - }, - "typography": { - "fontSizes": [ - { - "slug": "normal", - "size": 16 - }, - { - "slug": "big", - "size": 32 - } - ] - } - } - } + "settings": { + "defaults": { + "color": { + "palette": [ + { + "slug": "strong-magenta", + "color": "#a156b4" + }, + { + "slug": "very-dark-grey", + "color": "rgb(131, 12, 8)" + } + ], + "gradients": [ + { + "slug": "blush-bordeaux", + "gradient": "linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%)" + }, + { + "slug": "blush-light-purple", + "gradient": "linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%)" + } + ] + }, + "typography": { + "fontSizes": [ + { + "slug": "normal", + "size": 16 + }, + { + "slug": "big", + "size": 32 + } + ] + } + } + } } ``` + {% Output %} + ```css :root { - --wp--preset--color--strong-magenta: #a156b4; - --wp--preset--color--very-dark-gray: #444; - --wp--preset--font-size--big: 32; - --wp--preset--font-size--normal: 16; - --wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%); - --wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%); + --wp--preset--color--strong-magenta: #a156b4; + --wp--preset--color--very-dark-gray: #444; + --wp--preset--font-size--big: 32; + --wp--preset--font-size--normal: 16; + --wp--preset--gradient--blush-bordeaux: linear-gradient( + 135deg, + rgb( 254, 205, 165 ) 0%, + rgb( 254, 45, 45 ) 50%, + rgb( 107, 0, 62 ) 100% + ); + --wp--preset--gradient--blush-light-purple: linear-gradient( + 135deg, + rgb( 255, 206, 236 ) 0%, + rgb( 152, 150, 240 ) 100% + ); } ``` + {% end %} To maintain backward compatibility, the presets declared via `add_theme_support` will also generate the CSS Custom Properties. If the `experimental-theme.json` contains any presets, these will take precedence over the ones declared via `add_theme_support`. @@ -284,71 +317,75 @@ For example: {% codetabs %} {% Input %} + ```json { - "settings": { - "defaults": { - "custom": { - "base-font": 16, - "line-height": { - "small": 1.2, - "medium": 1.4, - "large": 1.8 - } - } - } - } + "settings": { + "defaults": { + "custom": { + "base-font": 16, + "line-height": { + "small": 1.2, + "medium": 1.4, + "large": 1.8 + } + } + } + } } ``` + {% Output %} + ```css :root { - --wp--custom--base-font: 16; - --wp--custom--line-height--small: 1.2; - --wp--custom--line-height--medium: 1.4; - --wp--custom--line-height--large: 1.8; + --wp--custom--base-font: 16; + --wp--custom--line-height--small: 1.2; + --wp--custom--line-height--medium: 1.4; + --wp--custom--line-height--large: 1.8; } ``` + {% end %} Note that, the name of the variable is created by adding `--` in between each nesting level. ### Styles -Each block declares which style properties it exposes via the [block supports mechanism](../block-api/block-supports.md). The support declarations are used to automatically generate the UI controls for the block in the editor, as well as being available through the `experimental-theme.json` file for themes to target. +Each block declares which style properties it exposes via the [block supports mechanism](../block-api/block-supports.md). The support declarations are used to automatically generate the UI controls for the block in the editor. Themes can use any style property via the `experimental-theme.json` for any block โ€• it's the theme's responsibility to verify that it works properly according to the block markup, etc. ```json { - "styles": { - "some/block/selector": { - "border": { - "radius": "value" - }, - "color": { - "background": "value", - "gradient": "value", - "link": "value", - "text": "value" - }, - "spacing": { - "padding": { - "top": "value", - "right": "value", - "bottom": "value", - "left": "value", - }, - }, - "typography": { - "fontFamily": "value", - "fontSize": "value", - "fontStyle": "value", - "fontWeight": "value", - "lineHeight": "value", - "textDecoration": "value", - "textTransform": "value" - } - } - } + "styles": { + "some/block/selector": { + "border": { + "radius": "value" + }, + "color": { + "background": "value", + "gradient": "value", + "link": "value", + "text": "value" + }, + "spacing": { + "padding": { + "top": "value", + "right": "value", + "bottom": "value", + "left": "value" + } + }, + "typography": { + "fontFamily": "value", + "fontSize": "value", + "fontStyle": "value", + "fontWeight": "value", + "lineHeight": "value", + "textDecoration": "value", + "textTransform": "value" + } + } + } } ``` @@ -356,137 +393,74 @@ For example: {% codetabs %} {% Input %} + ```json { - "styles": { - "root": { - "color": { - "text": "var(--wp--preset--color--primary)" - }, - }, - "core/heading/h1": { - "color": { - "text": "var(--wp--preset--color--primary)" - }, - "typography": { - "fontSize": "calc(1px * var(--wp--preset--font-size--huge))" - } - }, - "core/heading/h4": { - "color": { - "text": "var(--wp--preset--color--secondary)" - }, - "typography": { - "fontSize": "var(--wp--preset--font-size--normal)" - } - } - } + "styles": { + "root": { + "color": { + "text": "var(--wp--preset--color--primary)" + } + }, + "core/heading/h1": { + "color": { + "text": "var(--wp--preset--color--primary)" + }, + "typography": { + "fontSize": "calc(1px * var(--wp--preset--font-size--huge))" + } + }, + "core/heading/h4": { + "color": { + "text": "var(--wp--preset--color--secondary)" + }, + "typography": { + "fontSize": "var(--wp--preset--font-size--normal)" + } + } + } } ``` + {% Output %} + ```css :root { - color: var(--wp--preset--color--primary); + color: var( --wp--preset--color--primary ); } h1 { - color: var(--wp--preset--color--primary); - font-size: calc(1px * var(--wp--preset--font-size--huge)); + color: var( --wp--preset--color--primary ); + font-size: calc( 1px * var( --wp--preset--font-size--huge ) ); } h4 { - color: var(--wp--preset--color--secondary); - font-size: calc(1px * var(--wp--preset--font-size--normal)); + color: var( --wp--preset--color--secondary ); + font-size: calc( 1px * var( --wp--preset--font-size--normal ) ); } ``` + {% end %} The `defaults` block selector can't be part of the `styles` section and will be ignored if it's present. The `root` block selector will generate a style rule with the `:root` CSS selector. -#### Border Properties - -| Block | Color | Radius | Style | Width | -| --- | --- | --- | --- | --- | -| Group | Yes | Yes | Yes | Yes | -| Image | Yes | - | - | - | - -#### Color Properties - -These are the current color properties supported by blocks: - -| Block | Background | Gradient | Link | Text | -| --- | --- | --- | --- | --- | -| Global | Yes | Yes | Yes | Yes | -| Columns | Yes | Yes | Yes | Yes | -| Group | Yes | Yes | Yes | Yes | -| Heading [1] | Yes | - | Yes | Yes | -| List | Yes | Yes | - | Yes | -| Media & text | Yes | Yes | Yes | Yes | -| Navigation | Yes | - | - | Yes | -| Paragraph | Yes | - | Yes | Yes | -| Post Author | Yes | Yes | Yes | Yes | -| Post Comments | Yes | Yes | Yes | Yes | -| Post Comments Count | Yes | Yes | - | Yes | -| Post Comments Form | Yes | Yes | Yes | Yes | -| Post Date | Yes | Yes | - | Yes | -| Post Excerpt | Yes | Yes | Yes | Yes | -| Post Hierarchical Terms | Yes | Yes | Yes | Yes | -| Post Tags | Yes | Yes | Yes | Yes | -| Post Title | Yes | Yes | - | Yes | -| Site Tagline | Yes | Yes | - | Yes | -| Site Title | Yes | Yes | - | Yes | -| Template Part | Yes | Yes | Yes | Yes | - -[1] The heading block represents 6 distinct HTML elements: H1-H6. It comes with selectors to target each individual element (ex: core/heading/h1 for H1, etc). - -#### Spacing Properties - -| Block | Padding | -| --- | --- | -| Cover | Yes | -| Group | Yes | - -#### Typography Properties - -These are the current typography properties supported by blocks: - -| Block | Font Family | Font Size | Font Style | Font Weight | Line Height | Text Decoration | Text Transform | -| --- | --- | --- | --- | --- | --- | --- | --- | -| Global | Yes | Yes | Yes | Yes | Yes | Yes | Yes | -| Code | - | Yes | - | - | - | - | - | -| Heading [1] | - | Yes | - | - | Yes | - | - | -| List | - | Yes | - | - | - | - | - | -| Navigation | Yes | Yes | Yes | Yes | - | Yes | Yes | -| Paragraph | - | Yes | - | - | Yes | - | - | -| Post Author | - | Yes | - | - | Yes | - | - | -| Post Comments | - | Yes | - | - | Yes | - | - | -| Post Comments Count | - | Yes | - | - | Yes | - | - | -| Post Comments Form | - | Yes | - | - | Yes | - | - | -| Post Date | - | Yes | - | - | Yes | - | - | -| Post Excerpt | - | Yes | - | - | Yes | - | - | -| Post Hierarchical Terms | - | Yes | - | - | Yes | - | - | -| Post Tags | - | Yes | - | - | Yes | - | - | -| Post Title | Yes | Yes | - | - | Yes | - | - | -| Preformatted | - | Yes | - | - | - | - | - | -| Site Tagline | Yes | Yes | - | - | Yes | - | - | -| Site Title | Yes | Yes | - | - | Yes | - | Yes | -| Verse | Yes | Yes | - | - | - | - | - | - -[1] The heading block represents 6 distinct HTML elements: H1-H6. It comes with selectors to target each individual element (ex: core/heading/h1 for H1, etc). - - ### Other theme metadata There's a growing need to add more theme metadata to the theme.json. This section lists those other fields: -**customTemplates**: within this field themes can list the custom templates present in the `block-templates` folder, the keys should match the custom template name. For example, for a custom template named `my-custom-template.html`, the `theme.json` can declare what post types can use it and what's the title to show the user: +**customTemplates**: within this field themes can list the custom templates present in the `block-templates` folder. For example, for a custom template named `my-custom-template.html`, the `theme.json` can declare what post types can use it and what's the title to show the user: ```json { - "customTemplates": { - "my-custom-template": { - "title": "The template title", /* Mandatory */ - "postTypes": [ "page", "post", "my-cpt" ] /* Optional, will only apply to "page" by default. */ - } - } + "customTemplates": [ + { + "name": "my-custom-template" /* Mandatory */, + "title": "The template title" /* Mandatory, translatable */, + "postTypes": [ + "page", + "post", + "my-cpt" + ] /* Optional, will only apply to "page" by default. */ + } + ] } ``` @@ -498,21 +472,21 @@ One thing you may have noticed is the naming schema used for the CSS Custom Prop **Presets** such as `--wp--preset--color--black` can be divided into the following chunks: -- `--wp`: prefix to namespace the CSS variable. -- `preset `: indicates is a CSS variable that belongs to the presets. -- `color`: indicates which preset category the variable belongs to. It can be `color`, `font-size`, `gradients`. -- `black`: the `slug` of the particular preset value. +- `--wp`: prefix to namespace the CSS variable. +- `preset `: indicates is a CSS variable that belongs to the presets. +- `color`: indicates which preset category the variable belongs to. It can be `color`, `font-size`, `gradients`. +- `black`: the `slug` of the particular preset value. **Custom** properties such as `--wp--custom--line-height--body`, which can be divided into the following chunks: -- `--wp`: prefix to namespace the CSS variable. -- `custom`: indicates is a "free-form" CSS variable created by the theme. -- `line-height--body`: the result of converting the "custom" object keys into a string. +- `--wp`: prefix to namespace the CSS variable. +- `custom`: indicates is a "free-form" CSS variable created by the theme. +- `line-height--body`: the result of converting the "custom" object keys into a string. The `--` as a separator has two functions: -- Readibility, for human understanding. It can be thought as similar to the BEM naming schema, it separates "categories". -- Parseability, for machine understanding. Using a defined structure allows machines to understand the meaning of the property `--wp--preset--color--black`: it's a value bounded to the color preset whose slug is "black", which then gives us room to do more things with them. +- Readibility, for human understanding. It can be thought as similar to the BEM naming schema, it separates "categories". +- Parseability, for machine understanding. Using a defined structure allows machines to understand the meaning of the property `--wp--preset--color--black`: it's a value bounded to the color preset whose slug is "black", which then gives us room to do more things with them. ### Why using `--` as a separator? @@ -532,46 +506,49 @@ For example: {% codetabs %} {% Input %} + ```json { - "settings": { - "defaults": { - "custom": { - "lineHeight": { - "body": 1.7 - }, - "font-primary": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif" - }, - }, - }, + "settings": { + "defaults": { + "custom": { + "lineHeight": { + "body": 1.7 + }, + "font-primary": "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif" + } + } + } } ``` + {% Output %} + ```css :root { - --wp--custom--line-height--body: 1.7; - --wp--custom--font-primary: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif", + --wp--custom--line-height--body: 1.7; + --wp--custom--font-primary: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', sans-serif"; } ``` + {% end %} A few notes about this process: -- `camelCased` keys are transformed into its `kebab-case` form, as to follow the CSS property naming schema. Example: `lineHeight` is transformed into `line-height`. -- Keys at different depth levels are separated by `--`. That's why `line-height` and `body` are separated by `--`. -- You shouldn't use `--` in the names of the keys within the `custom` object. Example, **don't do** this: - +- `camelCased` keys are transformed into its `kebab-case` form, as to follow the CSS property naming schema. Example: `lineHeight` is transformed into `line-height`. +- Keys at different depth levels are separated by `--`. That's why `line-height` and `body` are separated by `--`. +- You shouldn't use `--` in the names of the keys within the `custom` object. Example, **don't do** this: ```json { - "settings": { - "defaults": { - "custom": { - "line--height": { - "body": 1.7 - }, - }, - }, - }, + "settings": { + "defaults": { + "custom": { + "line--height": { + "body": 1.7 + } + } + } + } } ``` diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index 8ef4491e9756cd..ccbcb925950b11 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -128,6 +128,7 @@ The core provided categories are: - media - design - widgets +- theme - embed Plugins and Themes can also register [custom block categories](/docs/reference-guides/filters/block-filters.md#managing-block-categories). diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md index 5ac9d2a3e78bb3..34baf7a7e124d1 100644 --- a/docs/reference-guides/block-api/block-registration.md +++ b/docs/reference-guides/block-api/block-registration.md @@ -60,6 +60,7 @@ The core provided categories are: - media - design - widgets +- theme - embed ```js @@ -226,8 +227,6 @@ example: { Similarly to how the block's style variations can be declared, a block type can define block variations that the user can pick from. The difference is that, rather than changing only the visual appearance, this field provides a way to apply initial custom attributes and inner blocks at the time when a block is inserted. See the [Block Variations API](/docs/reference-guides/block-api/block-variations.md) for more details. - - #### supports (optional) - **_Type:_** `Object` diff --git a/docs/reference-guides/richtext.md b/docs/reference-guides/richtext.md index 98a22256b1dbfc..e5c3edbc4ea7f6 100644 --- a/docs/reference-guides/richtext.md +++ b/docs/reference-guides/richtext.md @@ -110,7 +110,7 @@ While using the RichText component a number of common issues tend to appear. In some cases the placeholder content on RichText can appear separate from the input where you would write your content. This is likely due to one of two reasons: -1. You can't use an [inline HTML element](https://developer.mozilla.org/en-US/docs/Web/HTML/Inline_elements) as the RichText component. If your `tagName` property is using an inline element such as `span`, `a` or `code`, it needs to be changed to a [block-level element](https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements). +1. You can't have an element with the CSS `display` property set to `inline`. You will need to set it to `inline-block` or any other value. 2. The `position` CSS property value for the element must be set to `relative` or `absolute` within the admin. If the styles within style.css or editor.css modify the `position` property value for this element, you may see issues with how it displays. ### HTML Formatting Tags Display in the Content diff --git a/gutenberg.php b/gutenberg.php index a1c216f7c78c34..3aa79859396ab3 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,9 +3,9 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Requires at least: 5.3 + * Requires at least: 5.6 * Requires PHP: 5.6 - * Version: 10.2.0-rc.1 + * Version: 10.3.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/lib/block-supports/border.php b/lib/block-supports/border.php index 0861119c3a32f3..feecb788443a29 100644 --- a/lib/block-supports/border.php +++ b/lib/block-supports/border.php @@ -38,6 +38,16 @@ function gutenberg_register_border_support( $block_type ) { * @return array Border CSS classes and inline styles. */ function gutenberg_apply_border_support( $block_type, $block_attributes ) { + $border_support = _wp_array_get( $block_type->supports, array( '__experimentalBorder' ), false ); + + if ( + is_array( $border_support ) && + array_key_exists( '__experimentalSkipSerialization', $border_support ) && + $border_support['__experimentalSkipSerialization'] + ) { + return array(); + } + // Arrays used to ease addition of further border related features in future. $styles = array(); diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php new file mode 100644 index 00000000000000..57a0576c7be36b --- /dev/null +++ b/lib/block-supports/layout.php @@ -0,0 +1,149 @@ +supports, array( '__experimentalLayout' ), false ); + } + if ( $support_layout ) { + if ( ! $block_type->attributes ) { + $block_type->attributes = array(); + } + + if ( ! array_key_exists( 'layout', $block_type->attributes ) ) { + $block_type->attributes['layout'] = array( + 'type' => 'object', + ); + } + } +} + +/** + * Renders the layout config to the block wrapper. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_render_layout_support_flag( $block_content, $block ) { + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); + $support_layout = false; + if ( $block_type && property_exists( $block_type, 'supports' ) ) { + $support_layout = _wp_array_get( $block_type->supports, array( '__experimentalLayout' ), false ); + } + if ( ! $support_layout || ! isset( $block['attrs']['layout'] ) ) { + return $block_content; + } + + $used_layout = $block['attrs']['layout']; + if ( isset( $used_layout['inherit'] ) && $used_layout['inherit'] ) { + $tree = WP_Theme_JSON_Resolver::get_merged_data( array(), 'theme' ); + $default_layout = _wp_array_get( $tree->get_settings(), array( 'defaults', 'layout' ) ); + if ( ! $default_layout ) { + return $block_content; + } + $used_layout = $default_layout; + } + + $id = uniqid(); + $content_size = isset( $used_layout['contentSize'] ) ? $used_layout['contentSize'] : null; + $wide_size = isset( $used_layout['wideSize'] ) ? $used_layout['wideSize'] : null; + + $style = ''; + if ( $content_size || $wide_size ) { + ob_start(); + ?> + > * { + max-width: ; + margin-left: auto; + margin-right: auto; + } + + > .alignwide { + max-width: ; + } + + .alignfull { + max-width: none; + } + + .alignleft { + float: left; + margin-right: 2em; + } + + .alignright { + float: right; + margin-left: 2em; + } + ' . $style . ''; +} + +// Register the block support. +WP_Block_Supports::get_instance()->register( + 'layout', + array( + 'register_attribute' => 'gutenberg_register_layout_support', + ) +); +add_filter( 'render_block', 'gutenberg_render_layout_support_flag', 10, 2 ); + +/** + * For themes without theme.json file, make sure + * to restore the inner div for the group block + * to avoid breaking styles relying on that div. + * + * @param string $block_content Rendered block content. + * @param array $block Block object. + * @return string Filtered block content. + */ +function gutenberg_restore_group_inner_container( $block_content, $block ) { + $group_with_inner_container_regex = '/(^\s*]*wp-block-group(\s|")[^>]*>)(\s*]*wp-block-group__inner-container(\s|")[^>]*>)((.|\S|\s)*)/'; + + if ( + 'core/group' !== $block['blockName'] || + WP_Theme_JSON_Resolver::theme_has_support() || + 1 === preg_match( $group_with_inner_container_regex, $block_content ) + ) { + return $block_content; + } + + $replace_regex = '/(^\s*]*wp-block-group[^>]*>)(.*)(<\/div>\s*$)/ms'; + $updated_content = preg_replace_callback( + $replace_regex, + function( $matches ) { + return $matches[1] . '
' . $matches[2] . '
' . $matches[3]; + }, + $block_content + ); + return $updated_content; +} + +add_filter( 'render_block', 'gutenberg_restore_group_inner_container', 10, 2 ); diff --git a/lib/blocks.php b/lib/blocks.php index 4cd8fe50a8fd34..5ae68252a61556 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -47,51 +47,52 @@ function gutenberg_reregister_core_block_types() { 'video', 'embed', ), - 'block_names' => array_merge( - array( - 'archives.php' => 'core/archives', - 'block.php' => 'core/block', - 'calendar.php' => 'core/calendar', - 'categories.php' => 'core/categories', - 'cover.php' => 'core/cover', - 'latest-comments.php' => 'core/latest-comments', - 'latest-posts.php' => 'core/latest-posts', - 'navigation.php' => 'core/navigation', - 'navigation-link.php' => 'core/navigation-link', - 'rss.php' => 'core/rss', - 'search.php' => 'core/search', - 'shortcode.php' => 'core/shortcode', - 'social-link.php' => 'core/social-link', - 'tag-cloud.php' => 'core/tag-cloud', - 'page-list.php' => 'core/page-list', - 'post-author.php' => 'core/post-author', - 'post-comment.php' => 'core/post-comment', - 'post-comment-author.php' => 'core/post-comment-author', - 'post-comment-content.php' => 'core/post-comment-content', - 'post-comment-date.php' => 'core/post-comment-date', - 'post-comments.php' => 'core/post-comments', - 'post-comments-count.php' => 'core/post-comments-count', - 'post-comments-form.php' => 'core/post-comments-form', - 'post-content.php' => 'core/post-content', - 'post-date.php' => 'core/post-date', - 'post-excerpt.php' => 'core/post-excerpt', - 'post-featured-image.php' => 'core/post-featured-image', - 'post-hierarchical-terms.php' => 'core/post-hierarchical-terms', - 'post-navigation-link.php' => 'core/post-navigation-link', - 'post-tags.php' => 'core/post-tags', - 'post-title.php' => 'core/post-title', - 'query.php' => 'core/query', - 'query-loop.php' => 'core/query-loop', - 'query-pagination.php' => 'core/query-pagination', - 'query-pagination-next.php' => 'core/query-pagination-next', - 'query-pagination-numbers.php' => 'core/query-pagination-numbers', - 'query-pagination-previous.php' => 'core/query-pagination-previous', - 'site-logo.php' => 'core/site-logo', - 'site-tagline.php' => 'core/site-tagline', - 'site-title.php' => 'core/site-title', - // 'table-of-contents.php' => 'core/table-of-contents', - 'template-part.php' => 'core/template-part', - ) + 'block_names' => array( + 'archives.php' => 'core/archives', + 'block.php' => 'core/block', + 'calendar.php' => 'core/calendar', + 'categories.php' => 'core/categories', + 'cover.php' => 'core/cover', + 'latest-comments.php' => 'core/latest-comments', + 'latest-posts.php' => 'core/latest-posts', + 'loginout.php' => 'core/loginout', + 'navigation.php' => 'core/navigation', + 'navigation-link.php' => 'core/navigation-link', + 'rss.php' => 'core/rss', + 'search.php' => 'core/search', + 'shortcode.php' => 'core/shortcode', + 'social-link.php' => 'core/social-link', + 'tag-cloud.php' => 'core/tag-cloud', + 'page-list.php' => 'core/page-list', + 'post-author.php' => 'core/post-author', + 'post-comment.php' => 'core/post-comment', + 'post-comment-author.php' => 'core/post-comment-author', + 'post-comment-content.php' => 'core/post-comment-content', + 'post-comment-date.php' => 'core/post-comment-date', + 'post-comments.php' => 'core/post-comments', + 'post-comments-count.php' => 'core/post-comments-count', + 'post-comments-form.php' => 'core/post-comments-form', + 'post-content.php' => 'core/post-content', + 'post-date.php' => 'core/post-date', + 'post-excerpt.php' => 'core/post-excerpt', + 'post-featured-image.php' => 'core/post-featured-image', + 'post-hierarchical-terms.php' => 'core/post-hierarchical-terms', + 'post-navigation-link.php' => 'core/post-navigation-link', + 'post-tags.php' => 'core/post-tags', + 'post-title.php' => 'core/post-title', + 'query.php' => 'core/query', + 'query-loop.php' => 'core/query-loop', + 'query-title.php' => 'core/query-title', + 'query-pagination.php' => 'core/query-pagination', + 'query-pagination-next.php' => 'core/query-pagination-next', + 'query-pagination-numbers.php' => 'core/query-pagination-numbers', + 'query-pagination-previous.php' => 'core/query-pagination-previous', + 'site-logo.php' => 'core/site-logo', + 'site-tagline.php' => 'core/site-tagline', + 'site-title.php' => 'core/site-title', + // 'table-of-contents.php' => 'core/table-of-contents', + 'template-part.php' => 'core/template-part', + 'term-description.php' => 'core/term-description', ), ), __DIR__ . '/../build/edit-widgets/blocks/' => array( @@ -130,23 +131,17 @@ function gutenberg_reregister_core_block_types() { register_block_type_from_metadata( $block_json_file ); } - foreach ( $block_names as $file => $block_names ) { + foreach ( $block_names as $file => $sub_block_names ) { if ( ! file_exists( $blocks_dir . $file ) ) { return; } - if ( is_string( $block_names ) ) { - if ( $registry->is_registered( $block_names ) ) { - $registry->unregister( $block_names ); - } - gutenberg_register_core_block_styles( $block_names ); - } elseif ( is_array( $block_names ) ) { - foreach ( $block_names as $block_name ) { - if ( $registry->is_registered( $block_name ) ) { - $registry->unregister( $block_name ); - } - gutenberg_register_core_block_styles( $block_name ); + $sub_block_names_normalized = is_string( $sub_block_names ) ? array( $sub_block_names ) : $sub_block_names; + foreach ( $sub_block_names_normalized as $block_name ) { + if ( $registry->is_registered( $block_name ) ) { + $registry->unregister( $block_name ); } + gutenberg_register_core_block_styles( $block_name ); } require $blocks_dir . $file; @@ -349,3 +344,35 @@ function gutenberg_register_legacy_social_link_blocks() { } add_action( 'init', 'gutenberg_register_legacy_social_link_blocks' ); + +/** + * Filters the default block categories array to add a new one for themes. + * + * This can be removed when plugin support requires WordPress 5.8.0+. + * + * @see https://core.trac.wordpress.org/ticket/52883 + * + * @param array[] $categories The list of default block categories. + * + * @return array[] Filtered block categories. + */ +function gutenberg_register_theme_block_category( $categories ) { + foreach ( $categories as $category ) { + // Skip when the category is already set in WordPress core. + if ( + isset( $category['slug'] ) && + 'theme' === $category['slug'] + ) { + return $categories; + } + } + + $categories[] = array( + 'slug' => 'theme', + 'title' => _x( 'Theme', 'block category', 'gutenberg' ), + 'icon' => null, + ); + return $categories; +} + +add_filter( 'block_categories', 'gutenberg_register_theme_block_category' ); diff --git a/lib/class-wp-rest-widget-types-controller.php b/lib/class-wp-rest-widget-types-controller.php index a2cd76aac0da76..8e68cc40bd4c5f 100644 --- a/lib/class-wp-rest-widget-types-controller.php +++ b/lib/class-wp-rest-widget-types-controller.php @@ -68,6 +68,39 @@ public function register_routes() { ) ); + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P[a-zA-Z0-9_-]+)/encode', + array( + 'args' => array( + 'id' => array( + 'description' => __( 'The widget type id.', 'gutenberg' ), + 'type' => 'string', + 'required' => true, + ), + 'instance' => array( + 'description' => __( 'Current instance settings of the widget.', 'gutenberg' ), + 'type' => 'object', + ), + 'form_data' => array( + 'description' => __( 'Serialized widget form data to encode into instance settings.', 'gutenberg' ), + 'type' => 'string', + 'sanitize_callback' => function( $string ) { + $array = array(); + wp_parse_str( $string, $array ); + return $array; + }, + ), + ), + array( + 'methods' => WP_REST_Server::CREATABLE, + 'permission_callback' => array( $this, 'get_item_permissions_check' ), + 'callback' => array( $this, 'encode_form_data' ), + ), + ) + ); + + // Backwards compatibility. TODO: Remove. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P[a-zA-Z0-9_-]+)/form-renderer', @@ -200,19 +233,30 @@ protected function get_widgets() { global $wp_registered_widgets; $widgets = array(); - foreach ( $wp_registered_widgets as $widget ) { - $widget_callback = $widget['callback']; + + foreach ( $wp_registered_widgets as $widget ) { + $parsed_id = gutenberg_parse_widget_id( $widget['id'] ); + $widget['id'] = $parsed_id['id_base']; + unset( $widget['callback'] ); - if ( is_array( $widget_callback ) && $widget_callback[0] instanceof WP_Widget ) { - $widget_class = $widget_callback[0]; - $widget_array = (array) $widget_class; - $widget = array_merge( $widget, $widget_array ); - $widget['id'] = $widget['id_base']; - $widget['widget_class'] = get_class( $widget_class ); - } else { - unset( $widget['classname'] ); + $classname = ''; + foreach ( (array) $widget['classname'] as $cn ) { + if ( is_string( $cn ) ) { + $classname .= '_' . $cn; + } elseif ( is_object( $cn ) ) { + $classname .= '_' . get_class( $cn ); + } } + $widget['classname'] = ltrim( $classname, '_' ); + + // Backwards compatibility. TODO: Remove. + $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] ); + if ( $widget_object ) { + $widget['option_name'] = $widget_object->option_name; + $widget['widget_class'] = get_class( $widget_object ); + } + $widgets[] = $widget; } @@ -362,21 +406,21 @@ public function get_item_schema() { 'readonly' => true, ), 'option_name' => array( - 'description' => __( 'Option name.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Option name.', 'gutenberg' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'widget_class' => array( - 'description' => __( 'Widget class name.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Widget class name.', 'gutenberg' ), 'type' => 'string', 'default' => '', 'context' => array( 'embed', 'view', 'edit' ), 'readonly' => true, ), 'customize_selective_refresh' => array( - 'description' => __( 'Customize selective refresh.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Customize selective refresh.', 'gutenberg' ), 'type' => 'boolean', 'default' => false, 'context' => array( 'embed', 'view', 'edit' ), @@ -399,6 +443,8 @@ public function get_item_schema() { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function get_widget_form( $request ) { + _deprecated_function( __METHOD__, '10.2.0' ); + $instance = $request->get_param( 'instance' ); $widget_name = $request['id']; @@ -427,6 +473,118 @@ public function get_widget_form( $request ) { ); } + /** + * An RPC-style endpoint which can be used by clients to turn user input in + * a widget admin form into an encoded instance object. + * + * Accepts: + * + * - id: A widget type ID. + * - instance: A widget's encoded instance object. Optional. + * - form_data: Form data from submitting a widget's admin form. Optional. + * + * Returns: + * - instance: The encoded instance object after updating the widget with + * the given form data. + * - form: The widget's admin form after updating the widget with the + * given form data. + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function encode_form_data( $request ) { + $id = $request['id']; + $widget_object = gutenberg_get_widget_object( $id ); + + if ( ! $widget_object ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'Cannot preview a widget that does not extend WP_Widget.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + + // Set the widget's number to 1 so that the `id` attributes in the HTML + // that we return are predictable. + $widget_object->_set( 1 ); + + if ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) { + $serialized_instance = base64_decode( $request['instance']['encoded'] ); + if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'The provided instance is malformed.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + $instance = unserialize( $serialized_instance ); + } else { + $instance = array(); + } + + if ( + isset( $request['form_data'][ "widget-$id" ] ) && + is_array( $request['form_data'][ "widget-$id" ] ) + ) { + $new_instance = array_values( $request['form_data'][ "widget-$id" ] )[0]; + $old_instance = $instance; + + $instance = $widget_object->update( $new_instance, $old_instance ); + + /** This filter is documented in wp-includes/class-wp-widget.php */ + $instance = apply_filters( + 'widget_update_callback', + $instance, + $new_instance, + $old_instance, + $widget_object + ); + } + + ob_start(); + + /** This filter is documented in wp-includes/class-wp-widget.php */ + $instance = apply_filters( + 'widget_form_callback', + $instance, + $widget_object + ); + + if ( false !== $instance ) { + $return = $widget_object->form( $instance ); + + /** This filter is documented in wp-includes/class-wp-widget.php */ + do_action_ref_array( + 'in_widget_form', + array( &$widget_object, &$return, $instance ) + ); + } + + $form = ob_get_clean(); + + $serialized_instance = serialize( $instance ); + + $response = array( + 'form' => trim( $form ), + 'instance' => array( + 'encoded' => base64_encode( $serialized_instance ), + 'hash' => wp_hash( $serialized_instance ), + ), + ); + + if ( ! empty( $widget_object->show_instance_in_rest ) ) { + if ( empty( $instance ) ) { + // Use new stdClass() instead of array() so that endpoint + // returns {} and not []. + $response['instance']['raw'] = new stdClass; + } else { + $response['instance']['raw'] = $instance; + } + } + + return rest_ensure_response( $response ); + } + /** * Retrieves the query params for collections. * diff --git a/lib/class-wp-rest-widgets-controller.php b/lib/class-wp-rest-widgets-controller.php index 3b9fb9955921ea..817efd789e6a95 100644 --- a/lib/class-wp-rest-widgets-controller.php +++ b/lib/class-wp-rest-widgets-controller.php @@ -156,7 +156,7 @@ public function get_item( $request ) { return $sidebar_id; } - return $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request ); } /** @@ -182,11 +182,13 @@ public function create_item_permissions_check( $request ) { // phpcs:ignore Vari public function create_item( $request ) { $sidebar_id = $request['sidebar']; - $backup_post = $_POST; - $widget_id = $this->save_widget( $request ); - $_POST = $backup_post; + $widget_id = $this->save_widget( $request ); - $this->assign_to_sidebar( $widget_id, $sidebar_id ); + if ( is_wp_error( $widget_id ) ) { + return $widget_id; + } + + gutenberg_assign_widget_to_sidebar( $widget_id, $sidebar_id ); $request['context'] = 'edit'; @@ -225,30 +227,34 @@ public function update_item( $request ) { $widget_id = $request['id']; $sidebar_id = $this->find_widgets_sidebar( $widget_id ); - if ( is_wp_error( $sidebar_id ) ) { - // Allow for an update request to a reference widget if the widget hasn't been assigned to a sidebar yet. - if ( $request['sidebar'] && $this->is_reference_widget( $widget_id ) ) { - $sidebar_id = $request['sidebar']; - $this->assign_to_sidebar( $widget_id, $sidebar_id ); - } else { - return $sidebar_id; - } + // Allow sidebar to be unset or missing when widget is not a WP_Widget. + $parsed_id = gutenberg_parse_widget_id( $widget_id ); + $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] ); + if ( is_wp_error( $sidebar_id ) && $widget_object ) { + return $sidebar_id; } - if ( isset( $request['settings'] ) ) { - $backup_post = $_POST; - $this->save_widget( $request ); - $_POST = $backup_post; + if ( + $request->has_param( 'settings' ) || // Backwards compatibility. TODO: Remove. + $request->has_param( 'instance' ) || + $request->has_param( 'form_data' ) + ) { + $maybe_error = $this->save_widget( $request ); + if ( is_wp_error( $maybe_error ) ) { + return $maybe_error; + } } - if ( isset( $request['sidebar'] ) && $request['sidebar'] !== $sidebar_id ) { - $sidebar_id = $request['sidebar']; - $this->assign_to_sidebar( $widget_id, $sidebar_id ); + if ( $request->has_param( 'sidebar' ) ) { + if ( $sidebar_id !== $request['sidebar'] ) { + $sidebar_id = $request['sidebar']; + gutenberg_assign_widget_to_sidebar( $widget_id, $sidebar_id ); + } } $request['context'] = 'edit'; - return $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); + return $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request ); } /** @@ -282,8 +288,8 @@ public function delete_item( $request ) { $request['context'] = 'edit'; if ( $request['force'] ) { - $prepared = $this->prepare_item_for_response( compact( 'sidebar_id', 'widget_id' ), $request ); - $this->assign_to_sidebar( $widget_id, '' ); + $prepared = $this->prepare_item_for_response( compact( 'widget_id', 'sidebar_id' ), $request ); + gutenberg_assign_widget_to_sidebar( $widget_id, '' ); $prepared->set_data( array( 'deleted' => true, @@ -291,7 +297,7 @@ public function delete_item( $request ) { ) ); } else { - $this->assign_to_sidebar( $widget_id, 'wp_inactive_widgets' ); + gutenberg_assign_widget_to_sidebar( $widget_id, 'wp_inactive_widgets' ); $prepared = $this->prepare_item_for_response( array( 'sidebar_id' => 'wp_inactive_widgets', @@ -304,34 +310,6 @@ public function delete_item( $request ) { return $prepared; } - /** - * Assigns a widget to the given sidebar. - * - * @since 5.6.0 - * - * @param string $widget_id The widget id to assign. - * @param string $sidebar_id The sidebar id to assign to. If empty, the widget won't be added to any sidebar. - */ - protected function assign_to_sidebar( $widget_id, $sidebar_id ) { - $sidebars = wp_get_sidebars_widgets(); - - foreach ( $sidebars as $maybe_sidebar_id => $widgets ) { - foreach ( $widgets as $i => $maybe_widget_id ) { - if ( $widget_id === $maybe_widget_id && $sidebar_id !== $maybe_sidebar_id ) { - unset( $sidebars[ $maybe_sidebar_id ][ $i ] ); - // We could technically break 2 here, but continue looping in case the id is duplicated. - continue 2; - } - } - } - - if ( $sidebar_id ) { - $sidebars[ $sidebar_id ][] = $widget_id; - } - - wp_set_sidebars_widgets( $sidebars ); - } - /** * Performs a permissions check for managing widgets. * @@ -383,99 +361,130 @@ protected function find_widgets_sidebar( $widget_id ) { * @return string The saved widget ID. */ protected function save_widget( $request ) { - global $wp_registered_widget_updates, $wp_registered_widgets; - - $input_widget = $request->get_params(); - - if ( isset( $input_widget['id'] ) && ! $this->is_reference_widget( $input_widget['id'] ) ) { - $widget = $wp_registered_widgets[ $input_widget['id'] ]; + global $wp_registered_widget_updates; - $input_widget['number'] = (int) $widget['params'][0]['number']; - $input_widget['id_base'] = _get_widget_id_base( $input_widget['id'] ); + require_once ABSPATH . 'wp-admin/includes/widgets.php'; // For next_widget_id_number(). + + if ( isset( $request['id'] ) ) { + // Saving an existing widget. + $id = $request['id']; + $parsed_id = gutenberg_parse_widget_id( $id ); + $id_base = $parsed_id['id_base']; + $number = isset( $parsed_id['number'] ) ? $parsed_id['number'] : null; + $widget_object = gutenberg_get_widget_object( $id_base ); + } elseif ( $request['id_base'] ) { + // Saving a new widget. + $id_base = $request['id_base']; + $widget_object = gutenberg_get_widget_object( $id_base ); + $number = $widget_object ? next_widget_id_number( $id_base ) : null; + $id = $widget_object ? $id_base . '-' . $number : $id_base; + } else { + return new WP_Error( + 'rest_invalid_widget', + __( 'Widget type (id_base) is required.', 'gutenberg' ), + array( 'status' => 400 ) + ); } - ob_start(); - if ( isset( $input_widget['id_base'] ) && isset( $wp_registered_widget_updates[ $input_widget['id_base'] ] ) ) { - // Class-based widget. - $update_control = $wp_registered_widget_updates[ $input_widget['id_base'] ]; - if ( ! isset( $input_widget['id'] ) ) { - $number = $this->get_last_number_for_widget( $input_widget['id_base'] ) + 1; - $id = $input_widget['id_base'] . '-' . $number; + if ( ! isset( $wp_registered_widget_updates[ $id_base ] ) ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'The provided widget type (id_base) cannot be updated.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } - $input_widget['id'] = $id; - $input_widget['number'] = $number; + if ( ! empty( $request['settings'] ) ) { // Backwards compatibility. TODO: Remove. + _deprecated_argument( 'settings', '10.2.0' ); + if ( $widget_object ) { + $form_data = array( + "widget-$id_base" => array( + $number => $request['settings'], + ), + ); + } else { + $form_data = $request['settings']; } - $field = 'widget-' . $input_widget['id_base']; - $number = $input_widget['number']; - $_POST = $input_widget; - $_POST[ $field ][ $number ] = wp_slash( $input_widget['settings'] ); - call_user_func( $update_control['callback'] ); - $update_control['callback'][0]->updated = false; - - // Just because we saved new widget doesn't mean it was added to $wp_registered_widgets. - // Let's make sure it's there so that it's included in the response. - if ( ! isset( $wp_registered_widgets[ $input_widget['id'] ] ) || 1 === $number ) { - $widget_class = get_class( $update_control['callback'][0] ); - $new_object = new $widget_class( - $input_widget['id_base'], - $input_widget['name'], - $input_widget['settings'] + } elseif ( isset( $request['instance'] ) ) { + if ( ! $widget_object ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'Cannot set instance on a widget that does not extend WP_Widget.', 'gutenberg' ), + array( 'status' => 400 ) ); - $new_object->_set( $number ); - $new_object->_register(); } - } else { - $registered_widget_id = null; - if ( isset( $wp_registered_widget_updates[ $input_widget['id'] ] ) ) { - $registered_widget_id = $input_widget['id']; - } else { - $numberless_id = substr( $input_widget['id'], 0, strrpos( $input_widget['id'], '-' ) ); - if ( isset( $wp_registered_widget_updates[ $numberless_id ] ) ) { - $registered_widget_id = $numberless_id; + + if ( isset( $request['instance']['raw'] ) ) { + if ( empty( $widget_object->show_instance_in_rest ) ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'Widget type does not support raw instances.', 'gutenberg' ), + array( 'status' => 400 ) + ); } + $instance = $request['instance']['raw']; + } elseif ( isset( $request['instance']['encoded'], $request['instance']['hash'] ) ) { + $serialized_instance = base64_decode( $request['instance']['encoded'] ); + if ( ! hash_equals( wp_hash( $serialized_instance ), $request['instance']['hash'] ) ) { + return new WP_Error( + 'rest_invalid_widget', + __( 'The provided instance is malformed.', 'gutenberg' ), + array( 'status' => 400 ) + ); + } + $instance = unserialize( $serialized_instance ); + } else { + return new WP_Error( + 'rest_invalid_widget', + __( 'The provided instance is invalid. Must contain raw OR encoded and hash.', 'gutenberg' ), + array( 'status' => 400 ) + ); } - if ( $registered_widget_id ) { - // Old-style widget. - $update_control = $wp_registered_widget_updates[ $registered_widget_id ]; - $_POST = wp_slash( $input_widget['settings'] ); - call_user_func( $update_control['callback'] ); - } + $form_data = array( + "widget-$id_base" => array( + $number => $instance, + ), + ); + } elseif ( isset( $request['form_data'] ) ) { + $form_data = $request['form_data']; + } else { + $form_data = array(); } - ob_end_clean(); - return $input_widget['id']; - } - - /** - * Gets the last number used by the given widget. - * - * @since 5.6.0 - * - * @global array $wp_registered_widget_updates List of widget update callbacks. - * - * @param string $id_base The widget id base. - * @return int The last number, or zero if the widget has not been used. - */ - protected function get_last_number_for_widget( $id_base ) { - global $wp_registered_widget_updates; + $original_post = $_POST; + $original_request = $_REQUEST; - if ( ! is_array( $wp_registered_widget_updates[ $id_base ]['callback'] ) ) { - return 0; + foreach ( $form_data as $key => $value ) { + $slashed_value = wp_slash( $value ); + $_POST[ $key ] = $slashed_value; + $_REQUEST[ $key ] = $slashed_value; } - if ( ! $wp_registered_widget_updates[ $id_base ]['callback'][0] instanceof WP_Widget ) { - return 0; + $callback = $wp_registered_widget_updates[ $id_base ]['callback']; + $params = $wp_registered_widget_updates[ $id_base ]['params']; + + if ( is_callable( $callback ) ) { + ob_start(); + call_user_func_array( $callback, $params ); + ob_end_clean(); } - $widget = $wp_registered_widget_updates[ $id_base ]['callback'][0]; - $instances = array_filter( $widget->get_settings(), 'is_numeric', ARRAY_FILTER_USE_KEY ); + $_POST = $original_post; + $_REQUEST = $original_request; - if ( ! $instances ) { - return 0; + if ( $widget_object ) { + // Register any multi-widget that the update callback just created. + $widget_object->_set( $number ); + $widget_object->_register_one( $number ); + + // WP_Widget sets updated = true after an update to prevent more + // than one widget from being saved per request. This isn't what we + // want in the REST API, though, as we support batch requests. + $widget_object->updated = false; } - return $widget->number; + return $id; } /** @@ -492,100 +501,70 @@ protected function get_last_number_for_widget( $id_base ) { * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. */ public function prepare_item_for_response( $item, $request ) { - global $wp_registered_widgets, $wp_registered_sidebars, $wp_registered_widget_controls; + global $wp_registered_widgets; $widget_id = $item['widget_id']; $sidebar_id = $item['sidebar_id']; if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) { - return new WP_Error( 'rest_invalid_widget', __( 'The requested widget is invalid.', 'gutenberg' ), array( 'status' => 500 ) ); + return new WP_Error( + 'rest_invalid_widget', + __( 'The requested widget is invalid.', 'gutenberg' ), + array( 'status' => 500 ) + ); } - $fields = $this->get_fields_for_response( $request ); - - if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ) { - $registered_sidebar = $wp_registered_sidebars[ $sidebar_id ]; - } elseif ( 'wp_inactive_widgets' === $sidebar_id ) { - $registered_sidebar = array(); - } else { - $registered_sidebar = null; - } + $widget = $wp_registered_widgets[ $widget_id ]; + $parsed_id = gutenberg_parse_widget_id( $widget_id ); + $fields = $this->get_fields_for_response( $request ); - $widget = $wp_registered_widgets[ $widget_id ]; $prepared = array( 'id' => $widget_id, - 'id_base' => '', + 'id_base' => $parsed_id['id_base'], 'sidebar' => $sidebar_id, - 'widget_class' => '', - 'name' => $widget['name'], - 'description' => ! empty( $widget['description'] ) ? $widget['description'] : '', - 'number' => 0, 'rendered' => '', - 'rendered_form' => '', - 'settings' => array(), + 'rendered_form' => null, + 'instance' => null, ); - // Get the widget output. - if ( is_callable( $widget['callback'] ) && rest_is_field_included( 'rendered', $fields ) && 'wp_inactive_widgets' !== $sidebar_id ) { - // @note: everything up to ob_start is taken from the dynamic_sidebar function. - $widget_parameters = array_merge( - array( - array_merge( - (array) $registered_sidebar, - array( - 'widget_id' => $widget_id, - 'widget_name' => $widget['name'], - ) - ), - ), - (array) $widget['params'] - ); + if ( + rest_is_field_included( 'rendered', $fields ) && + 'wp_inactive_widgets' !== $sidebar_id + ) { + $prepared['rendered'] = trim( gutenberg_render_widget( $widget_id, $sidebar_id ) ); + } - $classname = ''; - foreach ( (array) $widget['classname'] as $cn ) { - if ( is_string( $cn ) ) { - $classname .= '_' . $cn; - } elseif ( is_object( $cn ) ) { - $classname .= '_' . get_class( $cn ); - } - } - $classname = ltrim( $classname, '_' ); - if ( isset( $widget_parameters[0]['before_widget'] ) ) { - $widget_parameters[0]['before_widget'] = sprintf( - $widget_parameters[0]['before_widget'], - $widget_id, - $classname - ); + if ( rest_is_field_included( 'rendered_form', $fields ) ) { + $rendered_form = gutenberg_render_widget_control( $widget_id ); + if ( ! is_null( $rendered_form ) ) { + $prepared['rendered_form'] = trim( $rendered_form ); } - - ob_start(); - call_user_func_array( $widget['callback'], $widget_parameters ); - $prepared['rendered'] = trim( ob_get_clean() ); } - if ( is_array( $widget['callback'] ) && isset( $widget['callback'][0] ) ) { - $instance = $widget['callback'][0]; - $prepared['widget_class'] = get_class( $instance ); - $prepared['settings'] = $this->get_sidebar_widget_instance( - $registered_sidebar, - $widget_id - ); - $prepared['number'] = (int) $widget['params'][0]['number']; - $prepared['id_base'] = $instance->id_base; - } + if ( rest_is_field_included( 'instance', $fields ) ) { + $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] ); + $instance = gutenberg_get_widget_instance( $widget_id ); - if ( - rest_is_field_included( 'rendered_form', $fields ) && - isset( $wp_registered_widget_controls[ $widget_id ]['callback'] ) - ) { - $control = $wp_registered_widget_controls[ $widget_id ]; - $arguments = array(); - if ( ! empty( $prepared['number'] ) ) { - $arguments[0] = array( 'number' => $prepared['number'] ); + if ( $instance ) { + $serialized_instance = serialize( $instance ); + $prepared['instance']['encoded'] = base64_encode( $serialized_instance ); + $prepared['instance']['hash'] = wp_hash( $serialized_instance ); + + if ( ! empty( $widget_object->show_instance_in_rest ) ) { + $prepared['instance']['raw'] = $instance; + } } - ob_start(); - call_user_func_array( $control['callback'], $arguments ); - $prepared['rendered_form'] = trim( ob_get_clean() ); + } + + // Backwards compatibility. TODO: Remove. + $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] ); + $prepared['widget_class'] = $widget_object ? get_class( $widget_object ) : ''; + $prepared['name'] = $widget['name']; + $prepared['description'] = ! empty( $widget['description'] ) ? $widget['description'] : ''; + $prepared['number'] = $widget_object ? (int) $parsed_id['number'] : 0; + if ( rest_is_field_included( 'settings', $fields ) ) { + $instance = gutenberg_get_widget_instance( $widget_id ); + $prepared['settings'] = $instance ? $instance : array(); } $context = ! empty( $request['context'] ) ? $request['context'] : 'view'; @@ -636,88 +615,6 @@ protected function prepare_links( $prepared ) { ); } - /** - * Retrieves a widget instance. - * - * @since 5.6.0 - * - * @param array $sidebar The sidebar data registered with {@see register_sidebar()}. - * @param string $id Identifier of the widget instance. - * @return array Array containing the widget instance. - */ - protected function get_sidebar_widget_instance( $sidebar, $id ) { - list( $object, $number, $name ) = $this->get_widget_info( $id ); - if ( ! $object ) { - return array(); - } - - $object->_set( $number ); - - $instances = $object->get_settings(); - $instance = $instances[ $number ]; - - $args = array_merge( - is_array( $sidebar ) ? $sidebar : array(), - array( - 'widget_id' => $id, - 'widget_name' => $name, - ) - ); - - /** This filter is documented in wp-includes/class-wp-widget.php */ - $instance = apply_filters( 'widget_display_callback', $instance, $object, $args ); - - if ( false === $instance ) { - return array(); - } - - return $instance; - } - - /** - * Returns an array containing information about the requested widget. - * - * @since 5.6.0 - * - * @global array $wp_registered_widgets The list of reigstered widgets. - * - * @param string $widget_id Identifier of the widget. - * @return array Array containing the the widget object, the number, and the name. - */ - protected function get_widget_info( $widget_id ) { - global $wp_registered_widgets; - - if ( - ! is_array( $wp_registered_widgets[ $widget_id ]['callback'] ) || - ! isset( $wp_registered_widgets[ $widget_id ]['callback'][0] ) || - ! isset( $wp_registered_widgets[ $widget_id ]['params'][0]['number'] ) || - ! isset( $wp_registered_widgets[ $widget_id ]['name'] ) || - ! ( $wp_registered_widgets[ $widget_id ]['callback'][0] instanceof WP_Widget ) - ) { - return array( null, null, null ); - } - - $object = $wp_registered_widgets[ $widget_id ]['callback'][0]; - $number = $wp_registered_widgets[ $widget_id ]['params'][0]['number']; - $name = $wp_registered_widgets[ $widget_id ]['name']; - - return array( $object, $number, $name ); - } - - /** - * Checks if the given widget id is a reference widget, ie one that does not use WP_Widget. - * - * @since 5.6.0 - * - * @param string $widget_id The widget id to check. - * @return bool Whether this is a reference widget or not. - */ - protected function is_reference_widget( $widget_id ) { - list ( $object ) = $this->get_widget_info( $widget_id ); - - return ! $object instanceof WP_Widget; - } - /** * Gets the list of collection params. * @@ -758,7 +655,7 @@ public function get_item_schema() { 'context' => array( 'view', 'edit', 'embed' ), ), 'id_base' => array( - 'description' => __( 'Type of widget for the object.', 'gutenberg' ), + 'description' => __( 'The type of the widget. Corresponds to ID in widget-types endpoint.', 'gutenberg' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), ), @@ -769,44 +666,64 @@ public function get_item_schema() { 'required' => true, 'context' => array( 'view', 'edit', 'embed' ), ), + 'rendered' => array( + 'description' => __( 'HTML representation of the widget.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'view', 'edit', 'embed' ), + 'readonly' => true, + ), + 'rendered_form' => array( + 'description' => __( 'HTML representation of the widget admin form.', 'gutenberg' ), + 'type' => 'string', + 'context' => array( 'edit' ), + 'readonly' => true, + ), + 'instance' => array( + 'description' => __( 'Instance settings of the widget, if supported.', 'gutenberg' ), + 'type' => 'object', + 'context' => array( 'view', 'edit', 'embed' ), + 'default' => null, + ), + 'form_data' => array( + 'description' => __( 'URL-encoded form data from the widget admin form. Used to update a widget that does not support instance. Write only.', 'gutenberg' ), + 'type' => 'string', + 'context' => array(), + 'arg_options' => array( + 'sanitize_callback' => function( $string ) { + $array = array(); + wp_parse_str( $string, $array ); + return $array; + }, + ), + ), + // BEGIN backwards compatibility. TODO: Remove. 'widget_class' => array( - 'description' => __( 'Class name of the widget implementation.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Class name of the widget implementation.', 'gutenberg' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), ), 'name' => array( - 'description' => __( 'Name of the widget.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Name of the widget.', 'gutenberg' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), ), 'description' => array( - 'description' => __( 'Description of the widget.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Description of the widget.', 'gutenberg' ), 'type' => 'string', 'context' => array( 'view', 'edit', 'embed' ), ), 'number' => array( - 'description' => __( 'Number of the widget.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Number of the widget.', 'gutenberg' ), 'type' => 'integer', 'context' => array( 'view', 'edit', 'embed' ), ), - 'rendered' => array( - 'description' => __( 'HTML representation of the widget.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'view', 'edit', 'embed' ), - 'readonly' => true, - ), - 'rendered_form' => array( - 'description' => __( 'HTML representation of the widget admin form.', 'gutenberg' ), - 'type' => 'string', - 'context' => array( 'edit' ), - 'readonly' => true, - ), 'settings' => array( - 'description' => __( 'Settings of the widget.', 'gutenberg' ), + 'description' => __( 'DEPRECATED. Settings of the widget.', 'gutenberg' ), 'type' => 'object', 'context' => array( 'view', 'edit', 'embed' ), 'default' => array(), ), + // END backwards compatibility. ), ); diff --git a/lib/class-wp-theme-json-resolver.php b/lib/class-wp-theme-json-resolver.php index 7719dbf9ddf15d..e3a2a2a0e84d60 100644 --- a/lib/class-wp-theme-json-resolver.php +++ b/lib/class-wp-theme-json-resolver.php @@ -140,9 +140,9 @@ private static function extract_paths_to_translate( $i18n_partial, $current_path /** * Returns a data structure used in theme.json translation. * - * @return array An array of theme.json paths that are translatable and the keys that are translatable + * @return array An array of theme.json fields that are translatable and the keys that are translatable */ - public static function get_presets_to_translate() { + public static function get_fields_to_translate() { static $theme_json_i18n = null; if ( null === $theme_json_i18n ) { $file_structure = self::read_json_file( __DIR__ . '/experimental-i18n-theme.json' ); @@ -151,6 +151,29 @@ public static function get_presets_to_translate() { return $theme_json_i18n; } + /** + * Translates a chunk of the loaded theme.json structure. + * + * @param array $array_to_translate The chunk of theme.json to translate. + * @param string $key The key of the field that contains the string to translate. + * @param string $context The context to apply in the translation call. + * @param string $domain Text domain. Unique identifier for retrieving translated strings. + * + * @return array Returns the modified $theme_json chunk. + */ + private static function translate_theme_json_chunk( array $array_to_translate, $key, $context, $domain ) { + foreach ( $array_to_translate as $item_key => $item_to_translate ) { + if ( empty( $item_to_translate[ $key ] ) ) { + continue; + } + + // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain + $array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain ); + } + + return $array_to_translate; + } + /** * Given a theme.json structure modifies it in place * to update certain values by its translated strings @@ -163,37 +186,31 @@ public static function get_presets_to_translate() { * @return array Returns the modified $theme_json_structure. */ private static function translate( $theme_json, $domain = 'default' ) { - if ( ! isset( $theme_json['settings'] ) ) { - return $theme_json; - } - - $presets = self::get_presets_to_translate(); - foreach ( $theme_json['settings'] as $setting_key => $settings ) { - if ( empty( $settings ) ) { - continue; - } - - foreach ( $presets as $preset ) { - $path = array_slice( $preset['path'], 2 ); - $key = $preset['key']; - $context = $preset['context']; - - $array_to_translate = _wp_array_get( $theme_json['settings'][ $setting_key ], $path, null ); - if ( null === $array_to_translate ) { + $fields = self::get_fields_to_translate(); + foreach ( $fields as $field ) { + $path = $field['path']; + $key = $field['key']; + $context = $field['context']; + if ( 'settings' === $path[0] ) { + if ( empty( $theme_json['settings'] ) ) { continue; } - - foreach ( $array_to_translate as $item_key => $item_to_translate ) { - if ( empty( $item_to_translate[ $key ] ) ) { + $path = array_slice( $path, 2 ); + foreach ( $theme_json['settings'] as $setting_key => $setting ) { + $array_to_translate = _wp_array_get( $setting, $path, null ); + if ( is_null( $array_to_translate ) ) { continue; } - - // phpcs:ignore WordPress.WP.I18n.LowLevelTranslationFunction,WordPress.WP.I18n.NonSingularStringLiteralText,WordPress.WP.I18n.NonSingularStringLiteralContext,WordPress.WP.I18n.NonSingularStringLiteralDomain - $array_to_translate[ $item_key ][ $key ] = translate_with_gettext_context( $array_to_translate[ $item_key ][ $key ], $context, $domain ); - // phpcs:enable + $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain ); + gutenberg_experimental_set( $theme_json['settings'][ $setting_key ], $path, $translated_array ); } - - gutenberg_experimental_set( $theme_json['settings'][ $setting_key ], $path, $array_to_translate ); + } else { + $array_to_translate = _wp_array_get( $theme_json, $path, null ); + if ( is_null( $array_to_translate ) ) { + continue; + } + $translated_array = self::translate_theme_json_chunk( $array_to_translate, $key, $context, $domain ); + gutenberg_experimental_set( $theme_json, $path, $translated_array ); } } @@ -374,7 +391,7 @@ public static function get_user_data() { $json_decoding_error = json_last_error(); if ( JSON_ERROR_NONE !== $json_decoding_error ) { error_log( 'Error when decoding user schema: ' . json_last_error_msg() ); - return $config; + return new WP_Theme_JSON( $config ); } // Very important to verify if the flag isGlobalStylesUserThemeJSON is true. diff --git a/lib/class-wp-theme-json.php b/lib/class-wp-theme-json.php index d967e1681acf18..35da7df75b79a4 100644 --- a/lib/class-wp-theme-json.php +++ b/lib/class-wp-theme-json.php @@ -56,25 +56,6 @@ class WP_Theme_JSON { */ const ROOT_BLOCK_SELECTOR = ':root'; - /** - * The supported properties of the root block. - * - * @var array - */ - const ROOT_BLOCK_SUPPORTS = array( - '--wp--style--color--link', - 'background', - 'backgroundColor', - 'color', - 'fontFamily', - 'fontSize', - 'fontStyle', - 'fontWeight', - 'lineHeight', - 'textDecoration', - 'textTransform', - ); - /** * Data schema of each block within a theme.json. * @@ -166,6 +147,7 @@ class WP_Theme_JSON { 'customTextTransforms' => null, ), 'custom' => null, + 'layout' => null, ), ); @@ -254,73 +236,56 @@ class WP_Theme_JSON { * Each property declares: * * - 'value': path to the value in theme.json and block attributes. - * - 'support': path to the block support in block.json. */ const PROPERTIES_METADATA = array( '--wp--style--color--link' => array( - 'value' => array( 'color', 'link' ), - 'support' => array( 'color', 'link' ), + 'value' => array( 'color', 'link' ), ), 'background' => array( - 'value' => array( 'color', 'gradient' ), - 'support' => array( 'color', 'gradients' ), + 'value' => array( 'color', 'gradient' ), ), - 'backgroundColor' => array( - 'value' => array( 'color', 'background' ), - 'support' => array( 'color' ), + 'background-color' => array( + 'value' => array( 'color', 'background' ), ), - 'borderRadius' => array( - 'value' => array( 'border', 'radius' ), - 'support' => array( '__experimentalBorder', 'radius' ), + 'border-radius' => array( + 'value' => array( 'border', 'radius' ), ), - 'borderColor' => array( - 'value' => array( 'border', 'color' ), - 'support' => array( '__experimentalBorder', 'color' ), + 'border-color' => array( + 'value' => array( 'border', 'color' ), ), - 'borderWidth' => array( - 'value' => array( 'border', 'width' ), - 'support' => array( '__experimentalBorder', 'width' ), + 'border-width' => array( + 'value' => array( 'border', 'width' ), ), - 'borderStyle' => array( - 'value' => array( 'border', 'style' ), - 'support' => array( '__experimentalBorder', 'style' ), + 'border-style' => array( + 'value' => array( 'border', 'style' ), ), 'color' => array( - 'value' => array( 'color', 'text' ), - 'support' => array( 'color' ), + 'value' => array( 'color', 'text' ), ), - 'fontFamily' => array( - 'value' => array( 'typography', 'fontFamily' ), - 'support' => array( '__experimentalFontFamily' ), + 'font-family' => array( + 'value' => array( 'typography', 'fontFamily' ), ), - 'fontSize' => array( - 'value' => array( 'typography', 'fontSize' ), - 'support' => array( 'fontSize' ), + 'font-size' => array( + 'value' => array( 'typography', 'fontSize' ), ), - 'fontStyle' => array( - 'value' => array( 'typography', 'fontStyle' ), - 'support' => array( '__experimentalFontStyle' ), + 'font-style' => array( + 'value' => array( 'typography', 'fontStyle' ), ), - 'fontWeight' => array( - 'value' => array( 'typography', 'fontWeight' ), - 'support' => array( '__experimentalFontWeight' ), + 'font-weight' => array( + 'value' => array( 'typography', 'fontWeight' ), ), - 'lineHeight' => array( - 'value' => array( 'typography', 'lineHeight' ), - 'support' => array( 'lineHeight' ), + 'line-height' => array( + 'value' => array( 'typography', 'lineHeight' ), ), 'padding' => array( 'value' => array( 'spacing', 'padding' ), - 'support' => array( 'spacing', 'padding' ), 'properties' => array( 'top', 'right', 'bottom', 'left' ), ), - 'textDecoration' => array( - 'value' => array( 'typography', 'textDecoration' ), - 'support' => array( '__experimentalTextDecoration' ), + 'text-decoration' => array( + 'value' => array( 'typography', 'textDecoration' ), ), - 'textTransform' => array( - 'value' => array( 'typography', 'textTransform' ), - 'support' => array( '__experimentalTextTransform' ), + 'text-transform' => array( + 'value' => array( 'typography', 'textTransform' ), ), ); @@ -360,14 +325,7 @@ public function __construct( $theme_json = array() ) { continue; } - // Remove the properties the block doesn't support. - // This is a subset of the full styles schema. - $styles_schema = self::SCHEMA['styles']; - foreach ( self::PROPERTIES_METADATA as $prop_name => $prop_meta ) { - if ( ! in_array( $prop_name, $metadata['supports'], true ) ) { - unset( $styles_schema[ $prop_meta['value'][0] ][ $prop_meta['value'][1] ] ); - } - } + $styles_schema = self::SCHEMA['styles']; $this->theme_json['styles'][ $block_selector ] = self::remove_keys_not_in_schema( $this->theme_json['styles'][ $block_selector ], $styles_schema @@ -409,63 +367,26 @@ public function __construct( $theme_json = array() ) { } /** - * Returns the kebab-cased name of a given property. - * - * @param string $property Property name to convert. - * @return string kebab-cased name of the property - */ - private static function to_kebab_case( $property ) { - $mappings = self::get_case_mappings(); - return $mappings['to_kebab_case'][ $property ]; - } - - /** - * Returns the property name of a kebab-cased property. - * - * @param string $property Property name to convert in kebab-case. - * @return string Name of the property - */ - private static function to_property( $property ) { - $mappings = self::get_case_mappings(); - return $mappings['to_property'][ $property ]; - } - - /** - * Returns a mapping on metadata properties to avoid having to constantly - * transforms properties between camel case and kebab. - * - * @return array Containing two mappings: + * Given a CSS property name, returns the property it belongs + * within the self::PROPERTIES_METADATA map. * - * - "to_kebab_case" mapping properties in camel case to - * properties in kebab case e.g: "paddingTop" to "padding-top". + * @param string $css_name The CSS property name. * - * - "to_property" mapping properties in kebab case to - * the main properties in camel case e.g: "padding-top" to "padding". + * @return string The property name. */ - private static function get_case_mappings() { - static $case_mappings; - if ( null === $case_mappings ) { - $case_mappings = array( - 'to_kebab_case' => array(), - 'to_property' => array(), - ); + private static function to_property( $css_name ) { + static $to_property; + if ( null === $to_property ) { foreach ( self::PROPERTIES_METADATA as $key => $metadata ) { - $kebab_case = strtolower( preg_replace( '/(? array( 'selector' => self::ROOT_BLOCK_SELECTOR, - 'supports' => self::ROOT_BLOCK_SUPPORTS, ), - // By make supports an empty array - // this won't have any styles associated - // but still allows adding settings - // and generate presets. self::ALL_BLOCKS_NAME => array( 'selector' => self::ALL_BLOCKS_SELECTOR, - 'supports' => array(), ), ); $registry = WP_Block_Type_Registry::get_instance(); $blocks = $registry->get_all_registered(); foreach ( $blocks as $block_name => $block_type ) { - /* - * Extract block support keys that are related to the style properties. - */ - $block_supports = array(); - foreach ( self::PROPERTIES_METADATA as $key => $metadata ) { - if ( _wp_array_get( $block_type->supports, $metadata['support'] ) ) { - $block_supports[] = $key; - } - } - /* * Assign the selector for the block. * @@ -544,7 +447,6 @@ private static function get_blocks_metadata() { ) { self::$blocks_metadata[ $block_name ] = array( 'selector' => $block_type->supports['__experimentalSelector'], - 'supports' => $block_supports, ); } elseif ( isset( $block_type->supports['__experimentalSelector'] ) && @@ -557,13 +459,11 @@ private static function get_blocks_metadata() { self::$blocks_metadata[ $key ] = array( 'selector' => $selector_metadata['selector'], - 'supports' => $block_supports, ); } } else { self::$blocks_metadata[ $block_name ] = array( 'selector' => '.wp-block-' . str_replace( '/', '-', str_replace( 'core/', '', $block_name ) ), - 'supports' => $block_supports, ); } } @@ -718,28 +618,23 @@ private static function has_properties( $metadata ) { * * @param array $declarations Holds the existing declarations. * @param array $styles Styles to process. - * @param array $supports Supports information for this block. * * @return array Returns the modified $declarations. */ - private static function compute_style_properties( $declarations, $styles, $supports ) { + private static function compute_style_properties( $declarations, $styles ) { if ( empty( $styles ) ) { return $declarations; } $properties = array(); foreach ( self::PROPERTIES_METADATA as $name => $metadata ) { - if ( ! in_array( $name, $supports, true ) ) { - continue; - } - // Some properties can be shorthand properties, meaning that // they contain multiple values instead of a single one. // An example of this is the padding property, see self::SCHEMA. if ( self::has_properties( $metadata ) ) { foreach ( $metadata['properties'] as $property ) { $properties[] = array( - 'name' => $name . ucfirst( $property ), + 'name' => $name . '-' . $property, 'value' => array_merge( $metadata['value'], array( $property ) ), ); } @@ -754,9 +649,8 @@ private static function compute_style_properties( $declarations, $styles, $suppo foreach ( $properties as $prop ) { $value = self::get_property_value( $styles, $prop['value'] ); if ( ! empty( $value ) ) { - $kebab_cased_name = self::to_kebab_case( $prop['name'] ); $declarations[] = array( - 'name' => $kebab_cased_name, + 'name' => $prop['name'], 'value' => $value, ); } @@ -992,14 +886,12 @@ private function get_block_styles() { } $selector = $metadata['selector']; - $supports = $metadata['supports']; $declarations = array(); if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { $declarations = self::compute_style_properties( $declarations, - $this->theme_json['styles'][ $block_selector ], - $supports + $this->theme_json['styles'][ $block_selector ] ); } @@ -1051,11 +943,20 @@ public function get_settings() { * @return array */ public function get_custom_templates() { + $custom_templates = array(); if ( ! isset( $this->theme_json['customTemplates'] ) ) { - return array(); - } else { - return $this->theme_json['customTemplates']; + return $custom_templates; + } + + foreach ( $this->theme_json['customTemplates'] as $item ) { + if ( isset( $item['name'] ) ) { + $custom_templates[ $item['name'] ] = array( + 'title' => isset( $item['title'] ) ? $item['title'] : '', + 'postTypes' => isset( $item['postTypes'] ) ? $item['postTypes'] : array( 'page' ), + ); + } } + return $custom_templates; } /** @@ -1064,10 +965,19 @@ public function get_custom_templates() { * @return array */ public function get_template_parts() { + $template_parts = array(); if ( ! isset( $this->theme_json['templateParts'] ) ) { - return array(); + return $template_parts; + } + + foreach ( $this->theme_json['templateParts'] as $item ) { + if ( isset( $item['name'] ) ) { + $template_parts[ $item['name'] ] = array( + 'area' => isset( $item['area'] ) ? $item['area'] : '', + ); + } } - return $this->theme_json['templateParts']; + return $template_parts; } /** @@ -1141,7 +1051,7 @@ public function remove_insecure_properties() { // Style escaping. if ( isset( $this->theme_json['styles'][ $block_selector ] ) ) { - $declarations = self::compute_style_properties( array(), $this->theme_json['styles'][ $block_selector ], $metadata['supports'] ); + $declarations = self::compute_style_properties( array(), $this->theme_json['styles'][ $block_selector ] ); foreach ( $declarations as $declaration ) { $style_to_validate = $declaration['name'] . ': ' . $declaration['value']; if ( esc_html( safecss_filter_attr( $style_to_validate ) ) === $style_to_validate ) { diff --git a/lib/class-wp-widget-block.php b/lib/class-wp-widget-block.php index e16283ed90d0ea..553f5f2d7e6b4f 100644 --- a/lib/class-wp-widget-block.php +++ b/lib/class-wp-widget-block.php @@ -22,6 +22,15 @@ class WP_Widget_Block extends WP_Widget { 'content' => '', ); + /** + * Whether or not to show the widget's instance settings array in the REST + * API. + * + * @since 5.8.0 + * @var array + */ + public $show_instance_in_rest = true; + /** * Sets up a new Block widget instance. * diff --git a/lib/client-assets.php b/lib/client-assets.php index ef74ce9e025126..6bcfff4f67124b 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -233,6 +233,10 @@ function gutenberg_register_vendor_scripts( $scripts ) { * @param WP_Scripts $scripts WP_Scripts instance. */ function gutenberg_register_packages_scripts( $scripts ) { + // When in production, use the plugin's version as the default asset version; + // else (for development or test) default to use the current time. + $default_version = defined( 'GUTENBERG_VERSION' ) && ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? GUTENBERG_VERSION : time(); + foreach ( glob( gutenberg_dir_path() . 'build/*/index.js' ) as $path ) { // Prefix `wp-` to package directory to get script handle. // For example, `โ€ฆ/build/a11y/index.js` becomes `wp-a11y`. @@ -244,7 +248,7 @@ function gutenberg_register_packages_scripts( $scripts ) { ? require( $asset_file ) : null; $dependencies = isset( $asset['dependencies'] ) ? $asset['dependencies'] : array(); - $version = isset( $asset['version'] ) ? $asset['version'] : filemtime( $path ); + $version = isset( $asset['version'] ) ? $asset['version'] : $default_version; // Add dependencies that cannot be detected and generated by build tools. switch ( $handle ) { @@ -285,13 +289,17 @@ function gutenberg_register_packages_scripts( $scripts ) { * @param WP_Styles $styles WP_Styles instance. */ function gutenberg_register_packages_styles( $styles ) { + // When in production, use the plugin's version as the asset version; + // else (for development or test) default to use the current time. + $version = defined( 'GUTENBERG_VERSION' ) && ! ( defined( 'SCRIPT_DEBUG' ) && SCRIPT_DEBUG ) ? GUTENBERG_VERSION : time(); + // Editor Styles. gutenberg_override_style( $styles, 'wp-block-editor', gutenberg_url( 'build/block-editor/style.css' ), array( 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/editor/style.css' ) + $version ); $styles->add_data( 'wp-block-editor', 'rtl', 'replace' ); @@ -300,7 +308,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-editor', gutenberg_url( 'build/editor/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-nux', 'wp-reusable-blocks' ), - filemtime( gutenberg_dir_path() . 'build/editor/style.css' ) + $version ); $styles->add_data( 'wp-editor', 'rtl', 'replace' ); @@ -309,7 +317,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-edit-post', gutenberg_url( 'build/edit-post/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-editor', 'wp-edit-blocks', 'wp-block-library', 'wp-nux' ), - filemtime( gutenberg_dir_path() . 'build/edit-post/style.css' ) + $version ); $styles->add_data( 'wp-edit-post', 'rtl', 'replace' ); @@ -318,7 +326,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-components', gutenberg_url( 'build/components/style.css' ), array( 'dashicons' ), - filemtime( gutenberg_dir_path() . 'build/components/style.css' ) + $version ); $styles->add_data( 'wp-components', 'rtl', 'replace' ); @@ -328,7 +336,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-block-library', gutenberg_url( 'build/block-library/' . $block_library_filename . '.css' ), array(), - filemtime( gutenberg_dir_path() . 'build/block-library/' . $block_library_filename . '.css' ) + $version ); $styles->add_data( 'wp-block-library', 'rtl', 'replace' ); $styles->add_data( 'wp-block-library', 'path', gutenberg_dir_path() . 'build/block-library/' . $block_library_filename . '.css' ); @@ -338,13 +346,16 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-format-library', gutenberg_url( 'build/format-library/style.css' ), array( 'wp-block-editor', 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/format-library/style.css' ) + $version ); $styles->add_data( 'wp-format-library', 'rtl', 'replace' ); $wp_edit_blocks_dependencies = array( 'wp-components', 'wp-editor', + // This need to be added before the block library styles, + // The block library styles override the "reset" styles. + 'wp-reset-editor-styles', 'wp-block-library', 'wp-reusable-blocks', ); @@ -355,12 +366,21 @@ function gutenberg_register_packages_styles( $styles ) { $wp_edit_blocks_dependencies[] = 'wp-block-library-theme'; } + gutenberg_override_style( + $styles, + 'wp-reset-editor-styles', + gutenberg_url( 'build/block-library/reset.css' ), + array(), + $version + ); + $styles->add_data( 'wp-reset-editor-styles', 'rtl', 'replace' ); + gutenberg_override_style( $styles, 'wp-edit-blocks', gutenberg_url( 'build/block-library/editor.css' ), $wp_edit_blocks_dependencies, - filemtime( gutenberg_dir_path() . 'build/block-library/editor.css' ) + $version ); $styles->add_data( 'wp-edit-blocks', 'rtl', 'replace' ); @@ -369,7 +389,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-nux', gutenberg_url( 'build/nux/style.css' ), array( 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/nux/style.css' ) + $version ); $styles->add_data( 'wp-nux', 'rtl', 'replace' ); @@ -378,7 +398,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-block-library-theme', gutenberg_url( 'build/block-library/theme.css' ), array(), - filemtime( gutenberg_dir_path() . 'build/block-library/theme.css' ) + $version ); $styles->add_data( 'wp-block-library-theme', 'rtl', 'replace' ); @@ -387,7 +407,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-list-reusable-blocks', gutenberg_url( 'build/list-reusable-blocks/style.css' ), array( 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/list-reusable-blocks/style.css' ) + $version ); $styles->add_data( 'wp-list-reusable-block', 'rtl', 'replace' ); @@ -396,7 +416,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-edit-navigation', gutenberg_url( 'build/edit-navigation/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks' ), - filemtime( gutenberg_dir_path() . 'build/edit-navigation/style.css' ) + $version ); $styles->add_data( 'wp-edit-navigation', 'rtl', 'replace' ); @@ -405,7 +425,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-edit-site', gutenberg_url( 'build/edit-site/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks' ), - filemtime( gutenberg_dir_path() . 'build/edit-site/style.css' ) + $version ); $styles->add_data( 'wp-edit-site', 'rtl', 'replace' ); @@ -414,7 +434,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-edit-widgets', gutenberg_url( 'build/edit-widgets/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks', 'wp-reusable-blocks' ), - filemtime( gutenberg_dir_path() . 'build/edit-widgets/style.css' ) + $version ); $styles->add_data( 'wp-edit-widgets', 'rtl', 'replace' ); @@ -423,7 +443,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-block-directory', gutenberg_url( 'build/block-directory/style.css' ), array( 'wp-block-editor', 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/block-directory/style.css' ) + $version ); $styles->add_data( 'wp-block-directory', 'rtl', 'replace' ); @@ -432,7 +452,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-customize-widgets', gutenberg_url( 'build/customize-widgets/style.css' ), array( 'wp-components', 'wp-block-editor', 'wp-edit-blocks' ), - filemtime( gutenberg_dir_path() . 'build/customize-widgets/style.css' ) + $version ); $styles->add_data( 'wp-customize-widgets', 'rtl', 'replace' ); @@ -441,7 +461,7 @@ function gutenberg_register_packages_styles( $styles ) { 'wp-reusable-blocks', gutenberg_url( 'build/reusable-blocks/style.css' ), array( 'wp-components' ), - filemtime( gutenberg_dir_path() . 'build/reusable-blocks/style.css' ) + $version ); $styles->add_data( 'wp-reusable-block', 'rtl', 'replace' ); } @@ -584,26 +604,13 @@ function gutenberg_register_vendor_script( $scripts, $handle, $src, $deps = arra } /** - * Extends block editor settings to include Gutenberg's `editor-styles.css` as - * taking precedent those styles shipped with core. + * Extends block editor settings to remove the Gutenberg's `editor-styles.css`; * * @param array $settings Default editor settings. * * @return array Filtered editor settings. */ function gutenberg_extend_block_editor_styles( $settings ) { - $editor_styles_file = is_rtl() ? - gutenberg_dir_path() . 'build/editor/editor-styles-rtl.css' : - gutenberg_dir_path() . 'build/editor/editor-styles.css'; - - /* - * If, for whatever reason, the built editor styles do not exist, avoid - * override and fall back to the default. - */ - if ( ! file_exists( $editor_styles_file ) ) { - return $settings; - } - if ( empty( $settings['styles'] ) ) { $settings['styles'] = array(); } else { @@ -633,23 +640,17 @@ function gutenberg_extend_block_editor_styles( $settings ) { } } - $editor_styles = array( - 'css' => file_get_contents( $editor_styles_file ), - ); - // Substitute default styles if found. Otherwise, prepend to setting array. if ( isset( $i ) && $i >= 0 ) { - $settings['styles'][ $i ] = $editor_styles; - } else { - array_unshift( $settings['styles'], $editor_styles ); + unset( $settings['styles'][ $i ] ); } - // Remove the default font editor styles. - // When Gutenberg is updated to have minimum version of WordPress 5.8 - // This could be removed. - foreach ( $settings['styles'] as $j => $style ) { - if ( 0 === strpos( $style['css'], 'body { font-family:' ) ) { - unset( $settings['styles'][ $j ] ); + // Remove the default font editor styles for FSE themes. + if ( gutenberg_is_fse_theme() ) { + foreach ( $settings['styles'] as $j => $style ) { + if ( 0 === strpos( $style['css'], 'body { font-family:' ) ) { + unset( $settings['styles'][ $j ] ); + } } } @@ -657,28 +658,6 @@ function gutenberg_extend_block_editor_styles( $settings ) { } add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_styles' ); -/** - * Load the default editor styles. - * These styles are used if the "no theme styles" options is triggered. - * - * @param array $settings Default editor settings. - * - * @return array Filtered editor settings. - */ -function gutenberg_extend_block_editor_settings_with_default_editor_styles( $settings ) { - $editor_styles_file = is_rtl() ? - gutenberg_dir_path() . 'build/editor/editor-styles-rtl.css' : - gutenberg_dir_path() . 'build/editor/editor-styles.css'; - $settings['defaultEditorStyles'] = array( - array( - 'css' => file_get_contents( $editor_styles_file ), - ), - ); - - return $settings; -} -add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_default_editor_styles' ); - /** * Adds a flag to the editor settings to know whether we're in FSE theme or not. * @@ -688,6 +667,10 @@ function gutenberg_extend_block_editor_settings_with_default_editor_styles( $set */ function gutenberg_extend_block_editor_settings_with_fse_theme_flag( $settings ) { $settings['isFSETheme'] = gutenberg_is_fse_theme(); + + // Enable the new layout options for themes with a theme.json file. + $settings['supportsLayout'] = WP_Theme_JSON_Resolver::theme_has_support(); + return $settings; } add_filter( 'block_editor_settings', 'gutenberg_extend_block_editor_settings_with_fse_theme_flag' ); diff --git a/lib/editor-settings.php b/lib/editor-settings.php index e50141f1f3aca7..9a3cec289bc9a5 100644 --- a/lib/editor-settings.php +++ b/lib/editor-settings.php @@ -77,7 +77,79 @@ function gutenberg_get_common_block_editor_settings() { * @return array Filtered settings. */ function gutenberg_extend_post_editor_settings( $settings ) { + $image_default_size = get_option( 'image_default_size', 'large' ); + $image_sizes = wp_list_pluck( $settings['imageSizes'], 'slug' ); + + $settings['imageDefaultSize'] = in_array( $image_default_size, $image_sizes, true ) ? $image_default_size : 'large'; $settings['__unstableEnableFullSiteEditingBlocks'] = gutenberg_is_fse_theme(); + return $settings; } add_filter( 'block_editor_settings', 'gutenberg_extend_post_editor_settings' ); + +/** + * Initialize a block-based editor. + * + * @param string $editor_name Editor name. + * @param string $editor_script_handle Editor script handle. + * @param array $settings { + * Elements to initialize a block-based editor. + * + * @type array $preload_paths Array of paths to preload. + * @type string $initializer_name Editor initialization function name. + * @type array $editor_settings Editor settings. + * } + * @return void + */ +function gutenberg_initialize_editor( $editor_name, $editor_script_handle, $settings ) { + + $defaults = array( + 'preload_paths' => array(), + 'initializer_name' => 'initialize', + 'editor_settings' => array(), + ); + + $settings = wp_parse_args( $settings, $defaults ); + + /** + * Preload common data by specifying an array of REST API paths that will be preloaded. + * + * Filters the array of paths that will be preloaded. + * + * @param string[] $preload_paths Array of paths to preload. + */ + $preload_paths = apply_filters( "{$editor_name}_preload_paths", $settings['preload_paths'] ); + + $preload_data = array_reduce( + $preload_paths, + 'rest_preload_api_request', + array() + ); + wp_add_inline_script( + 'wp-api-fetch', + sprintf( + 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', + wp_json_encode( $preload_data ) + ), + 'after' + ); + wp_add_inline_script( + "wp-{$editor_script_handle}", + sprintf( + 'wp.domReady( function() { + wp.%s.%s( "%s", %s ); + } );', + lcfirst( str_replace( '-', '', ucwords( $editor_script_handle, '-' ) ) ), + $settings['initializer_name'], + str_replace( '_', '-', $editor_name ), + wp_json_encode( $settings['editor_settings'] ) + ) + ); + + // Preload server-registered block schemas. + wp_add_inline_script( + 'wp-blocks', + 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');' + ); + +} diff --git a/lib/experimental-i18n-theme.json b/lib/experimental-i18n-theme.json index 80c7011208d53a..94889f733a3310 100644 --- a/lib/experimental-i18n-theme.json +++ b/lib/experimental-i18n-theme.json @@ -2,17 +2,54 @@ "settings": { "*": { "typography": { - "fontSizes": [ { "name": "Font size name" } ], - "fontStyles": [ { "name": "Font style name" } ], - "fontWeights": [ { "name": "Font weight name" } ], - "fontFamilies": [ { "name": "Font family name" } ], - "textTransforms": [ { "name": "Text transform name" } ], - "textDecorations": [ { "name": "Text decoration name" } ] + "fontSizes": [ + { + "name": "Font size name" + } + ], + "fontStyles": [ + { + "name": "Font style name" + } + ], + "fontWeights": [ + { + "name": "Font weight name" + } + ], + "fontFamilies": [ + { + "name": "Font family name" + } + ], + "textTransforms": [ + { + "name": "Text transform name" + } + ], + "textDecorations": [ + { + "name": "Text decoration name" + } + ] }, "color": { - "palette": [ { "name": "Color name" } ], - "gradients": [ { "name": "Gradient name" } ] + "palette": [ + { + "name": "Color name" + } + ], + "gradients": [ + { + "name": "Gradient name" + } + ] } } - } + }, + "customTemplates": [ + { + "title": "Custom template name" + } + ] } diff --git a/lib/full-site-editing/block-templates.php b/lib/full-site-editing/block-templates.php index b1ee167d999619..265d0bda0f4a7f 100644 --- a/lib/full-site-editing/block-templates.php +++ b/lib/full-site-editing/block-templates.php @@ -343,7 +343,7 @@ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { } list( $theme, $slug ) = $parts; $wp_query_args = array( - 'name' => $slug, + 'post_name__in' => array( $slug ), 'post_type' => $template_type, 'post_status' => array( 'auto-draft', 'draft', 'publish', 'trash' ), 'posts_per_page' => 1, @@ -380,31 +380,47 @@ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { /** * Generates a unique slug for templates or template parts. * - * @param string $slug The resolved slug (post_name). + * @param string $override_slug The filtered value of the slug (starts as `null` from apply_filter). + * @param string $slug The original/un-filtered slug (post_name). * @param int $post_ID Post ID. * @param string $post_status No uniqueness checks are made if the post is still draft or pending. * @param string $post_type Post type. * @return string The original, desired slug. */ -function gutenberg_filter_wp_template_unique_post_slug( $slug, $post_ID, $post_status, $post_type ) { - if ( 'wp_template' !== $post_type || 'wp_template_part' !== $post_type ) { - return $slug; +function gutenberg_filter_wp_template_unique_post_slug( $override_slug, $slug, $post_ID, $post_status, $post_type ) { + if ( 'wp_template' !== $post_type && 'wp_template_part' !== $post_type ) { + return $override_slug; + } + + if ( ! $override_slug ) { + $override_slug = $slug; } // Template slugs must be unique within the same theme. - $theme = get_the_terms( $post_ID, 'wp_theme' )[0]->slug; + // TODO - Figure out how to update this to work for a multi-theme + // environment. Unfortunately using `get_the_terms` for the 'wp-theme' + // term does not work in the case of new entities since is too early in + // the process to have been saved to the entity. So for now we use the + // currently activated theme for creation. + $theme = wp_get_theme()->get_stylesheet(); + $terms = get_the_terms( $post_ID, 'wp_theme' ); + if ( $terms && ! is_wp_error( $terms ) ) { + $theme = $terms[0]->name; + } $check_query_args = array( - 'post_name' => $slug, + 'post_name__in' => array( $override_slug ), 'post_type' => $post_type, 'posts_per_page' => 1, - 'post__not_in' => $post_ID, + 'no_found_rows' => true, + 'post__not_in' => array( $post_ID ), 'tax_query' => array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $theme, + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => $theme, + ), ), - 'no_found_rows' => true, ); $check_query = new WP_Query( $check_query_args ); $posts = $check_query->get_posts(); @@ -412,15 +428,15 @@ function gutenberg_filter_wp_template_unique_post_slug( $slug, $post_ID, $post_s if ( count( $posts ) > 0 ) { $suffix = 2; do { - $query_args = $check_query_args; - $alt_post_name = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; - $query_args['post_name'] = $alt_post_name; - $query = new WP_Query( $check_query_args ); + $query_args = $check_query_args; + $alt_post_name = _truncate_post_slug( $override_slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"; + $query_args['post_name__in'] = array( $alt_post_name ); + $query = new WP_Query( $query_args ); $suffix++; } while ( count( $query->get_posts() ) > 0 ); - $slug = $alt_post_name; + $override_slug = $alt_post_name; } - return $slug; + return $override_slug; } -add_filter( 'wp_unique_post_slug', 'gutenberg_filter_wp_template_unique_post_slug', 10, 4 ); +add_filter( 'pre_wp_unique_post_slug', 'gutenberg_filter_wp_template_unique_post_slug', 10, 5 ); diff --git a/lib/full-site-editing/edit-site-page.php b/lib/full-site-editing/edit-site-page.php index 22bb780d2ec2be..03ca0bc892a99b 100644 --- a/lib/full-site-editing/edit-site-page.php +++ b/lib/full-site-editing/edit-site-page.php @@ -41,13 +41,7 @@ function gutenberg_get_editor_styles() { global $editor_styles; // Ideally the code is extracted into a reusable function. - $styles = array( - array( - 'css' => file_get_contents( - ABSPATH . WPINC . '/css/dist/editor/editor-styles.css' - ), - ), - ); + $styles = array(); if ( $editor_styles && current_theme_supports( 'editor-styles' ) ) { foreach ( $editor_styles as $style ) { @@ -109,50 +103,41 @@ function gutenberg_edit_site_init( $hook ) { ); $settings = gutenberg_experimental_global_styles_settings( $settings ); - // Preload block editor paths. - // most of these are copied from edit-forms-blocks.php. - $preload_paths = array( - '/?context=edit', - '/wp/v2/types?context=edit', - '/wp/v2/taxonomies?context=edit', - '/wp/v2/pages?context=edit', - '/wp/v2/themes?status=active', - array( '/wp/v2/media', 'OPTIONS' ), - ); - $preload_data = array_reduce( - $preload_paths, - 'rest_preload_api_request', - array() - ); - wp_add_inline_script( - 'wp-api-fetch', - sprintf( 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', wp_json_encode( $preload_data ) ), - 'after' - ); - - // Initialize editor. - wp_add_inline_script( - 'wp-edit-site', - sprintf( - 'wp.domReady( function() { - wp.editSite.initialize( "edit-site-editor", %s ); - } );', - wp_json_encode( $settings ) + gutenberg_initialize_editor( + 'edit_site_editor', + 'edit-site', + array( + 'preload_paths' => array( + array( '/wp/v2/media', 'OPTIONS' ), + '/?context=edit', + '/wp/v2/types?context=edit', + '/wp/v2/taxonomies?context=edit', + '/wp/v2/pages?context=edit', + '/wp/v2/themes?status=active', + ), + 'initializer_name' => 'initialize', + 'editor_settings' => $settings, ) ); - wp_add_inline_script( - 'wp-blocks', - sprintf( 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions( %s );', wp_json_encode( get_block_editor_server_block_settings() ) ), - 'after' - ); - wp_add_inline_script( 'wp-blocks', sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $post ) ) ), 'after' ); + wp_enqueue_script( 'wp-edit-site' ); + wp_enqueue_script( 'wp-format-library' ); + wp_enqueue_style( 'wp-edit-site' ); + wp_enqueue_style( 'wp-format-library' ); + + if ( + current_theme_supports( 'wp-block-styles' ) || + ( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 ) + ) { + wp_enqueue_style( 'wp-block-library-theme' ); + } + /** * Fires after block assets have been enqueued for the editing interface. * @@ -166,18 +151,6 @@ function gutenberg_edit_site_init( $hook ) { do_action( 'enqueue_block_editor_assets' ); - wp_enqueue_script( 'wp-edit-site' ); - wp_enqueue_script( 'wp-format-library' ); - wp_enqueue_style( 'wp-edit-site' ); - wp_enqueue_style( 'wp-format-library' ); - - if ( - current_theme_supports( 'wp-block-styles' ) || - ( ! is_array( $editor_styles ) || count( $editor_styles ) === 0 ) - ) { - wp_enqueue_style( 'wp-block-library-theme' ); - } - } add_action( 'admin_enqueue_scripts', 'gutenberg_edit_site_init' ); diff --git a/lib/full-site-editing/template-loader.php b/lib/full-site-editing/template-loader.php index e0047b81c21f1c..f90f07c02fff55 100644 --- a/lib/full-site-editing/template-loader.php +++ b/lib/full-site-editing/template-loader.php @@ -65,6 +65,19 @@ function gutenberg_override_query_template( $template, $type, array $templates = global $_wp_current_template_content; $current_template = gutenberg_resolve_template( $type, $templates ); + // Allow falling back to a PHP template if it has a higher priority than the block template. + $current_template_slug = basename( $template, '.php' ); + $current_block_template_slug = is_object( $current_template ) ? $current_template->slug : false; + foreach ( $templates as $template_item ) { + $template_item_slug = gutenberg_strip_php_suffix( $template_item ); + + // Don't override the template if we find a template matching the slug we look for + // and which does not match a block template slug. + if ( $current_template_slug !== $current_block_template_slug && $current_template_slug === $template_item_slug ) { + return $template; + } + } + if ( $current_template ) { $_wp_current_template_content = empty( $current_template->content ) ? __( 'Empty template.', 'gutenberg' ) : $current_template->content; diff --git a/lib/global-styles.php b/lib/global-styles.php index 9a935de29145eb..7a7225f4bf816c 100644 --- a/lib/global-styles.php +++ b/lib/global-styles.php @@ -158,7 +158,9 @@ function gutenberg_experimental_global_styles_get_stylesheet( $tree, $type = 'al * and enqueues the resulting stylesheet. */ function gutenberg_experimental_global_styles_enqueue_assets() { - if ( ! WP_Theme_JSON_Resolver::theme_has_support() ) { + if ( + ! get_theme_support( 'experimental-link-color' ) && // link color support needs the presets CSS variables regardless of the presence of theme.json file. + ! WP_Theme_JSON_Resolver::theme_has_support() ) { return; } @@ -231,7 +233,10 @@ function_exists( 'gutenberg_is_edit_site_page' ) && $settings['__experimentalGlobalStylesUserEntityId'] = $user_cpt_id; $settings['__experimentalGlobalStylesBaseStyles'] = $base_styles; - } elseif ( WP_Theme_JSON_Resolver::theme_has_support() ) { + } elseif ( + WP_Theme_JSON_Resolver::theme_has_support() || + get_theme_support( 'experimental-link-color' ) // link color support needs the presets CSS variables regardless of the presence of theme.json file. + ) { // STEP 3 - ADD STYLES IF THEME HAS SUPPORT // // If we are in a block editor context, but not in edit-site, diff --git a/lib/load.php b/lib/load.php index 0ac067e9d8166f..e697119a6721f2 100644 --- a/lib/load.php +++ b/lib/load.php @@ -107,6 +107,7 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/client-assets.php'; require __DIR__ . '/demo.php'; require __DIR__ . '/widgets.php'; +require __DIR__ . '/widgets-api.php'; require __DIR__ . '/widgets-customize.php'; require __DIR__ . '/navigation.php'; require __DIR__ . '/navigation-page.php'; @@ -120,3 +121,4 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/block-supports/typography.php'; require __DIR__ . '/block-supports/custom-classname.php'; require __DIR__ . '/block-supports/border.php'; +require __DIR__ . '/block-supports/layout.php'; diff --git a/lib/navigation-page.php b/lib/navigation-page.php index 2ea6ff94f1ec23..1837245544c530 100644 --- a/lib/navigation-page.php +++ b/lib/navigation-page.php @@ -40,26 +40,35 @@ function gutenberg_navigation_init( $hook ) { ); $settings = gutenberg_experimental_global_styles_settings( $settings ); - wp_add_inline_script( - 'wp-edit-navigation', - sprintf( - 'wp.domReady( function() { - wp.editNavigation.initialize( "navigation-editor", %s ); - } );', - wp_json_encode( $settings ) + gutenberg_initialize_editor( + 'navigation_editor', + 'edit-navigation', + array( + 'initializer_name' => 'initialize', + 'editor_settings' => $settings, ) ); - // Preload server-registered block schemas. - wp_add_inline_script( - 'wp-blocks', - 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');' - ); - wp_enqueue_script( 'wp-edit-navigation' ); wp_enqueue_style( 'wp-edit-navigation' ); - wp_enqueue_style( 'wp-block-library' ); wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-format-library' ); + do_action( 'enqueue_block_editor_assets' ); } add_action( 'admin_enqueue_scripts', 'gutenberg_navigation_init' ); + +/** + * Tells the script loader to load the scripts and styles of custom block on navigation editor screen. + * + * @param bool $is_block_editor_screen Current decision about loading block assets. + * @return bool Filtered decision about loading block assets. + */ +function gutenberg_navigation_editor_load_block_editor_scripts_and_styles( $is_block_editor_screen ) { + if ( is_callable( 'get_current_screen' ) && 'gutenberg_page_gutenberg-navigation' === get_current_screen()->base ) { + return true; + } + + return $is_block_editor_screen; +} + +add_filter( 'should_load_block_editor_scripts_and_styles', 'gutenberg_navigation_editor_load_block_editor_scripts_and_styles' ); diff --git a/lib/utils.php b/lib/utils.php index 4a38c86cd1eae1..8e2f18051e6f3d 100644 --- a/lib/utils.php +++ b/lib/utils.php @@ -59,7 +59,7 @@ function gutenberg_experimental_set( &$array, $path, $value = null ) { ) { $array[ $path_element ] = array(); } - $array = &$array[ $path_element ]; + $array = &$array[ $path_element ]; // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration } $array[ $path[ $i ] ] = $value; } diff --git a/lib/widgets-api.php b/lib/widgets-api.php new file mode 100644 index 00000000000000..dde5901d20b0eb --- /dev/null +++ b/lib/widgets-api.php @@ -0,0 +1,207 @@ + $widgets ) { + foreach ( $widgets as $i => $maybe_widget_id ) { + if ( $widget_id === $maybe_widget_id && $sidebar_id !== $maybe_sidebar_id ) { + unset( $sidebars[ $maybe_sidebar_id ][ $i ] ); + // We could technically break 2 here, but continue looping in case the id is duplicated. + continue 2; + } + } + } + + if ( $sidebar_id ) { + $sidebars[ $sidebar_id ][] = $widget_id; + } + + wp_set_sidebars_widgets( $sidebars ); +} + +/** + * Converts a widget ID into its id_base and number components. + * + * Belongs in wp-includes/widgets.php when merged to Core. + * WP_Customize_Widgets::parse_widget_id should then be deprecated. + * + * @since 10.2.0 + * + * @param string $id Widget ID. + * @return array Array containing a widget's id_base and number components. + */ +function gutenberg_parse_widget_id( $id ) { + $parsed = array(); + + if ( preg_match( '/^(.+)-(\d+)$/', $id, $matches ) ) { + $parsed['id_base'] = $matches[1]; + $parsed['number'] = (int) $matches[2]; + } else { + // Likely an old single widget. + $parsed['id_base'] = $id; + } + + return $parsed; +} + +/** + * Calls the render callback of a widget and returns the output. + * + * Belongs in wp-includes/widgets.php when merged to Core. Some of the code in + * dynamic_sidebar() and WP_Customize_Widgets should then be DRYed up. + * + * @since 10.2.0 + * + * @param string $widget_id Widget ID. + * @param string $sidebar_id Sidebar ID. + * @return string + */ +function gutenberg_render_widget( $widget_id, $sidebar_id ) { + global $wp_registered_widgets, $wp_registered_sidebars; + + if ( ! isset( $wp_registered_widgets[ $widget_id ] ) ) { + return ''; + } + + if ( isset( $wp_registered_sidebars[ $sidebar_id ] ) ) { + $sidebar = $wp_registered_sidebars[ $sidebar_id ]; + } elseif ( 'wp_inactive_widgets' === $sidebar_id ) { + $sidebar = array(); + } else { + return ''; + } + + $params = array_merge( + array( + array_merge( + $sidebar, + array( + 'widget_id' => $widget_id, + 'widget_name' => $wp_registered_widgets[ $widget_id ]['name'], + ) + ), + ), + (array) $wp_registered_widgets[ $widget_id ]['params'] + ); + + // Substitute HTML `id` and `class` attributes into `before_widget`. + $classname_ = ''; + foreach ( (array) $wp_registered_widgets[ $widget_id ]['classname'] as $cn ) { + if ( is_string( $cn ) ) { + $classname_ .= '_' . $cn; + } elseif ( is_object( $cn ) ) { + $classname_ .= '_' . get_class( $cn ); + } + } + $classname_ = ltrim( $classname_, '_' ); + $params[0]['before_widget'] = sprintf( $params[0]['before_widget'], $widget_id, $classname_ ); + + /** This filter is documented in wp-includes/widgets.php */ + $params = apply_filters( 'dynamic_sidebar_params', $params ); + + $callback = $wp_registered_widgets[ $widget_id ]['callback']; + + ob_start(); + + /** This filter is documented in wp-includes/widgets.php */ + do_action( 'dynamic_sidebar', $wp_registered_widgets[ $widget_id ] ); + + if ( is_callable( $callback ) ) { + call_user_func_array( $callback, $params ); + } + + return ob_get_clean(); +} + +/** + * Calls the control callback of a widget and returns the output. + * + * Belongs in wp-admin/includes/widgets.php when merged to Core. Some of the + * code in wp_widget_control() should then be DRYed up. + * + * @since 10.2.0 + * + * @param string $id Widget ID. + * @return string|null + */ +function gutenberg_render_widget_control( $id ) { + global $wp_registered_widget_controls; + + if ( ! isset( $wp_registered_widget_controls[ $id ]['callback'] ) ) { + return null; + } + + $callback = $wp_registered_widget_controls[ $id ]['callback']; + $params = $wp_registered_widget_controls[ $id ]['params']; + + ob_start(); + + if ( is_callable( $callback ) ) { + call_user_func_array( $callback, $params ); + } + + return ob_get_clean(); +} + +/** + * Returns the instance settings of the given widget. Must be a widget that + * is registered using WP_Widget. + * + * Belongs in WP_Widget when merged to Core. + * + * @since 10.2.0 + * + * @param string $id Widget ID. + * @return array|null + */ +function gutenberg_get_widget_instance( $id ) { + $parsed_id = gutenberg_parse_widget_id( $id ); + $widget_object = gutenberg_get_widget_object( $parsed_id['id_base'] ); + + if ( ! isset( $parsed_id['number'] ) || ! $widget_object ) { + return null; + } + + $all_instances = $widget_object->get_settings(); + return $all_instances[ $parsed_id['number'] ]; +} + +/** + * Returns the registered WP_Widget object for the given widget type. + * + * Belongs in WP_Widget_Factory when merged to Core. + * + * @since 10.2.0 + * + * @param string $id_base Widget type ID. + * @return WP_Widget|null + */ +function gutenberg_get_widget_object( $id_base ) { + global $wp_widget_factory; + + foreach ( $wp_widget_factory->widgets as $widget_object ) { + if ( $widget_object->id_base === $id_base ) { + return $widget_object; + } + } + + return null; +} diff --git a/lib/widgets-page.php b/lib/widgets-page.php index 6815bf5b75b7a7..92c98a696c9c1f 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -41,8 +41,6 @@ function gutenberg_widgets_init( $hook ) { return; } - $initializer_name = 'initialize'; - $settings = array_merge( gutenberg_get_common_block_editor_settings(), gutenberg_get_legacy_widget_settings() @@ -54,47 +52,25 @@ function gutenberg_widgets_init( $hook ) { $settings = gutenberg_experimental_global_styles_settings( $settings ); $settings = gutenberg_extend_block_editor_styles( $settings ); - $preload_paths = array( - array( '/wp/v2/media', 'OPTIONS' ), - '/wp/v2/sidebars?context=edit&per_page=-1', - '/wp/v2/widgets?context=edit&per_page=-1', - ); - $preload_data = array_reduce( - $preload_paths, - 'rest_preload_api_request', - array() - ); - wp_add_inline_script( - 'wp-api-fetch', - sprintf( - 'wp.apiFetch.use( wp.apiFetch.createPreloadingMiddleware( %s ) );', - wp_json_encode( $preload_data ) - ), - 'after' - ); - - wp_add_inline_script( - 'wp-edit-widgets', - sprintf( - 'wp.domReady( function() { - wp.editWidgets.%s( "widgets-editor", %s ); - } );', - $initializer_name, - wp_json_encode( $settings ) + gutenberg_initialize_editor( + 'widgets_editor', + 'edit-widgets', + array( + 'preload_paths' => array( + array( '/wp/v2/media', 'OPTIONS' ), + '/wp/v2/sidebars?context=edit&per_page=-1', + '/wp/v2/widgets?context=edit&per_page=-1', + ), + 'editor_settings' => $settings, ) ); - // Preload server-registered block schemas. - wp_add_inline_script( - 'wp-blocks', - 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');' - ); - wp_enqueue_script( 'wp-edit-widgets' ); wp_enqueue_script( 'admin-widgets' ); wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-edit-widgets' ); wp_enqueue_style( 'wp-format-library' ); + do_action( 'enqueue_block_editor_assets' ); } add_action( 'admin_enqueue_scripts', 'gutenberg_widgets_init' ); diff --git a/lib/widgets.php b/lib/widgets.php index 4236a66413ba67..90090b23cd687d 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -284,3 +284,38 @@ function gutenberg_load_widget_preview_if_requested() { } add_filter( 'load-appearance_page_gutenberg-widgets', 'gutenberg_load_widget_preview_if_requested' ); +/** + * Sets show_instance_in_rest to true on all of the core WP_Widget subclasses. + * When merge dto Core, this property should be added to WP_Widget and set to + * true on each WP_Widget subclass. + */ +function gutenberg_set_show_instance_in_rest_on_core_widgets() { + global $wp_widget_factory; + + $core_widgets = array( + 'WP_Widget_Pages', + 'WP_Widget_Calendar', + 'WP_Widget_Archives', + 'WP_Widget_Media_Audio', + 'WP_Widget_Media_Image', + 'WP_Widget_Media_Gallery', + 'WP_Widget_Media_Video', + 'WP_Widget_Meta', + 'WP_Widget_Search', + 'WP_Widget_Text', + 'WP_Widget_Categories', + 'WP_Widget_Recent_Posts', + 'WP_Widget_Recent_Comments', + 'WP_Widget_RSS', + 'WP_Widget_Tag_Cloud', + 'WP_Nav_Menu_Widget', + 'WP_Widget_Custom_HTML', + ); + + foreach ( $core_widgets as $widget ) { + if ( isset( $wp_widget_factory->widgets[ $widget ] ) ) { + $wp_widget_factory->widgets[ $widget ]->show_instance_in_rest = true; + } + } +} +add_action( 'widgets_init', 'gutenberg_set_show_instance_in_rest_on_core_widgets' ); diff --git a/package-lock.json b/package-lock.json index a5ce85b9b1df3c..0602f3984ea008 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "10.2.0-rc.1", + "version": "10.3.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -46,34 +46,41 @@ } }, "@babel/compat-data": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.12.7.tgz", - "integrity": "sha512-YaxPMGs/XIWtYqrdEOZOCPsVWfEoriXopnsz3/i7apYPXQ3698UFhS6dVT1KN5qOsWmVgw/FOrmQgpRaZayGsw==", - "dev": true + "version": "7.13.11", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.11.tgz", + "integrity": "sha512-BwKEkO+2a67DcFeS3RLl0Z3Gs2OvdXewuWjc1Hfokhb5eQWP9YRYH1/+VrVZvql2CfjOiNGqSAFOYt4lsqTHzg==" }, "@babel/core": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", - "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helpers": "^7.12.5", - "@babel/parser": "^7.12.7", - "@babel/template": "^7.12.7", - "@babel/traverse": "^7.12.9", - "@babel/types": "^7.12.7", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.10.tgz", + "integrity": "sha512-bfIYcT0BdKeAZrovpMqX2Mx5NrgAckGbwT982AkdS5GNfn3KMGiprlBAtmBcFZRUmpaufS6WZFP8trvx8ptFDw==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.9", + "@babel/helper-compilation-targets": "^7.13.10", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helpers": "^7.13.10", + "@babel/parser": "^7.13.10", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", + "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", + "semver": "^6.3.0", "source-map": "^0.5.0" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "requires": { + "@babel/highlight": "^7.12.13" + } + }, "convert-source-map": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", @@ -91,9 +98,9 @@ } }, "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", "requires": { "minimist": "^1.2.5" } @@ -104,231 +111,235 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, "@babel/generator": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.12.5.tgz", - "integrity": "sha512-m16TQQJ8hPt7E+OS/XVQg/7U184MLXtvuGbCdA7na61vha+ImkyyNM/9DDA0unYCVZn3ZOhng+qz48/KBOT96A==", + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.13.9.tgz", + "integrity": "sha512-mHOOmY0Axl/JCTkxTU6Lf5sWOg/v8nUa+Xkt4zMTftX0wqmb6Sh7J8gvcehBw7q0AhrhAR+FDacKjCZ2X8K+Sw==", "requires": { - "@babel/types": "^7.12.5", + "@babel/types": "^7.13.0", "jsesc": "^2.5.1", "source-map": "^0.5.0" } }, "@babel/helper-annotate-as-pure": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz", - "integrity": "sha512-XQlqKQP4vXFB7BN8fEEerrmYvHp3fK/rBkRFz9jaJbzK0B1DSfej9Kc7ZzE8Z/OnId1jpJdNAZ3BFQjWG68rcA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.13.tgz", + "integrity": "sha512-7YXfX5wQ5aYM/BOlbSccHDbuXXFPxeoUmfWtz8le2yTkTZc+BxsiEnENFoi2SlmA8ewDkG2LgIMIVzzn2h8kfw==", "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.13" } }, "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.10.4.tgz", - "integrity": "sha512-L0zGlFrGWZK4PbT8AszSfLTM5sDU1+Az/En9VrdT8/LmEiJt4zXt+Jve9DCAnQcbqDhCI+29y/L93mrDzddCcg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.12.13.tgz", + "integrity": "sha512-CZOv9tGphhDRlVjVkAgm8Nhklm9RzSmWpX2my+t7Ua/KT616pEzXsQCjinzvkRvHWJ9itO4f296efroX23XCMA==", "requires": { - "@babel/helper-explode-assignable-expression": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.10.4.tgz", - "integrity": "sha512-5nPcIZ7+KKDxT1427oBivl9V9YTal7qk0diccnh7RrcgrT/pGFOjgGw1dgryyx1GvHEpXVfoDF6Ak3rTiWh8Rg==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/types": "^7.10.4" - } - }, - "@babel/helper-builder-react-jsx-experimental": { - "version": "7.12.4", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.12.4.tgz", - "integrity": "sha512-AjEa0jrQqNk7eDQOo0pTfUOwQBMF+xVqrausQwT9/rTKy0g04ggFNaJpaE09IQMn9yExluigWMJcj0WC7bq+Og==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-module-imports": "^7.12.1", - "@babel/types": "^7.12.1" + "@babel/helper-explode-assignable-expression": "^7.12.13", + "@babel/types": "^7.12.13" } }, "@babel/helper-compilation-targets": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.12.5.tgz", - "integrity": "sha512-+qH6NrscMolUlzOYngSBMIOQpKUGPPsc61Bu5W10mg84LxZ7cmvnBHzARKbDoFxVvqqAbj6Tg6N7bSrWSPXMyw==", - "dev": true, + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.10.tgz", + "integrity": "sha512-/Xju7Qg1GQO4mHZ/Kcs6Au7gfafgZnwm+a7sy/ow/tV1sHeraRUHbjdat8/UvDor4Tez+siGKDk6zIKtCPKVJA==", "requires": { - "@babel/compat-data": "^7.12.5", - "@babel/helper-validator-option": "^7.12.1", + "@babel/compat-data": "^7.13.8", + "@babel/helper-validator-option": "^7.12.17", "browserslist": "^4.14.5", - "semver": "^5.5.0" + "semver": "^6.3.0" }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, "@babel/helper-create-class-features-plugin": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.12.1.tgz", - "integrity": "sha512-hkL++rWeta/OVOBTRJc9a5Azh5mt5WgZUGAKMD8JM141YsE08K//bp1unBBieO6rUKkIPyUE0USQ30jAy3Sk1w==", + "version": "7.13.11", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.13.11.tgz", + "integrity": "sha512-ays0I7XYq9xbjCSvT+EvysLgfc3tOkwCULHjrnscGT3A9qD4sk3wXnJ3of0MAWsWGjdinFvajHU2smYuqXKMrw==", "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.10.4" + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-member-expression-to-functions": "^7.13.0", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-split-export-declaration": "^7.12.13" } }, "@babel/helper-create-regexp-features-plugin": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.7.tgz", - "integrity": "sha512-idnutvQPdpbduutvi3JVfEgcVIHooQnhvhx0Nk9isOINOIGYkZea1Pk2JlJRiUnMefrlvr0vkByATBY/mB4vjQ==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.12.17.tgz", + "integrity": "sha512-p2VGmBu9oefLZ2nQpgnEnG0ZlRPvL8gAGvPUMQwUdaE8k49rOMuZpOwdQoy5qJf6K8jL3bcAMhVUlHAjIgJHUg==", "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", + "@babel/helper-annotate-as-pure": "^7.12.13", "regexpu-core": "^4.7.1" + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.1.5.tgz", + "integrity": "sha512-nXuzCSwlJ/WKr8qxzW816gwyT6VZgiJG17zR40fou70yfAcqjoNyTLl/DQ+FExw5Hx5KNqshmN8Ldl/r2N7cTg==", + "requires": { + "@babel/helper-compilation-targets": "^7.13.0", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/traverse": "^7.13.0", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" }, "dependencies": { - "regexpu-core": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", - "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "path-parse": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, - "@babel/helper-define-map": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.10.5.tgz", - "integrity": "sha512-fMw4kgFB720aQFXSVaXr79pjjcW5puTCM16+rECJ/plGS+zByelE8l9nCpV1GibxTnFVmUuYG9U8wYfQHdzOEQ==", - "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/types": "^7.10.5", - "lodash": "^4.17.19" - } - }, "@babel/helper-explode-assignable-expression": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.12.1.tgz", - "integrity": "sha512-dmUwH8XmlrUpVqgtZ737tK88v07l840z9j3OEhCLwKTkjlvKpfqXVIZ0wpK3aeOxspwGrf/5AP5qLx4rO3w5rA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.13.0.tgz", + "integrity": "sha512-qS0peLTDP8kOisG1blKbaoBg/o9OSa1qoumMjTK5pM+KDTtpxpsiubnCGP34vK8BXGcb2M9eigwgvoJryrzwWA==", "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.13.0" } }, "@babel/helper-function-name": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", - "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", "requires": { - "@babel/helper-get-function-arity": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" } }, "@babel/helper-get-function-arity": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", - "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", "requires": { - "@babel/types": "^7.10.4" + "@babel/types": "^7.12.13" } }, "@babel/helper-hoist-variables": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.10.4.tgz", - "integrity": "sha512-wljroF5PgCk2juF69kanHVs6vrLwIPNp6DLD+Lrl3hoQ3PpPPikaDRNFA+0t81NOoMt2DL6WW/mdU8k4k6ZzuA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz", + "integrity": "sha512-0kBzvXiIKfsCA0y6cFEIJf4OdzfpRuNk4+YTeHZpGGc666SATFKTz6sRncwFnQk7/ugJ4dSrCj6iJuvW4Qwr2g==", "dev": true, "requires": { - "@babel/types": "^7.10.4" + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" } }, "@babel/helper-member-expression-to-functions": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.12.7.tgz", - "integrity": "sha512-DCsuPyeWxeHgh1Dus7APn7iza42i/qXqiFPWyBDdOFtvS581JQePsc1F/nD+fHrcswhLlRc2UpYS1NwERxZhHw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz", + "integrity": "sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ==", "requires": { - "@babel/types": "^7.12.7" + "@babel/types": "^7.13.0" } }, "@babel/helper-module-imports": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.5.tgz", - "integrity": "sha512-SR713Ogqg6++uexFRORf/+nPXMmWIn80TALu0uaFb+iQIUoR7bOC7zBWyzBs5b3tBBJXuyD0cRu1F15GyzjOWA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", + "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", "requires": { - "@babel/types": "^7.12.5" + "@babel/types": "^7.12.13" } }, "@babel/helper-module-transforms": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.12.1.tgz", - "integrity": "sha512-QQzehgFAZ2bbISiCpmVGfiGux8YVFXQ0abBic2Envhej22DVXV9nCFaS5hIQbkyo1AdGb+gNME2TSh3hYJVV/w==", - "requires": { - "@babel/helper-module-imports": "^7.12.1", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-simple-access": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/helper-validator-identifier": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.1", - "@babel/types": "^7.12.1", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz", + "integrity": "sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw==", + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-simple-access": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.12.11", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", "lodash": "^4.17.19" } }, "@babel/helper-optimise-call-expression": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.7.tgz", - "integrity": "sha512-I5xc9oSJ2h59OwyUqjv95HRyzxj53DAubUERgQMrpcCEYQyToeHA+NEcUEsVWB4j53RDeskeBJ0SgRAYHDBckw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", "requires": { - "@babel/types": "^7.12.7" + "@babel/types": "^7.12.13" } }, "@babel/helper-plugin-utils": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", - "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==" + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.13.0.tgz", + "integrity": "sha512-ZPafIPSwzUlAoWT8DKs1W2VyF2gOWthGd5NGFMsBcMMol+ZhK+EQY/e6V96poa6PA/Bh+C9plWN0hXO1uB8AfQ==" }, "@babel/helper-remap-async-to-generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.12.1.tgz", - "integrity": "sha512-9d0KQCRM8clMPcDwo8SevNs+/9a8yWVVmaE80FGJcEP8N1qToREmWEGnBn8BUlJhYRFz6fqxeRL1sl5Ogsed7A==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.13.0.tgz", + "integrity": "sha512-pUQpFBE9JvC9lrQbpX0TmeNIy5s7GnZjna2lhhcHC7DzgBs6fWn722Y5cfwgrtrqc7NAJwMvOa0mKhq6XaE4jg==", "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-wrap-function": "^7.10.4", - "@babel/types": "^7.12.1" + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-wrap-function": "^7.13.0", + "@babel/types": "^7.13.0" } }, "@babel/helper-replace-supers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.12.5.tgz", - "integrity": "sha512-5YILoed0ZyIpF4gKcpZitEnXEJ9UoDRki1Ey6xz46rxOzfNMAhVIJMoune1hmPVxh40LRv1+oafz7UsWX+vyWA==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz", + "integrity": "sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw==", "requires": { - "@babel/helper-member-expression-to-functions": "^7.12.1", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" + "@babel/helper-member-expression-to-functions": "^7.13.0", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" } }, "@babel/helper-simple-access": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.1.tgz", - "integrity": "sha512-OxBp7pMrjVewSSC8fXDFrHrBcJATOOFssZwv16F3/6Xtc138GHybBfPbm9kfiqQHKhYQrlamWILwlDCeyMFEaA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", + "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", "requires": { - "@babel/types": "^7.12.1" + "@babel/types": "^7.12.13" } }, "@babel/helper-skip-transparent-expression-wrappers": { @@ -340,51 +351,50 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.11.0", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", - "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", "requires": { - "@babel/types": "^7.11.0" + "@babel/types": "^7.12.13" } }, "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==" + "version": "7.12.11", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz", + "integrity": "sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw==" }, "@babel/helper-validator-option": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.1.tgz", - "integrity": "sha512-YpJabsXlJVWP0USHjnC/AQDTLlZERbON577YUVO/wLpqyj6HAtVYnWaQaN0iUN+1/tWn3c+uKKXjRut5115Y2A==", - "dev": true + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==" }, "@babel/helper-wrap-function": { - "version": "7.12.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.12.3.tgz", - "integrity": "sha512-Cvb8IuJDln3rs6tzjW3Y8UeelAOdnpB8xtQ4sme2MSZ9wOxrbThporC0y/EtE16VAtoyEfLM404Xr1e0OOp+ow==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.13.0.tgz", + "integrity": "sha512-1UX9F7K3BS42fI6qd2A4BjKzgGjToscyZTdp1DjknHLCIvpgne6918io+aL5LXFcER/8QWiwpoY902pVEqgTXA==", "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/helper-function-name": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" } }, "@babel/helpers": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.12.5.tgz", - "integrity": "sha512-lgKGMQlKqA8meJqKsW6rUnc4MdUk35Ln0ATDqdM1a/UpARODdI4j5Y5lVfUScnSNkJcdCRAaWkspykNoFg9sJA==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", + "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.12.5", - "@babel/types": "^7.12.5" + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0" } }, "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.13.10.tgz", + "integrity": "sha512-5aPpe5XQPzflQrFwL1/QoeHkP2MsA4JCntcXHRhEsdsfPVkvPi2w7Qix4iV7t5S/oC9OodGrggd8aco1g3SZFg==", "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "chalk": "^2.0.0", "js-tokens": "^4.0.0" }, @@ -402,164 +412,166 @@ } }, "@babel/parser": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.7.tgz", - "integrity": "sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg==" + "version": "7.13.11", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.11.tgz", + "integrity": "sha512-PhuoqeHoO9fc4ffMEVk4qb/w/s2iOSWohvbHxLtxui0eBg3Lg5gN1U8wp1V1u61hOWkPQJJyJzGH6Y+grwkq8Q==" }, "@babel/plugin-external-helpers": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.12.1.tgz", - "integrity": "sha512-5VBqan0daXhDSRjrq2miABuELRwWJWFdM42Jvs/CDuhp+Es+fW+ISA5l+co8d+9oN3WLz/N3VvzyeseL3AvjxA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-external-helpers/-/plugin-external-helpers-7.12.13.tgz", + "integrity": "sha512-ClvAsk4RqpE6iacYUjdU9PtvIwC9yAefZENsPfGeG5FckX3jFZLDlWPuyv5gi9/9C2VgwX6H8q1ukBifC0ha+Q==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-proposal-async-generator-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.12.1.tgz", - "integrity": "sha512-d+/o30tJxFxrA1lhzJqiUcEJdI6jKlNregCv5bASeGf2Q4MXmnwH7viDo7nhx1/ohf09oaH8j1GVYG/e3Yqk6A==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.13.8.tgz", + "integrity": "sha512-rPBnhj+WgoSmgq+4gQUtXx/vOcU+UYtjy1AA/aeD61Hwj410fwYyqfUcRP3lR8ucgliVJL/G7sXcNUecC75IXA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.12.1", - "@babel/plugin-syntax-async-generators": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-remap-async-to-generator": "^7.13.0", + "@babel/plugin-syntax-async-generators": "^7.8.4" } }, "@babel/plugin-proposal-class-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.12.1.tgz", - "integrity": "sha512-cKp3dlQsFsEs5CWKnN7BnSHOd0EOW8EKpEjkoz1pO2E5KzIDNV9Ros1b0CnmbVgAGXJubOYVBOGCT1OmJwOI7w==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.13.0.tgz", + "integrity": "sha512-KnTDjFNC1g+45ka0myZNvSBFLhNCLN+GeGYLDEA8Oq7MZ6yMgfLoIRh86GRT0FjtJhZw8JyUskP9uvj5pHM9Zg==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-proposal-decorators": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.12.1.tgz", - "integrity": "sha512-knNIuusychgYN8fGJHONL0RbFxLGawhXOJNLBk75TniTsZZeA+wdkDuv6wp4lGwzQEKjZi6/WYtnb3udNPmQmQ==", + "version": "7.13.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.13.5.tgz", + "integrity": "sha512-i0GDfVNuoapwiheevUOuSW67mInqJ8qw7uWfpjNVeHMn143kXblEy/bmL9AdZ/0yf/4BMQeWXezK0tQIvNPqag==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-decorators": "^7.12.1" + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-decorators": "^7.12.13" } }, "@babel/plugin-proposal-dynamic-import": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.12.1.tgz", - "integrity": "sha512-a4rhUSZFuq5W8/OO8H7BL5zspjnc1FLd9hlOxIK/f7qG4a0qsqk8uvF/ywgBA8/OmjsapjpvaEOYItfGG1qIvQ==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.13.8.tgz", + "integrity": "sha512-ONWKj0H6+wIRCkZi9zSbZtE/r73uOhMVHh256ys0UzfM7I3d4n+spZNWjOnJv2gzopumP2Wxi186vI8N0Y2JyQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-dynamic-import": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" } }, "@babel/plugin-proposal-export-default-from": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.12.1.tgz", - "integrity": "sha512-z5Q4Ke7j0AexQRfgUvnD+BdCSgpTEKnqQ3kskk2jWtOBulxICzd1X9BGt7kmWftxZ2W3++OZdt5gtmC8KLxdRQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.12.13.tgz", + "integrity": "sha512-idIsBT+DGXdOHL82U+8bwX4goHm/z10g8sGGrQroh+HCRcm7mDv/luaGdWJQMTuCX2FsdXS7X0Nyyzp4znAPJA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-export-default-from": "^7.12.1" + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-export-default-from": "^7.12.13" } }, "@babel/plugin-proposal-export-namespace-from": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.1.tgz", - "integrity": "sha512-6CThGf0irEkzujYS5LQcjBx8j/4aQGiVv7J9+2f7pGfxqyKh3WnmVJYW3hdrQjyksErMGBPQrCnHfOtna+WLbw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.12.13.tgz", + "integrity": "sha512-INAgtFo4OnLN3Y/j0VwAgw3HDXcDtX+C/erMvWzuV9v71r7urb6iyMXu7eM9IgLr1ElLlOkaHjJ0SbCmdOQ3Iw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-plugin-utils": "^7.12.13", "@babel/plugin-syntax-export-namespace-from": "^7.8.3" } }, "@babel/plugin-proposal-json-strings": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.12.1.tgz", - "integrity": "sha512-GoLDUi6U9ZLzlSda2Df++VSqDJg3CG+dR0+iWsv6XRw1rEq+zwt4DirM9yrxW6XWaTpmai1cWJLMfM8qQJf+yw==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.13.8.tgz", + "integrity": "sha512-w4zOPKUFPX1mgvTmL/fcEqy34hrQ1CRcGxdphBc6snDnnqJ47EZDIyop6IwXzAC8G916hsIuXB2ZMBCExC5k7Q==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" } }, "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.12.1.tgz", - "integrity": "sha512-k8ZmVv0JU+4gcUGeCDZOGd0lCIamU/sMtIiX3UWnUc5yzgq6YUGyEolNYD+MLYKfSzgECPcqetVcJP9Afe/aCA==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.13.8.tgz", + "integrity": "sha512-aul6znYB4N4HGweImqKn59Su9RS8lbUIqxtXTOcAGtNIDczoEFv+l1EhmX8rUBp3G1jMjKJm8m0jXVp63ZpS4A==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-plugin-utils": "^7.13.0", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" } }, "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.12.1.tgz", - "integrity": "sha512-nZY0ESiaQDI1y96+jk6VxMOaL4LPo/QDHBqL+SF3/vl6dHkTwHlOI8L4ZwuRBHgakRBw5zsVylel7QPbbGuYgg==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.13.8.tgz", + "integrity": "sha512-iePlDPBn//UhxExyS9KyeYU7RM9WScAG+D3Hhno0PLJebAEpDZMocbDe64eqynhNAnwz/vZoL/q/QB2T1OH39A==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" } }, "@babel/plugin-proposal-numeric-separator": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.7.tgz", - "integrity": "sha512-8c+uy0qmnRTeukiGsjLGy6uVs/TFjJchGXUeBqlG4VWYOdJWkhhVPdQ3uHwbmalfJwv2JsV0qffXP4asRfL2SQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.12.13.tgz", + "integrity": "sha512-O1jFia9R8BUCl3ZGB7eitaAPu62TXJRHn7rh+ojNERCFyqRwJMTmhz+tJ+k0CwI6CLjX/ee4qW74FSqlq9I35w==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-plugin-utils": "^7.12.13", "@babel/plugin-syntax-numeric-separator": "^7.10.4" } }, "@babel/plugin-proposal-object-rest-spread": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", - "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.13.8.tgz", + "integrity": "sha512-DhB2EuB1Ih7S3/IRX5AFVgZ16k3EzfRbq97CxAVI1KSYcW+lexV8VZb7G7L8zuPVSdQMRn0kiBpf/Yzu9ZKH0g==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-transform-parameters": "^7.12.1" + "@babel/compat-data": "^7.13.8", + "@babel/helper-compilation-targets": "^7.13.8", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.13.0" } }, "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.12.1.tgz", - "integrity": "sha512-hFvIjgprh9mMw5v42sJWLI1lzU5L2sznP805zeT6rySVRA0Y18StRhDqhSxlap0oVgItRsB6WSROp4YnJTJz0g==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.13.8.tgz", + "integrity": "sha512-0wS/4DUF1CuTmGo+NiaHfHcVSeSLj5S3e6RivPTg/2k3wOv3jO35tZ6/ZWsQhQMvdgI7CwphjQa/ccarLymHVA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" } }, "@babel/plugin-proposal-optional-chaining": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.12.7.tgz", - "integrity": "sha512-4ovylXZ0PWmwoOvhU2vhnzVNnm88/Sm9nx7V8BPgMvAzn5zDou3/Awy0EjglyubVHasJj+XCEkr/r1X3P5elCA==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.13.8.tgz", + "integrity": "sha512-hpbBwbTgd7Cz1QryvwJZRo1U0k1q8uyBmeXOSQUjdg/A2TASkhR/rz7AyqZ/kS8kbpsNA80rOYbxySBJAqmhhQ==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1", - "@babel/plugin-syntax-optional-chaining": "^7.8.0" + "@babel/plugin-syntax-optional-chaining": "^7.8.3" } }, "@babel/plugin-proposal-private-methods": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.12.1.tgz", - "integrity": "sha512-mwZ1phvH7/NHK6Kf8LP7MYDogGV+DKB1mryFOEwx5EBNQrosvIczzZFTUmWaeujd5xT6G1ELYWUz3CutMhjE1w==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.13.0.tgz", + "integrity": "sha512-MXyyKQd9inhx1kDYPkFRVOBXQ20ES8Pto3T7UZ92xj2mY0EVD8oAVzeyYuVfy/mxAdTSIayOvg+aVzcHV2bn6Q==", "dev": true, "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.1.tgz", - "integrity": "sha512-MYq+l+PvHuw/rKUz1at/vb6nCnQ2gmJBNaM62z0OgH7B2W1D9pvkpYtlti9bGtizNIU1K3zm4bZF9F91efVY0w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.12.13.tgz", + "integrity": "sha512-XyJmZidNfofEkqFV5VC/bLabGmO5QzenPO/YOfGuEbgU+2sSwMmio3YLb4WtBgcmmdwZHyVyv8on77IUjQ5Gvg==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-async-generators": { @@ -581,20 +593,20 @@ } }, "@babel/plugin-syntax-class-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.1.tgz", - "integrity": "sha512-U40A76x5gTwmESz+qiqssqmeEsKvcSyvtgktrm0uzcARAmM9I1jR221f6Oq+GmHrcD+LvZDag1UTOTe2fL3TeA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-decorators": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.1.tgz", - "integrity": "sha512-ir9YW5daRrTYiy9UJ2TzdNIJEZu8KclVzDcfSt4iEmOtwQ4llPtWInNKJyKnVXp1vE4bbVd5S31M/im3mYMO1w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.12.13.tgz", + "integrity": "sha512-Rw6aIXGuqDLr6/LoBBYE57nKOzQpz/aDkKlMqEwH+Vp0MXbG6H/TfRjaY343LKxzAKAMXIHsQ8JzaZKuDZ9MwA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-dynamic-import": { @@ -606,11 +618,11 @@ } }, "@babel/plugin-syntax-export-default-from": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.12.1.tgz", - "integrity": "sha512-dP5eGg6tHEkhnRD2/vRG/KJKRSg8gtxu2i+P/8/yFPJn/CfPU5G0/7Gks2i3M6IOVAPQekmsLN9LPsmXFFL4Uw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.12.13.tgz", + "integrity": "sha512-gVry0zqoums0hA+EniCYK3gABhjYSLX1dVuwYpPw9DrLNA4/GovXySHVg4FGRsZht09ON/5C2NVx3keq+qqVGQ==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-export-namespace-from": { @@ -623,11 +635,11 @@ } }, "@babel/plugin-syntax-flow": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.1.tgz", - "integrity": "sha512-1lBLLmtxrwpm4VKmtVFselI/P3pX+G63fAtUUt6b2Nzgao77KNDwyuRt90Mj2/9pKobtt68FdvjfqohZjg/FCA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.12.13.tgz", + "integrity": "sha512-J/RYxnlSLXZLVR7wTRsozxKT8qbsx1mNKJzXEEjQ0Kjx1ZACcyHgbanNWNCFtc36IzuWhYWPpvJFFoexoOWFmA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-import-meta": { @@ -649,11 +661,11 @@ } }, "@babel/plugin-syntax-jsx": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", - "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.13.tgz", + "integrity": "sha512-d4HM23Q1K7oq/SLNmG6mRt85l2csmQ0cHRaxRXjKW0YFdEXqlZ5kzFQKH5Uc3rDJECgu+yCRgPkG04Mm98R/1g==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-logical-assignment-operators": { @@ -707,68 +719,67 @@ } }, "@babel/plugin-syntax-top-level-await": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz", - "integrity": "sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.13.tgz", + "integrity": "sha512-A81F9pDwyS7yM//KwbCSDqy3Uj4NMIurtplxphWxoYtNPov7cJsDkAFNNyVlIZ3jwGycVsurZ+LtOA8gZ376iQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-syntax-typescript": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.1.tgz", - "integrity": "sha512-UZNEcCY+4Dp9yYRCAHrHDU+9ZXLYaY9MgBXSRLkB9WjYFRR6quJBumfVrEkUxrePPBwFcpWfNKXqVRQQtm7mMA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.12.13.tgz", + "integrity": "sha512-cHP3u1JiUiG2LFDKbXnwVad81GvfyIOmCD6HIEId6ojrY0Drfy2q1jw7BwN7dE84+kTnBjLkXoL3IEy/3JPu2w==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-arrow-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.12.1.tgz", - "integrity": "sha512-5QB50qyN44fzzz4/qxDPQMBCTHgxg3n0xRBLJUmBlLoU/sFvxVWGZF/ZUfMVDQuJUKXaBhbupxIzIfZ6Fwk/0A==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.13.0.tgz", + "integrity": "sha512-96lgJagobeVmazXFaDrbmCLQxBysKu7U6Do3mLsx27gf5Dk85ezysrs2BZUpXD703U/Su1xTBDxxar2oa4jAGg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-async-to-generator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.12.1.tgz", - "integrity": "sha512-SDtqoEcarK1DFlRJ1hHRY5HvJUj5kX4qmtpMAm2QnhOlyuMC4TMdCRgW6WXpv93rZeYNeLP22y8Aq2dbcDRM1A==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.13.0.tgz", + "integrity": "sha512-3j6E004Dx0K3eGmhxVJxwwI89CTJrce7lg3UrtFuDAVQ/2+SJ/h/aSFOeE6/n0WB1GsOffsJp6MnPQNQ8nmwhg==", "requires": { - "@babel/helper-module-imports": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-remap-async-to-generator": "^7.12.1" + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-remap-async-to-generator": "^7.13.0" } }, "@babel/plugin-transform-block-scoped-functions": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.1.tgz", - "integrity": "sha512-5OpxfuYnSgPalRpo8EWGPzIYf0lHBWORCkj5M0oLBwHdlux9Ri36QqGW3/LR13RSVOAoUUMzoPI/jpE4ABcHoA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.12.13.tgz", + "integrity": "sha512-zNyFqbc3kI/fVpqwfqkg6RvBgFpC4J18aKKMmv7KdQ/1GgREapSJAykLMVNwfRGO3BtHj3YQZl8kxCXPcVMVeg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-block-scoping": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.1.tgz", - "integrity": "sha512-zJyAC9sZdE60r1nVQHblcfCj29Dh2Y0DOvlMkcqSo0ckqjiCwNiUezUKw+RjOCwGfpLRwnAeQ2XlLpsnGkvv9w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.12.13.tgz", + "integrity": "sha512-Pxwe0iqWJX4fOOM2kEZeUuAxHMWb9nK+9oh5d11bsLoB0xMg+mkDpt0eYuDZB7ETrY9bbcVlKUGTOGWy7BHsMQ==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-classes": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.12.1.tgz", - "integrity": "sha512-/74xkA7bVdzQTBeSUhLLJgYIcxw/dpEpCdRDiHgPJ3Mv6uC11UhjpOhl72CgqbBCmt1qtssCyB2xnJm1+PFjog==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.10.4", - "@babel/helper-define-map": "^7.10.4", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-optimise-call-expression": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1", - "@babel/helper-split-export-declaration": "^7.10.4", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.13.0.tgz", + "integrity": "sha512-9BtHCPUARyVH1oXGcSJD3YpsqRLROJx5ZNP6tN5vnk17N0SVf9WCtf8Nuh1CFmgByKKAIMstitKduoCmsaDK5g==", + "requires": { + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-split-export-declaration": "^7.12.13", "globals": "^11.1.0" }, "dependencies": { @@ -780,273 +791,231 @@ } }, "@babel/plugin-transform-computed-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.12.1.tgz", - "integrity": "sha512-vVUOYpPWB7BkgUWPo4C44mUQHpTZXakEqFjbv8rQMg7TC6S6ZhGZ3otQcRH6u7+adSlE5i0sp63eMC/XGffrzg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.13.0.tgz", + "integrity": "sha512-RRqTYTeZkZAz8WbieLTvKUEUxZlUTdmL5KGMyZj7FnMfLNKV4+r5549aORG/mgojRmFlQMJDUupwAMiF2Q7OUg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-destructuring": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.12.1.tgz", - "integrity": "sha512-fRMYFKuzi/rSiYb2uRLiUENJOKq4Gnl+6qOv5f8z0TZXg3llUwUhsNNwrwaT/6dUhJTzNpBr+CUvEWBtfNY1cw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.13.0.tgz", + "integrity": "sha512-zym5em7tePoNT9s964c0/KU3JPPnuq7VhIxPRefJ4/s82cD+q1mgKfuGRDMCPL0HTyKz4dISuQlCusfgCJ86HA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-dotall-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.1.tgz", - "integrity": "sha512-B2pXeRKoLszfEW7J4Hg9LoFaWEbr/kzo3teWHmtFCszjRNa/b40f9mfeqZsIDLLt/FjwQ6pz/Gdlwy85xNckBA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.12.13.tgz", + "integrity": "sha512-foDrozE65ZFdUC2OfgeOCrEPTxdB3yjqxpXh8CH+ipd9CHd4s/iq81kcUpyH8ACGNEPdFqbtzfgzbT/ZGlbDeQ==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-duplicate-keys": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.1.tgz", - "integrity": "sha512-iRght0T0HztAb/CazveUpUQrZY+aGKKaWXMJ4uf9YJtqxSUe09j3wteztCUDRHs+SRAL7yMuFqUsLoAKKzgXjw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.12.13.tgz", + "integrity": "sha512-NfADJiiHdhLBW3pulJlJI2NB0t4cci4WTZ8FtdIuNc2+8pslXdPtRRAEWqUY+m9kNOk2eRYbTAOipAxlrOcwwQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-exponentiation-operator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.1.tgz", - "integrity": "sha512-7tqwy2bv48q+c1EHbXK0Zx3KXd2RVQp6OC7PbwFNt/dPTAV3Lu5sWtWuAj8owr5wqtWnqHfl2/mJlUmqkChKug==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.12.13.tgz", + "integrity": "sha512-fbUelkM1apvqez/yYx1/oICVnGo2KM5s63mhGylrmXUxK/IAXSIf87QIxVfZldWf4QsOafY6vV3bX8aMHSvNrA==", "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-flow-strip-types": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.12.1.tgz", - "integrity": "sha512-8hAtkmsQb36yMmEtk2JZ9JnVyDSnDOdlB+0nEGzIDLuK4yR3JcEjfuFPYkdEPSh8Id+rAMeBEn+X0iVEyho6Hg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.13.0.tgz", + "integrity": "sha512-EXAGFMJgSX8gxWD7PZtW/P6M+z74jpx3wm/+9pn+c2dOawPpBkUX7BrfyPvo6ZpXbgRIEuwgwDb/MGlKvu2pOg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-flow": "^7.12.1" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-flow": "^7.12.13" } }, "@babel/plugin-transform-for-of": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.12.1.tgz", - "integrity": "sha512-Zaeq10naAsuHo7heQvyV0ptj4dlZJwZgNAtBYBnu5nNKJoW62m0zKcIEyVECrUKErkUkg6ajMy4ZfnVZciSBhg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.13.0.tgz", + "integrity": "sha512-IHKT00mwUVYE0zzbkDgNRP6SRzvfGCYsOxIRz8KsiaaHCcT9BWIkO+H9QRJseHBLOGBZkHUdHiqj6r0POsdytg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-function-name": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.1.tgz", - "integrity": "sha512-JF3UgJUILoFrFMEnOJLJkRHSk6LUSXLmEFsA23aR2O5CSLUxbeUX1IZ1YQ7Sn0aXb601Ncwjx73a+FVqgcljVw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.12.13.tgz", + "integrity": "sha512-6K7gZycG0cmIwwF7uMK/ZqeCikCGVBdyP2J5SKNCXO5EOHcqi+z7Jwf8AmyDNcBgxET8DrEtCt/mPKPyAzXyqQ==", "requires": { - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.1.tgz", - "integrity": "sha512-+PxVGA+2Ag6uGgL0A5f+9rklOnnMccwEBzwYFL3EUaKuiyVnUipyXncFcfjSkbimLrODoqki1U9XxZzTvfN7IQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.12.13.tgz", + "integrity": "sha512-FW+WPjSR7hiUxMcKqyNjP05tQ2kmBCdpEpZHY1ARm96tGQCCBvXKnpjILtDplUnJ/eHZ0lALLM+d2lMFSpYJrQ==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-member-expression-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.1.tgz", - "integrity": "sha512-1sxePl6z9ad0gFMB9KqmYofk34flq62aqMt9NqliS/7hPEpURUCMbyHXrMPlo282iY7nAvUB1aQd5mg79UD9Jg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.12.13.tgz", + "integrity": "sha512-kxLkOsg8yir4YeEPHLuO2tXP9R/gTjpuTOjshqSpELUN3ZAg2jfDnKUvzzJxObun38sw3wm4Uu69sX/zA7iRvg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-modules-amd": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.12.1.tgz", - "integrity": "sha512-tDW8hMkzad5oDtzsB70HIQQRBiTKrhfgwC/KkJeGsaNFTdWhKNt/BiE8c5yj19XiGyrxpbkOfH87qkNg1YGlOQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.13.0.tgz", + "integrity": "sha512-EKy/E2NHhY/6Vw5d1k3rgoobftcNUmp9fGjb9XZwQLtTctsRBOTRO7RHHxfIky1ogMN5BxN7p9uMA3SzPfotMQ==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - } } }, "@babel/plugin-transform-modules-commonjs": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.12.1.tgz", - "integrity": "sha512-dY789wq6l0uLY8py9c1B48V8mVL5gZh/+PQ5ZPrylPYsnAvnEMjqsUXkuoDVPeVK+0VyGar+D08107LzDQ6pag==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.13.8.tgz", + "integrity": "sha512-9QiOx4MEGglfYZ4XOnU79OHr6vIWUakIj9b4mioN8eQIoEh+pf5p/zEB36JpDFWA12nNMiRf7bfoRvl9Rn79Bw==", "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-simple-access": "^7.12.1", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-simple-access": "^7.12.13", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "requires": { - "object.assign": "^4.1.0" - } - } } }, "@babel/plugin-transform-modules-systemjs": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.12.1.tgz", - "integrity": "sha512-Hn7cVvOavVh8yvW6fLwveFqSnd7rbQN3zJvoPNyNaQSvgfKmDBO9U1YL9+PCXGRlZD9tNdWTy5ACKqMuzyn32Q==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.13.8.tgz", + "integrity": "sha512-hwqctPYjhM6cWvVIlOIe27jCIBgHCsdH2xCJVAYQm7V5yTMoilbVMi9f6wKg0rpQAOn6ZG4AOyvCqFF/hUh6+A==", "dev": true, "requires": { - "@babel/helper-hoist-variables": "^7.10.4", - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-hoist-variables": "^7.13.0", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-identifier": "^7.12.11", "babel-plugin-dynamic-import-node": "^2.3.3" - }, - "dependencies": { - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - } } }, "@babel/plugin-transform-modules-umd": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.12.1.tgz", - "integrity": "sha512-aEIubCS0KHKM0zUos5fIoQm+AZUMt1ZvMpqz0/H5qAQ7vWylr9+PLYurT+Ic7ID/bKLd4q8hDovaG3Zch2uz5Q==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.13.0.tgz", + "integrity": "sha512-D/ILzAh6uyvkWjKKyFE/W0FzWwasv6vPTSqPcjxFqn6QpX3u8DjRVliq4F2BamO2Wee/om06Vyy+vPkNrd4wxw==", "dev": true, "requires": { - "@babel/helper-module-transforms": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.1.tgz", - "integrity": "sha512-tB43uQ62RHcoDp9v2Nsf+dSM8sbNodbEicbQNA53zHz8pWUhsgHSJCGpt7daXxRydjb0KnfmB+ChXOv3oADp1Q==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.12.13.tgz", + "integrity": "sha512-Xsm8P2hr5hAxyYblrfACXpQKdQbx4m2df9/ZZSQ8MAhsadw06+jW7s9zsSw6he+mJZXRlVMyEnVktJo4zjk1WA==", "dev": true, "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1" + "@babel/helper-create-regexp-features-plugin": "^7.12.13" } }, "@babel/plugin-transform-new-target": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.1.tgz", - "integrity": "sha512-+eW/VLcUL5L9IvJH7rT1sT0CzkdUTvPrXC2PXTn/7z7tXLBuKvezYbGdxD5WMRoyvyaujOq2fWoKl869heKjhw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.12.13.tgz", + "integrity": "sha512-/KY2hbLxrG5GTQ9zzZSc3xWiOy379pIETEhbtzwZcw9rvuaVV4Fqy7BYGYOWZnaoXIQYbbJ0ziXLa/sKcGCYEQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-object-assign": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.12.1.tgz", - "integrity": "sha512-geUHn4XwHznRAFiuROTy0Hr7bKbpijJCmr1Svt/VNGhpxmp0OrdxURNpWbOAf94nUbL+xj6gbxRVPHWIbRpRoA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-assign/-/plugin-transform-object-assign-7.12.13.tgz", + "integrity": "sha512-4QxDMc0lAOkIBSfCrnSGbAJ+4epDBF2XXwcLXuBcG1xl9u7LrktNVD4+LwhL47XuKVPQ7R25e/WdcV+h97HyZA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-object-super": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.1.tgz", - "integrity": "sha512-AvypiGJH9hsquNUn+RXVcBdeE3KHPZexWRdimhuV59cSoOt5kFBmqlByorAeUlGG2CJWd0U+4ZtNKga/TB0cAw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.12.13.tgz", + "integrity": "sha512-JzYIcj3XtYspZDV8j9ulnoMPZZnF/Cj0LUxPOjR89BdBVx+zYJI9MdMIlUZjbXDX+6YVeS6I3e8op+qQ3BYBoQ==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-replace-supers": "^7.12.1" + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/helper-replace-supers": "^7.12.13" } }, "@babel/plugin-transform-parameters": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.12.1.tgz", - "integrity": "sha512-xq9C5EQhdPK23ZeCdMxl8bbRnAgHFrw5EOC3KJUsSylZqdkCaFEXxGSBuTSObOpiiHHNyb82es8M1QYgfQGfNg==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.13.0.tgz", + "integrity": "sha512-Jt8k/h/mIwE2JFEOb3lURoY5C85ETcYPnbuAJ96zRBzh1XHtQZfs62ChZ6EP22QlC8c7Xqr9q+e1SU5qttwwjw==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-property-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.1.tgz", - "integrity": "sha512-6MTCR/mZ1MQS+AwZLplX4cEySjCpnIF26ToWo942nqn8hXSm7McaHQNeGx/pt7suI1TWOWMfa/NgBhiqSnX0cQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.12.13.tgz", + "integrity": "sha512-nqVigwVan+lR+g8Fj8Exl0UQX2kymtjcWfMOYM1vTYEKujeyv2SkMgazf2qNcK7l4SDiKyTA/nHCPqL4e2zo1A==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-react-constant-elements": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.12.1.tgz", - "integrity": "sha512-KOHd0tIRLoER+J+8f9DblZDa1fLGPwaaN1DI1TVHuQFOpjHV22C3CUB3obeC4fexHY9nx+fH0hQNvLFFfA1mxA==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.13.10.tgz", + "integrity": "sha512-E+aCW9j7mLq01tOuGV08YzLBt+vSyr4bOPT75B6WrAlrUfmOYOZ/yWk847EH0dv0xXiCihWLEmlX//O30YhpIw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-react-display-name": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.1.tgz", - "integrity": "sha512-cAzB+UzBIrekfYxyLlFqf/OagTvHLcVBb5vpouzkYkBclRPraiygVnafvAoipErZLI8ANv8Ecn6E/m5qPXD26w==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.12.13.tgz", + "integrity": "sha512-MprESJzI9O5VnJZrL7gg1MpdqmiFcUv41Jc7SahxYsNP2kDkFqClxxTZq+1Qv4AFCamm+GXMRDQINNn+qrxmiA==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-react-jsx": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.7.tgz", - "integrity": "sha512-YFlTi6MEsclFAPIDNZYiCRbneg1MFGao9pPG9uD5htwE0vDbPaMUMeYd6itWjw7K4kro4UbdQf3ljmFl9y48dQ==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.12.17.tgz", + "integrity": "sha512-mwaVNcXV+l6qJOuRhpdTEj8sT/Z0owAVWf9QujTZ0d2ye9X/K+MTOTSizcgKOj18PGnTc/7g1I4+cIUjsKhBcw==", "requires": { - "@babel/helper-builder-react-jsx": "^7.10.4", - "@babel/helper-builder-react-jsx-experimental": "^7.12.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-jsx": "^7.12.1" + "@babel/helper-annotate-as-pure": "^7.12.13", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-syntax-jsx": "^7.12.13", + "@babel/types": "^7.12.17" } }, "@babel/plugin-transform-react-jsx-development": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.7.tgz", - "integrity": "sha512-Rs3ETtMtR3VLXFeYRChle5SsP/P9Jp/6dsewBQfokDSzKJThlsuFcnzLTDRALiUmTC48ej19YD9uN1mupEeEDg==", - "dev": true, - "requires": { - "@babel/helper-builder-react-jsx-experimental": "^7.12.4", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-jsx": "^7.12.1" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.12.1.tgz", - "integrity": "sha512-FbpL0ieNWiiBB5tCldX17EtXgmzeEZjFrix72rQYeq9X6nUK38HCaxexzVQrZWXanxKJPKVVIU37gFjEQYkPkA==", + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.12.17.tgz", + "integrity": "sha512-BPjYV86SVuOaudFhsJR1zjgxxOhJDt6JHNoD48DxWEIxUCAMjV1ys6DYw4SDYZh0b1QsS2vfIA9t/ZsQGsDOUQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/plugin-transform-react-jsx": "^7.12.17" } }, "@babel/plugin-transform-react-jsx-source": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.1.tgz", - "integrity": "sha512-keQ5kBfjJNRc6zZN1/nVHCd6LLIHq4aUKcVnvE/2l+ZZROSbqoiGFRtT5t3Is89XJxBQaP7NLZX2jgGHdZvvFQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.12.13.tgz", + "integrity": "sha512-O5JJi6fyfih0WfDgIJXksSPhGP/G0fQpfxYy87sDc+1sFmsCS6wr3aAn+whbzkhbjtq4VMqLRaSzR6IsshIC0Q==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-react-pure-annotations": { @@ -1060,218 +1029,204 @@ } }, "@babel/plugin-transform-regenerator": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.1.tgz", - "integrity": "sha512-gYrHqs5itw6i4PflFX3OdBPMQdPbF4bj2REIUxlMRUFk0/ZOAIpDFuViuxPjUL7YC8UPnf+XG7/utJvqXdPKng==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.12.13.tgz", + "integrity": "sha512-lxb2ZAvSLyJ2PEe47hoGWPmW22v7CtSl9jW8mingV4H2sEX/JOcrAj2nPuGWi56ERUm2bUpjKzONAuT6HCn2EA==", "requires": { "regenerator-transform": "^0.14.2" } }, "@babel/plugin-transform-reserved-words": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.1.tgz", - "integrity": "sha512-pOnUfhyPKvZpVyBHhSBoX8vfA09b7r00Pmm1sH+29ae2hMTKVmSp4Ztsr8KBKjLjx17H0eJqaRC3bR2iThM54A==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.12.13.tgz", + "integrity": "sha512-xhUPzDXxZN1QfiOy/I5tyye+TRz6lA7z6xaT4CLOjPRMVg1ldRf0LHw0TDBpYL4vG78556WuHdyO9oi5UmzZBg==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-runtime": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.12.1.tgz", - "integrity": "sha512-Ac/H6G9FEIkS2tXsZjL4RAdS3L3WHxci0usAnz7laPWUmFiGtj7tIASChqKZMHTSQTQY6xDbOq+V1/vIq3QrWg==", - "requires": { - "@babel/helper-module-imports": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "resolve": "^1.8.1", - "semver": "^5.5.1" + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.13.10.tgz", + "integrity": "sha512-Y5k8ipgfvz5d/76tx7JYbKQTcgFSU6VgJ3kKQv4zGTKr+a9T/KBvfRvGtSFgKDQGt/DBykQixV0vNWKIdzWErA==", + "requires": { + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-plugin-utils": "^7.13.0", + "babel-plugin-polyfill-corejs2": "^0.1.4", + "babel-plugin-polyfill-corejs3": "^0.1.3", + "babel-plugin-polyfill-regenerator": "^0.1.2", + "semver": "^6.3.0" }, "dependencies": { "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, "@babel/plugin-transform-shorthand-properties": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.1.tgz", - "integrity": "sha512-GFZS3c/MhX1OusqB1MZ1ct2xRzX5ppQh2JU1h2Pnfk88HtFTM+TWQqJNfwkmxtPQtb/s1tk87oENfXJlx7rSDw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.12.13.tgz", + "integrity": "sha512-xpL49pqPnLtf0tVluuqvzWIgLEhuPpZzvs2yabUHSKRNlN7ScYU7aMlmavOeyXJZKgZKQRBlh8rHbKiJDraTSw==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-spread": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.12.1.tgz", - "integrity": "sha512-vuLp8CP0BE18zVYjsEBZ5xoCecMK6LBMMxYzJnh01rxQRvhNhH1csMMmBfNo5tGpGO+NhdSNW2mzIvBu3K1fng==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.13.0.tgz", + "integrity": "sha512-V6vkiXijjzYeFmQTr3dBxPtZYLPcUfY34DebOU27jIl2M/Y8Egm52Hw82CSjjPqd54GTlJs5x+CR7HeNr24ckg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4", + "@babel/helper-plugin-utils": "^7.13.0", "@babel/helper-skip-transparent-expression-wrappers": "^7.12.1" } }, "@babel/plugin-transform-sticky-regex": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.7.tgz", - "integrity": "sha512-VEiqZL5N/QvDbdjfYQBhruN0HYjSPjC4XkeqW4ny/jNtH9gcbgaqBIXYEZCNnESMAGs0/K/R7oFGMhOyu/eIxg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.12.13.tgz", + "integrity": "sha512-Jc3JSaaWT8+fr7GRvQP02fKDsYk4K/lYwWq38r/UGfaxo89ajud321NH28KRQ7xy1Ybc0VUE5Pz8psjNNDUglg==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-template-literals": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.12.1.tgz", - "integrity": "sha512-b4Zx3KHi+taXB1dVRBhVJtEPi9h1THCeKmae2qP0YdUHIFhVjtpqqNfxeVAa1xeHVhAy4SbHxEwx5cltAu5apw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.13.0.tgz", + "integrity": "sha512-d67umW6nlfmr1iehCcBv69eSUSySk1EsIS8aTDX4Xo9qajAh6mYtcl4kJrBkGXuxZPEgVr7RVfAvNW6YQkd4Mw==", "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.13.0" } }, "@babel/plugin-transform-typeof-symbol": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.1.tgz", - "integrity": "sha512-EPGgpGy+O5Kg5pJFNDKuxt9RdmTgj5sgrus2XVeMp/ZIbOESadgILUbm50SNpghOh3/6yrbsH+NB5+WJTmsA7Q==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.12.13.tgz", + "integrity": "sha512-eKv/LmUJpMnu4npgfvs3LiHhJua5fo/CysENxa45YCQXZwKnGCQKAg87bvoqSW1fFT+HA32l03Qxsm8ouTY3ZQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-typescript": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.12.1.tgz", - "integrity": "sha512-VrsBByqAIntM+EYMqSm59SiMEf7qkmI9dqMt6RbD/wlwueWmYcI0FFK5Fj47pP6DRZm+3teXjosKlwcZJ5lIMw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.13.0.tgz", + "integrity": "sha512-elQEwluzaU8R8dbVuW2Q2Y8Nznf7hnjM7+DSCd14Lo5fF63C9qNLbwZYbmZrtV9/ySpSUpkRpQXvJb6xyu4hCQ==", "requires": { - "@babel/helper-create-class-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-syntax-typescript": "^7.12.1" + "@babel/helper-create-class-features-plugin": "^7.13.0", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/plugin-syntax-typescript": "^7.12.13" } }, "@babel/plugin-transform-unicode-escapes": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.1.tgz", - "integrity": "sha512-I8gNHJLIc7GdApm7wkVnStWssPNbSRMPtgHdmH3sRM1zopz09UWPS4x5V4n1yz/MIWTVnJ9sp6IkuXdWM4w+2Q==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.12.13.tgz", + "integrity": "sha512-0bHEkdwJ/sN/ikBHfSmOXPypN/beiGqjo+o4/5K+vxEFNPRPdImhviPakMKG4x96l85emoa0Z6cDflsdBusZbw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/plugin-transform-unicode-regex": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.1.tgz", - "integrity": "sha512-SqH4ClNngh/zGwHZOOQMTD+e8FGWexILV+ePMyiDJttAWRh5dhDL8rcl5lSgU3Huiq6Zn6pWTMvdPAb21Dwdyg==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.12.13.tgz", + "integrity": "sha512-mDRzSNY7/zopwisPZ5kM9XKCfhchqIYwAKRERtEnhYscZB79VRekuRSoYbN0+KVe3y8+q1h6A4svXtP7N+UoCA==", "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.12.1", - "@babel/helper-plugin-utils": "^7.10.4" + "@babel/helper-create-regexp-features-plugin": "^7.12.13", + "@babel/helper-plugin-utils": "^7.12.13" } }, "@babel/preset-env": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.12.7.tgz", - "integrity": "sha512-OnNdfAr1FUQg7ksb7bmbKoby4qFOHw6DKWWUNB9KqnnCldxhxJlP+21dpyaWFmf2h0rTbOkXJtAGevY3XW1eew==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.12.7", - "@babel/helper-compilation-targets": "^7.12.5", - "@babel/helper-module-imports": "^7.12.5", - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", - "@babel/plugin-proposal-async-generator-functions": "^7.12.1", - "@babel/plugin-proposal-class-properties": "^7.12.1", - "@babel/plugin-proposal-dynamic-import": "^7.12.1", - "@babel/plugin-proposal-export-namespace-from": "^7.12.1", - "@babel/plugin-proposal-json-strings": "^7.12.1", - "@babel/plugin-proposal-logical-assignment-operators": "^7.12.1", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.12.1", - "@babel/plugin-proposal-numeric-separator": "^7.12.7", - "@babel/plugin-proposal-object-rest-spread": "^7.12.1", - "@babel/plugin-proposal-optional-catch-binding": "^7.12.1", - "@babel/plugin-proposal-optional-chaining": "^7.12.7", - "@babel/plugin-proposal-private-methods": "^7.12.1", - "@babel/plugin-proposal-unicode-property-regex": "^7.12.1", - "@babel/plugin-syntax-async-generators": "^7.8.0", - "@babel/plugin-syntax-class-properties": "^7.12.1", - "@babel/plugin-syntax-dynamic-import": "^7.8.0", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.13.10.tgz", + "integrity": "sha512-nOsTScuoRghRtUsRr/c69d042ysfPHcu+KOB4A9aAO9eJYqrkat+LF8G1yp1HD18QiwixT2CisZTr/0b3YZPXQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.8", + "@babel/helper-compilation-targets": "^7.13.10", + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-proposal-async-generator-functions": "^7.13.8", + "@babel/plugin-proposal-class-properties": "^7.13.0", + "@babel/plugin-proposal-dynamic-import": "^7.13.8", + "@babel/plugin-proposal-export-namespace-from": "^7.12.13", + "@babel/plugin-proposal-json-strings": "^7.13.8", + "@babel/plugin-proposal-logical-assignment-operators": "^7.13.8", + "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", + "@babel/plugin-proposal-numeric-separator": "^7.12.13", + "@babel/plugin-proposal-object-rest-spread": "^7.13.8", + "@babel/plugin-proposal-optional-catch-binding": "^7.13.8", + "@babel/plugin-proposal-optional-chaining": "^7.13.8", + "@babel/plugin-proposal-private-methods": "^7.13.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.12.13", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.0", + "@babel/plugin-syntax-json-strings": "^7.8.3", "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.0", - "@babel/plugin-syntax-optional-chaining": "^7.8.0", - "@babel/plugin-syntax-top-level-await": "^7.12.1", - "@babel/plugin-transform-arrow-functions": "^7.12.1", - "@babel/plugin-transform-async-to-generator": "^7.12.1", - "@babel/plugin-transform-block-scoped-functions": "^7.12.1", - "@babel/plugin-transform-block-scoping": "^7.12.1", - "@babel/plugin-transform-classes": "^7.12.1", - "@babel/plugin-transform-computed-properties": "^7.12.1", - "@babel/plugin-transform-destructuring": "^7.12.1", - "@babel/plugin-transform-dotall-regex": "^7.12.1", - "@babel/plugin-transform-duplicate-keys": "^7.12.1", - "@babel/plugin-transform-exponentiation-operator": "^7.12.1", - "@babel/plugin-transform-for-of": "^7.12.1", - "@babel/plugin-transform-function-name": "^7.12.1", - "@babel/plugin-transform-literals": "^7.12.1", - "@babel/plugin-transform-member-expression-literals": "^7.12.1", - "@babel/plugin-transform-modules-amd": "^7.12.1", - "@babel/plugin-transform-modules-commonjs": "^7.12.1", - "@babel/plugin-transform-modules-systemjs": "^7.12.1", - "@babel/plugin-transform-modules-umd": "^7.12.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.1", - "@babel/plugin-transform-new-target": "^7.12.1", - "@babel/plugin-transform-object-super": "^7.12.1", - "@babel/plugin-transform-parameters": "^7.12.1", - "@babel/plugin-transform-property-literals": "^7.12.1", - "@babel/plugin-transform-regenerator": "^7.12.1", - "@babel/plugin-transform-reserved-words": "^7.12.1", - "@babel/plugin-transform-shorthand-properties": "^7.12.1", - "@babel/plugin-transform-spread": "^7.12.1", - "@babel/plugin-transform-sticky-regex": "^7.12.7", - "@babel/plugin-transform-template-literals": "^7.12.1", - "@babel/plugin-transform-typeof-symbol": "^7.12.1", - "@babel/plugin-transform-unicode-escapes": "^7.12.1", - "@babel/plugin-transform-unicode-regex": "^7.12.1", - "@babel/preset-modules": "^0.1.3", - "@babel/types": "^7.12.7", - "core-js-compat": "^3.7.0", - "semver": "^5.5.0" + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.12.13", + "@babel/plugin-transform-arrow-functions": "^7.13.0", + "@babel/plugin-transform-async-to-generator": "^7.13.0", + "@babel/plugin-transform-block-scoped-functions": "^7.12.13", + "@babel/plugin-transform-block-scoping": "^7.12.13", + "@babel/plugin-transform-classes": "^7.13.0", + "@babel/plugin-transform-computed-properties": "^7.13.0", + "@babel/plugin-transform-destructuring": "^7.13.0", + "@babel/plugin-transform-dotall-regex": "^7.12.13", + "@babel/plugin-transform-duplicate-keys": "^7.12.13", + "@babel/plugin-transform-exponentiation-operator": "^7.12.13", + "@babel/plugin-transform-for-of": "^7.13.0", + "@babel/plugin-transform-function-name": "^7.12.13", + "@babel/plugin-transform-literals": "^7.12.13", + "@babel/plugin-transform-member-expression-literals": "^7.12.13", + "@babel/plugin-transform-modules-amd": "^7.13.0", + "@babel/plugin-transform-modules-commonjs": "^7.13.8", + "@babel/plugin-transform-modules-systemjs": "^7.13.8", + "@babel/plugin-transform-modules-umd": "^7.13.0", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.12.13", + "@babel/plugin-transform-new-target": "^7.12.13", + "@babel/plugin-transform-object-super": "^7.12.13", + "@babel/plugin-transform-parameters": "^7.13.0", + "@babel/plugin-transform-property-literals": "^7.12.13", + "@babel/plugin-transform-regenerator": "^7.12.13", + "@babel/plugin-transform-reserved-words": "^7.12.13", + "@babel/plugin-transform-shorthand-properties": "^7.12.13", + "@babel/plugin-transform-spread": "^7.13.0", + "@babel/plugin-transform-sticky-regex": "^7.12.13", + "@babel/plugin-transform-template-literals": "^7.13.0", + "@babel/plugin-transform-typeof-symbol": "^7.12.13", + "@babel/plugin-transform-unicode-escapes": "^7.12.13", + "@babel/plugin-transform-unicode-regex": "^7.12.13", + "@babel/preset-modules": "^0.1.4", + "@babel/types": "^7.13.0", + "babel-plugin-polyfill-corejs2": "^0.1.4", + "babel-plugin-polyfill-corejs3": "^0.1.3", + "babel-plugin-polyfill-regenerator": "^0.1.2", + "core-js-compat": "^3.9.0", + "semver": "^6.3.0" }, "dependencies": { - "core-js-compat": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.8.1.tgz", - "integrity": "sha512-a16TLmy9NVD1rkjUGbwuyWkiDoN0FDpAwrfLONvHFQx0D9k7J9y0srwMT8QP/Z6HE3MIFaVynEeYwZwPX1o5RQ==", - "dev": true, - "requires": { - "browserslist": "^4.15.0", - "semver": "7.0.0" - }, - "dependencies": { - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } - } - }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } }, "@babel/preset-flow": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.12.1.tgz", - "integrity": "sha512-UAoyMdioAhM6H99qPoKvpHMzxmNVXno8GYU/7vZmGaHk6/KqfDYL1W0NxszVbJ2EP271b7e6Ox+Vk2A9QsB3Sw==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.12.13.tgz", + "integrity": "sha512-gcEjiwcGHa3bo9idURBp5fmJPcyFPOszPQjztXrOjUE2wWVqc6fIVJPgWPIQksaQ5XZ2HWiRsf2s1fRGVjUtVw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-transform-flow-strip-types": "^7.12.1" + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-transform-flow-strip-types": "^7.12.13" } }, "@babel/preset-modules": { @@ -1288,35 +1243,33 @@ } }, "@babel/preset-react": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.7.tgz", - "integrity": "sha512-wKeTdnGUP5AEYCYQIMeXMMwU7j+2opxrG0WzuZfxuuW9nhKvvALBjl67653CWamZJVefuJGI219G591RSldrqQ==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.12.13.tgz", + "integrity": "sha512-TYM0V9z6Abb6dj1K7i5NrEhA13oS5ujUYQYDfqIBXYHOc2c2VkFgc+q9kyssIyUfy4/hEwqrgSlJ/Qgv8zJLsA==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/plugin-transform-react-display-name": "^7.12.1", - "@babel/plugin-transform-react-jsx": "^7.12.7", - "@babel/plugin-transform-react-jsx-development": "^7.12.7", - "@babel/plugin-transform-react-jsx-self": "^7.12.1", - "@babel/plugin-transform-react-jsx-source": "^7.12.1", + "@babel/helper-plugin-utils": "^7.12.13", + "@babel/plugin-transform-react-display-name": "^7.12.13", + "@babel/plugin-transform-react-jsx": "^7.12.13", + "@babel/plugin-transform-react-jsx-development": "^7.12.12", "@babel/plugin-transform-react-pure-annotations": "^7.12.1" } }, "@babel/preset-typescript": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.12.7.tgz", - "integrity": "sha512-nOoIqIqBmHBSEgBXWR4Dv/XBehtIFcw9PqZw6rFYuKrzsZmOQm3PR5siLBnKZFEsDb03IegG8nSjU/iXXXYRmw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.13.0.tgz", + "integrity": "sha512-LXJwxrHy0N3f6gIJlYbLta1D9BDtHpQeqwzM0LIfjDlr6UE/D5Mc7W4iDiQzaE+ks0sTjT26ArcHWnJVt0QiHw==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.10.4", - "@babel/helper-validator-option": "^7.12.1", - "@babel/plugin-transform-typescript": "^7.12.1" + "@babel/helper-plugin-utils": "^7.13.0", + "@babel/helper-validator-option": "^7.12.17", + "@babel/plugin-transform-typescript": "^7.13.0" } }, "@babel/register": { - "version": "7.12.1", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.12.1.tgz", - "integrity": "sha512-XWcmseMIncOjoydKZnWvWi0/5CUCD+ZYKhRwgYlWOrA8fGZ/FjuLRpqtIhLOVD/fvR1b9DQHtZPn68VvhpYf+Q==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.13.8.tgz", + "integrity": "sha512-yCVtABcmvQjRsX2elcZFUV5Q5kDDpHdtXKKku22hNDma60lYuhKmtp1ykZ/okRCPLT2bR5S+cA1kvtBdAFlDTQ==", "requires": { "find-cache-dir": "^2.0.0", "lodash": "^4.17.19", @@ -1403,9 +1356,9 @@ } }, "@babel/runtime": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz", - "integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.13.10.tgz", + "integrity": "sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==", "requires": { "regenerator-runtime": "^0.13.4" }, @@ -1418,9 +1371,9 @@ } }, "@babel/runtime-corejs3": { - "version": "7.12.5", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.12.5.tgz", - "integrity": "sha512-roGr54CsTmNPPzZoCP1AmDXuBoNao7tnSA83TXTwt+UK5QVyh1DIJnrgYRPWKCF2flqZQXwa7Yr8v7VmLzF0YQ==", + "version": "7.13.10", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.13.10.tgz", + "integrity": "sha512-x/XYVQ1h684pp1mJwOV4CyvqZXqbc8CMsMGUnAbuc82ZCdv1U63w5RSUzgDSXQHG5Rps/kiksH6g2D5BuaKyXg==", "dev": true, "requires": { "core-js-pure": "^3.0.0", @@ -1436,31 +1389,49 @@ } }, "@babel/template": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.7.tgz", - "integrity": "sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7" + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "requires": { + "@babel/highlight": "^7.12.13" + } + } } }, "@babel/traverse": { - "version": "7.12.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.12.9.tgz", - "integrity": "sha512-iX9ajqnLdoU1s1nHt36JDI9KG4k+vmI8WgjK5d+aDTwQbL2fUnzedNedssA645Ede3PM2ma1n8Q4h2ohwXgMXw==", - "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.12.5", - "@babel/helper-function-name": "^7.10.4", - "@babel/helper-split-export-declaration": "^7.11.0", - "@babel/parser": "^7.12.7", - "@babel/types": "^7.12.7", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz", + "integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==", + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.13.0", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.13.0", + "@babel/types": "^7.13.0", "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.19" }, "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "requires": { + "@babel/highlight": "^7.12.13" + } + }, "debug": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", @@ -1482,11 +1453,11 @@ } }, "@babel/types": { - "version": "7.12.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.12.7.tgz", - "integrity": "sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", + "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", "requires": { - "@babel/helper-validator-identifier": "^7.10.4", + "@babel/helper-validator-identifier": "^7.12.11", "lodash": "^4.17.19", "to-fast-properties": "^2.0.0" } @@ -1696,6 +1667,11 @@ "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" }, + "@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" + }, "@emotion/utils": { "version": "0.11.2", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.2.tgz", @@ -6104,6 +6080,57 @@ "unist-util-visit": "2.0.3" }, "dependencies": { + "@babel/core": { + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", + "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.9", + "@babel/types": "^7.12.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "is-buffer": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", @@ -6116,6 +6143,21 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -6154,6 +6196,12 @@ "xtend": "^4.0.1" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "unified": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", @@ -12187,7 +12235,7 @@ "@wordpress/a11y": { "version": "file:packages/a11y", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/dom-ready": "file:packages/dom-ready", "@wordpress/i18n": "file:packages/i18n" } @@ -12195,7 +12243,7 @@ "@wordpress/annotations": { "version": "file:packages/annotations", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/data": "file:packages/data", "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", @@ -12208,7 +12256,7 @@ "@wordpress/api-fetch": { "version": "file:packages/api-fetch", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/i18n": "file:packages/i18n", "@wordpress/url": "file:packages/url" } @@ -12216,7 +12264,7 @@ "@wordpress/autop": { "version": "file:packages/autop", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/babel-plugin-import-jsx-pragma": { @@ -12235,12 +12283,12 @@ "version": "file:packages/babel-preset-default", "dev": true, "requires": { - "@babel/core": "^7.12.9", + "@babel/core": "^7.13.10", "@babel/plugin-transform-react-jsx": "^7.12.7", - "@babel/plugin-transform-runtime": "^7.12.1", - "@babel/preset-env": "^7.12.7", - "@babel/preset-typescript": "^7.12.7", - "@babel/runtime": "^7.12.5", + "@babel/plugin-transform-runtime": "^7.13.10", + "@babel/preset-env": "^7.13.10", + "@babel/preset-typescript": "^7.13.0", + "@babel/runtime": "^7.13.10", "@wordpress/babel-plugin-import-jsx-pragma": "file:packages/babel-plugin-import-jsx-pragma", "@wordpress/browserslist-config": "file:packages/browserslist-config", "@wordpress/element": "file:packages/element", @@ -12255,13 +12303,13 @@ "@wordpress/blob": { "version": "file:packages/blob", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/block-directory": { "version": "file:packages/block-directory", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", @@ -12287,7 +12335,7 @@ "@wordpress/block-editor": { "version": "file:packages/block-editor", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/blob": "file:packages/blob", "@wordpress/blocks": "file:packages/blocks", @@ -12329,7 +12377,7 @@ "@wordpress/block-library": { "version": "file:packages/block-library", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/autop": "file:packages/autop", @@ -12370,7 +12418,7 @@ "@wordpress/block-serialization-default-parser": { "version": "file:packages/block-serialization-default-parser", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/block-serialization-spec-parser": { @@ -12383,7 +12431,7 @@ "@wordpress/blocks": { "version": "file:packages/blocks", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/autop": "file:packages/autop", "@wordpress/blob": "file:packages/blob", "@wordpress/block-serialization-default-parser": "file:packages/block-serialization-default-parser", @@ -12414,7 +12462,7 @@ "@wordpress/components": { "version": "file:packages/components", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@emotion/core": "^10.1.1", "@emotion/css": "^10.0.22", "@emotion/native": "^10.0.22", @@ -12433,10 +12481,10 @@ "@wordpress/primitives": "file:packages/primitives", "@wordpress/rich-text": "file:packages/rich-text", "@wordpress/warning": "file:packages/warning", - "@wp-g2/components": "^0.0.159", - "@wp-g2/context": "^0.0.159", - "@wp-g2/styles": "^0.0.159", - "@wp-g2/utils": "^0.0.159", + "@wp-g2/components": "^0.0.160", + "@wp-g2/context": "^0.0.160", + "@wp-g2/styles": "^0.0.160", + "@wp-g2/utils": "^0.0.160", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", "downshift": "^6.0.15", @@ -12454,138 +12502,12 @@ "rememo": "^3.0.0", "tinycolor2": "^1.4.2", "uuid": "^8.3.0" - }, - "dependencies": { - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "@wp-g2/components": { - "version": "0.0.159", - "resolved": "https://registry.npmjs.org/@wp-g2/components/-/components-0.0.159.tgz", - "integrity": "sha512-FUuQpZxiq7ROfOrPcCcru3znc82r9VtVRSAACQpadRswIYr9aTaedWOCnaI0QU32rs51nh4bc2LwalZVtE7nkg==", - "requires": { - "@popperjs/core": "^2.5.4", - "@wp-g2/context": "^0.0.159", - "@wp-g2/styles": "^0.0.159", - "@wp-g2/utils": "^0.0.159", - "csstype": "^3.0.3", - "downshift": "^6.0.15", - "framer-motion": "^2.1.0", - "highlight-words-core": "^1.2.2", - "history": "^4.9.0", - "lodash": "^4.17.19", - "path-to-regexp": "^1.7.0", - "react-colorful": "4.4.4", - "react-textarea-autosize": "^8.2.0", - "react-use-gesture": "^9.0.0", - "reakit": "^1.3.4" - } - }, - "@wp-g2/context": { - "version": "0.0.159", - "resolved": "https://registry.npmjs.org/@wp-g2/context/-/context-0.0.159.tgz", - "integrity": "sha512-FyWg2u2yjUfzoNv3W8L+JQKojYuYrauNU0SidqbGaPMrk5ftSkRHisCxiYQgfeBUSPVvEIVfIDacDfHZ6QjI8A==", - "requires": { - "@emotion/hash": "^0.8.0", - "@wp-g2/styles": "^0.0.159", - "@wp-g2/utils": "^0.0.159", - "lodash": "^4.17.19" - } - }, - "@wp-g2/create-styles": { - "version": "0.0.159", - "resolved": "https://registry.npmjs.org/@wp-g2/create-styles/-/create-styles-0.0.159.tgz", - "integrity": "sha512-cdoasY3TS2LoBHK/Erw57gcHAYex2csKULLoFcvlttMBMhsLT0tQWz6aJjJDFCW1QZAjjIuk+FXByOxyZwo2cg==", - "requires": { - "@emotion/core": "^10.1.1", - "@emotion/hash": "^0.8.0", - "@emotion/is-prop-valid": "^0.8.8", - "@wp-g2/utils": "^0.0.159", - "create-emotion": "^10.0.27", - "emotion": "^10.0.27", - "emotion-theming": "^10.0.27", - "lodash": "^4.17.19", - "mitt": "^2.1.0", - "rtlcss": "^2.6.2", - "styled-griddie": "^0.1.3" - } - }, - "@wp-g2/styles": { - "version": "0.0.159", - "resolved": "https://registry.npmjs.org/@wp-g2/styles/-/styles-0.0.159.tgz", - "integrity": "sha512-ZjhAKuLwNa+l5fJgwGzu7UzsUGJcW9D1EdfORKbGBv7Jw4+6P5xk+qf4CNYN6u3EE3J5r//CTF16f6Pbj2pjKg==", - "requires": { - "@wp-g2/create-styles": "^0.0.159", - "@wp-g2/utils": "^0.0.159" - } - }, - "@wp-g2/utils": { - "version": "0.0.159", - "resolved": "https://registry.npmjs.org/@wp-g2/utils/-/utils-0.0.159.tgz", - "integrity": "sha512-0bkbB1o+4J3qShVl5STiWVN9xl2+/LtiEi/sC4yx/yeFXN31Cr/HKtGVIjil2aRkL8Dk7imbaSJoMIyH5LfvsA==", - "requires": { - "copy-to-clipboard": "^3.3.1", - "create-emotion": "^10.0.27", - "deepmerge": "^4.2.2", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2", - "json2mq": "^0.2.0", - "lodash": "^4.17.19", - "memize": "^1.1.0", - "react-merge-refs": "^1.1.0", - "react-resize-aware": "^3.1.0", - "tinycolor2": "^1.4.2", - "use-enhanced-state": "^0.0.13", - "use-isomorphic-layout-effect": "^1.0.0" - } - }, - "csstype": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", - "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "requires": { - "isarray": "0.0.1" - } - } } }, "@wordpress/compose": { "version": "file:packages/compose", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", @@ -12603,7 +12525,7 @@ "@wordpress/core-data": { "version": "file:packages/core-data", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/blocks": "file:packages/blocks", "@wordpress/data": "file:packages/data", @@ -12653,18 +12575,43 @@ "version": "file:packages/customize-widgets", "requires": { "@babel/runtime": "^7.11.2", + "@wordpress/a11y": "file:packages/a11y", "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/block-library": "file:packages/block-library", "@wordpress/blocks": "file:packages/blocks", "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", + "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/icons": "file:packages/icons", + "classnames": "^2.2.6", "lodash": "^4.17.19" + }, + "dependencies": { + "classnames": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", + "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==" + }, + "reakit": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.3.6.tgz", + "integrity": "sha512-/I7XNPEUuRjCVwDkriy7HKuSKTtCmojZrrClGIg1gexB7Ij5vjpEbyUZzwHMmvIEoMqUZDC0Q6cDWfLpqaY9vA==", + "requires": { + "@popperjs/core": "^2.5.4", + "body-scroll-lock": "^3.1.5", + "reakit-system": "^0.15.1", + "reakit-utils": "^0.15.1", + "reakit-warning": "^0.6.1" + } + } } }, "@wordpress/data": { "version": "file:packages/data", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:packages/compose", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", @@ -12683,7 +12630,7 @@ "@wordpress/data-controls": { "version": "file:packages/data-controls", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/data": "file:packages/data", "@wordpress/deprecated": "file:packages/deprecated" @@ -12692,7 +12639,7 @@ "@wordpress/date": { "version": "file:packages/date", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "moment": "^2.22.1", "moment-timezone": "^0.5.31" } @@ -12708,7 +12655,7 @@ "@wordpress/deprecated": { "version": "file:packages/deprecated", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/hooks": "file:packages/hooks" } }, @@ -12716,7 +12663,7 @@ "version": "file:packages/docgen", "dev": true, "requires": { - "@babel/core": "^7.12.9", + "@babel/core": "^7.13.10", "comment-parser": "^1.1.1", "lodash": "^4.17.19", "mdast-util-inject": "1.1.0", @@ -12737,21 +12684,21 @@ "@wordpress/dom": { "version": "file:packages/dom", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19" } }, "@wordpress/dom-ready": { "version": "file:packages/dom-ready", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/e2e-test-utils": { "version": "file:packages/e2e-test-utils", "dev": true, "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/url": "file:packages/url", "lodash": "^4.17.19", @@ -12776,7 +12723,7 @@ "@wordpress/edit-navigation": { "version": "file:packages/edit-navigation", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/block-library": "file:packages/block-library", @@ -12792,6 +12739,7 @@ "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", "@wordpress/icons": "file:packages/icons", + "@wordpress/interface": "file:packages/interface", "@wordpress/keyboard-shortcuts": "file:packages/keyboard-shortcuts", "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", @@ -12805,7 +12753,7 @@ "@wordpress/edit-post": { "version": "file:packages/edit-post", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", @@ -12840,7 +12788,7 @@ "@wordpress/edit-site": { "version": "file:packages/edit-site", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", @@ -12875,7 +12823,7 @@ "@wordpress/edit-widgets": { "version": "file:packages/edit-widgets", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/block-library": "file:packages/block-library", @@ -12907,7 +12855,7 @@ "@wordpress/editor": { "version": "file:packages/editor", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/autop": "file:packages/autop", "@wordpress/blob": "file:packages/blob", @@ -12945,7 +12893,7 @@ "@wordpress/element": { "version": "file:packages/element", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@wordpress/escape-html": "file:packages/escape-html", @@ -13003,7 +12951,7 @@ "@wordpress/escape-html": { "version": "file:packages/escape-html", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/eslint-plugin": { @@ -13031,7 +12979,7 @@ "@wordpress/format-library": { "version": "file:packages/format-library", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/block-editor": "file:packages/block-editor", "@wordpress/components": "file:packages/components", @@ -13051,19 +12999,19 @@ "@wordpress/hooks": { "version": "file:packages/hooks", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/html-entities": { "version": "file:packages/html-entities", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/i18n": { "version": "file:packages/i18n", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/hooks": "file:packages/hooks", "gettext-parser": "^1.3.1", "lodash": "^4.17.19", @@ -13075,7 +13023,7 @@ "@wordpress/icons": { "version": "file:packages/icons", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:packages/element", "@wordpress/primitives": "file:packages/primitives" } @@ -13083,7 +13031,7 @@ "@wordpress/interface": { "version": "file:packages/interface", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/components": "file:packages/components", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", @@ -13100,14 +13048,14 @@ "@wordpress/is-shallow-equal": { "version": "file:packages/is-shallow-equal", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/jest-console": { "version": "file:packages/jest-console", "dev": true, "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "jest-matcher-utils": "^26.6.2", "lodash": "^4.17.19" } @@ -13128,13 +13076,13 @@ "dev": true, "requires": { "@axe-core/puppeteer": "^4.0.0", - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/keyboard-shortcuts": { "version": "file:packages/keyboard-shortcuts", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", "@wordpress/element": "file:packages/element", @@ -13146,7 +13094,7 @@ "@wordpress/keycodes": { "version": "file:packages/keycodes", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/i18n": "file:packages/i18n", "lodash": "^4.17.19" } @@ -13171,7 +13119,7 @@ "@wordpress/list-reusable-blocks": { "version": "file:packages/list-reusable-blocks", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/components": "file:packages/components", "@wordpress/compose": "file:packages/compose", @@ -13183,7 +13131,7 @@ "@wordpress/media-utils": { "version": "file:packages/media-utils", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/blob": "file:packages/blob", "@wordpress/element": "file:packages/element", @@ -13194,7 +13142,7 @@ "@wordpress/notices": { "version": "file:packages/notices", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:packages/a11y", "@wordpress/data": "file:packages/data", "lodash": "^4.17.19" @@ -13207,7 +13155,7 @@ "@wordpress/nux": { "version": "file:packages/nux", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/components": "file:packages/components", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", @@ -13222,7 +13170,7 @@ "@wordpress/plugins": { "version": "file:packages/plugins", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:packages/compose", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", @@ -13255,7 +13203,7 @@ "@wordpress/primitives": { "version": "file:packages/primitives", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:packages/element", "classnames": "^2.2.5" } @@ -13263,7 +13211,7 @@ "@wordpress/priority-queue": { "version": "file:packages/priority-queue", "requires": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/project-management-automation": { @@ -13272,13 +13220,13 @@ "requires": { "@actions/core": "^1.0.0", "@actions/github": "^1.0.0", - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" } }, "@wordpress/react-i18n": { "version": "file:packages/react-i18n", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:packages/element", "@wordpress/i18n": "file:packages/i18n", "utility-types": "^3.10.0" @@ -13300,7 +13248,7 @@ "@wordpress/react-native-editor": { "version": "file:packages/react-native-editor", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@react-native-community/blur": "3.6.0", "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#f65a51a3320e58404d7f38d967bfd1f42b439ca9", "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#d263ff16cdd9fb7352b354342522ff030f220f42", @@ -13358,8 +13306,7 @@ "integrity": "sha512-AH8NYyMNLNhcUEF97QbMxPNLNW+oiSBlvm1rsMNzgJ1d5TQzdh/AOJGsxeeESp3m9YIWGLCgUvGTVoVLs0p68A==" }, "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "version": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" } } @@ -13367,7 +13314,7 @@ "@wordpress/redux-routine": { "version": "file:packages/redux-routine", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "is-promise": "^4.0.0", "lodash": "^4.17.19", "rungen": "^0.3.2" @@ -13393,10 +13340,9 @@ "@wordpress/rich-text": { "version": "file:packages/rich-text", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", - "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/escape-html": "file:packages/escape-html", @@ -13422,7 +13368,7 @@ "@wordpress/prettier-config": "file:packages/prettier-config", "@wordpress/stylelint-config": "file:packages/stylelint-config", "babel-jest": "^26.6.3", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "chalk": "^4.0.0", "check-node-version": "^4.1.0", "clean-webpack-plugin": "^3.0.0", @@ -13466,10 +13412,11 @@ "@wordpress/server-side-render": { "version": "file:packages/server-side-render", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/blocks": "file:packages/blocks", "@wordpress/components": "file:packages/components", + "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/element": "file:packages/element", @@ -13481,7 +13428,7 @@ "@wordpress/shortcode": { "version": "file:packages/shortcode", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19", "memize": "^1.1.0" } @@ -13498,14 +13445,14 @@ "@wordpress/token-list": { "version": "file:packages/token-list", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19" } }, "@wordpress/url": { "version": "file:packages/url", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19", "react-native-url-polyfill": "^1.1.2" } @@ -13513,7 +13460,7 @@ "@wordpress/viewport": { "version": "file:packages/viewport", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", "lodash": "^4.17.19" @@ -13525,10 +13472,140 @@ "@wordpress/wordcount": { "version": "file:packages/wordcount", "requires": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", + "lodash": "^4.17.19" + } + }, + "@wp-g2/components": { + "version": "0.0.160", + "resolved": "https://registry.npmjs.org/@wp-g2/components/-/components-0.0.160.tgz", + "integrity": "sha512-44qUtiF5Nl/srD7Vzbpcd0im/EIej04fOdDfa0lfDxXJDNK3RRtSSEwCRhok/M5SKCmvYbZKRUx2K0ugXNqK0Q==", + "requires": { + "@popperjs/core": "^2.5.4", + "@wp-g2/context": "^0.0.160", + "@wp-g2/styles": "^0.0.160", + "@wp-g2/utils": "^0.0.160", + "csstype": "^3.0.3", + "downshift": "^6.0.15", + "framer-motion": "^2.1.0", + "highlight-words-core": "^1.2.2", + "history": "^4.9.0", + "lodash": "^4.17.19", + "path-to-regexp": "^1.7.0", + "react-colorful": "4.4.4", + "react-textarea-autosize": "^8.2.0", + "react-use-gesture": "^9.0.0", + "reakit": "^1.3.4" + }, + "dependencies": { + "csstype": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.7.tgz", + "integrity": "sha512-KxnUB0ZMlnUWCsx2Z8MUsr6qV6ja1w9ArPErJaJaF8a5SOWoHLIszeCTKGRGRgtLgYrs1E8CHkNSP1VZTTPc9g==" + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "requires": { + "isarray": "0.0.1" + } + } + } + }, + "@wp-g2/context": { + "version": "0.0.160", + "resolved": "https://registry.npmjs.org/@wp-g2/context/-/context-0.0.160.tgz", + "integrity": "sha512-50wSQCZkdZEexP88Ljutskn7/klT2Id1ks4GpzKDSBM8kadrfNdr2iabjgJdFLIH33S+r4dzEnzLs9SFtqUgwg==", + "requires": { + "@wp-g2/create-styles": "^0.0.160", + "@wp-g2/styles": "^0.0.160", + "@wp-g2/utils": "^0.0.160", "lodash": "^4.17.19" } }, + "@wp-g2/create-styles": { + "version": "0.0.160", + "resolved": "https://registry.npmjs.org/@wp-g2/create-styles/-/create-styles-0.0.160.tgz", + "integrity": "sha512-2/q8jcB9wIyfxkoCfNhz+9otRmAbDwfgk3nSEFhyz9ExR+OCqNUWqmITE3TZ4hYaSsV8E/gUUO4JjnPPy989bA==", + "requires": { + "@emotion/core": "^10.1.1", + "@emotion/hash": "^0.8.0", + "@emotion/is-prop-valid": "^0.8.8", + "@wp-g2/utils": "^0.0.160", + "create-emotion": "^10.0.27", + "emotion": "^10.0.27", + "emotion-theming": "^10.0.27", + "lodash": "^4.17.19", + "mitt": "^2.1.0", + "rtlcss": "^2.6.2", + "styled-griddie": "^0.1.3" + }, + "dependencies": { + "@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "requires": { + "@emotion/memoize": "0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" + } + } + }, + "@wp-g2/styles": { + "version": "0.0.160", + "resolved": "https://registry.npmjs.org/@wp-g2/styles/-/styles-0.0.160.tgz", + "integrity": "sha512-o91jxb0ZwEDRJrtVVjnqn3qTAXjnxZ1fX5KF3Q7oz776lMZPHsyfC0hvqnOz0w7zqaZZpdWtVQRShgrYXN6JHw==", + "requires": { + "@wp-g2/create-styles": "^0.0.160", + "@wp-g2/utils": "^0.0.160" + } + }, + "@wp-g2/utils": { + "version": "0.0.160", + "resolved": "https://registry.npmjs.org/@wp-g2/utils/-/utils-0.0.160.tgz", + "integrity": "sha512-4FhezjKyeYVb+3PZahW1kmqXpCvVvuJM97EcGqkKf+u4Qf66J3n1niHgfnRbn8aNydYK6EFze+6/UL48U35z1w==", + "requires": { + "copy-to-clipboard": "^3.3.1", + "create-emotion": "^10.0.27", + "deepmerge": "^4.2.2", + "fast-deep-equal": "^3.1.3", + "hoist-non-react-statics": "^3.3.2", + "json2mq": "^0.2.0", + "lodash": "^4.17.19", + "memize": "^1.1.0", + "react-merge-refs": "^1.1.0", + "react-resize-aware": "^3.1.0", + "tinycolor2": "^1.4.2", + "use-enhanced-state": "^0.0.13", + "use-isomorphic-layout-effect": "^1.0.0" + }, + "dependencies": { + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "requires": { + "react-is": "^16.7.0" + } + } + } + }, "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -22462,11 +22539,12 @@ "dev": true }, "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "dev": true, "requires": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } } @@ -22552,52 +22630,10 @@ "@types/istanbul-lib-report": "*" } }, - "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^26.6.2", - "babel-preset-current-node-syntax": "^1.0.0" - } - }, "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", + "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==", "dev": true }, "slash": { @@ -22609,22 +22645,27 @@ } }, "babel-loader": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", - "integrity": "sha512-7q7nC1tYOrqvUrN3LQK4GwSk/TQorZSOlO9C+RZDZpODgyN4ZlCqE5q9cDsyWOliN+aU9B4JX01xK9eJXowJLw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", + "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", "dev": true, "requires": { - "find-cache-dir": "^2.1.0", + "find-cache-dir": "^3.3.1", "loader-utils": "^1.4.0", - "mkdirp": "^0.5.3", - "pify": "^4.0.1", + "make-dir": "^3.1.0", "schema-utils": "^2.6.5" }, "dependencies": { + "@types/json-schema": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.7.tgz", + "integrity": "sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==", + "dev": true + }, "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -22633,6 +22674,12 @@ "uri-js": "^4.2.2" } }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -22646,29 +22693,30 @@ "dev": true }, "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.1.tgz", + "integrity": "sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ==", "dev": true, "requires": { "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" } }, "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, "requires": { - "locate-path": "^3.0.0" + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" } }, "json5": { @@ -22692,40 +22740,21 @@ } }, "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "p-locate": "^4.1.0" } }, "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", "dev": true, "requires": { - "minimist": "^1.2.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - } + "semver": "^6.0.0" } }, "p-limit": { @@ -22738,12 +22767,12 @@ } }, "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "requires": { - "p-limit": "^2.0.0" + "p-limit": "^2.2.0" } }, "p-try": { @@ -22752,35 +22781,36 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", "dev": true, "requires": { - "find-up": "^3.0.0" + "find-up": "^4.0.0" } }, "schema-utils": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz", - "integrity": "sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", + "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", "dev": true, "requires": { - "ajv": "^6.12.0", - "ajv-keywords": "^3.4.1" + "@types/json-schema": "^7.0.5", + "ajv": "^6.12.4", + "ajv-keywords": "^3.5.2" } }, "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true } } @@ -22799,12 +22829,28 @@ "requires": { "@babel/helper-plugin-utils": "7.10.4", "@mdx-js/util": "1.6.22" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } + } + }, + "babel-plugin-dynamic-import-node": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", + "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", + "requires": { + "object.assign": "^4.1.0" } }, "babel-plugin-emotion": { - "version": "10.0.33", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.33.tgz", - "integrity": "sha512-bxZbTTGz0AJQDHm8k6Rf3RQJ8tX2scsfsRyKVgAbiUPUNIRtlK+7JxP+TAd1kRLABFxe0CFm2VdK4ePkoA9FxQ==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", + "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", "requires": { "@babel/helper-module-imports": "^7.0.0", "@emotion/hash": "0.8.0", @@ -22818,11 +22864,6 @@ "source-map": "^0.5.7" }, "dependencies": { - "@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" - }, "@emotion/memoize": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", @@ -22840,11 +22881,6 @@ "csstype": "^2.5.7" } }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, "@emotion/utils": { "version": "0.11.3", "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", @@ -22859,6 +22895,14 @@ "dev": true, "requires": { "@babel/helper-plugin-utils": "7.10.4" + }, + "dependencies": { + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + } } }, "babel-plugin-inline-json-import": { @@ -22883,6 +22927,18 @@ "test-exclude": "^6.0.0" } }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, "babel-plugin-macros": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", @@ -22906,13 +22962,13 @@ } }, "parse-json": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.0.tgz", - "integrity": "sha512-OOY5b7PAEFV0E2Fir1KOkxchnZNCdowAJgQ5NuxjpBKTRP3pQhwkrkxqQjeoKJ+fO7bCpmIZaogI4eZGDMEGOw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -22927,10 +22983,11 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" }, "resolve": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", - "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", "requires": { + "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } } @@ -23036,6 +23093,40 @@ "integrity": "sha512-squySRkf+6JGnvjoUtDEjSREJEBirnXi9NqP6rjSYsylxQxqBTz+pkmf395i9E2zsvmYUaI40BHo6SqZUdydlw==", "dev": true }, + "babel-plugin-polyfill-corejs2": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.1.10.tgz", + "integrity": "sha512-DO95wD4g0A8KRaHKi0D51NdGXzvpqVLnLu5BTvDlpqUEpTmeEtypgC1xqesORaWmiUOQI14UHKlzNd9iZ2G3ZA==", + "requires": { + "@babel/compat-data": "^7.13.0", + "@babel/helper-define-polyfill-provider": "^0.1.5", + "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.1.7.tgz", + "integrity": "sha512-u+gbS9bbPhZWEeyy1oR/YaaSpod/KDT07arZHb80aTpl8H5ZBq+uN1nN9/xtX7jQyfLdPfoqI4Rue/MQSWJquw==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.1.5", + "core-js-compat": "^3.8.1" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.1.6.tgz", + "integrity": "sha512-OUrYG9iKPKz8NxswXbRAdSwF0GhRdIEMTloQATJi4bDuFqrXaXcCUT/VGNrr8pBcjMh1RxZ7Xt9cytVJTJfvMg==", + "requires": { + "@babel/helper-define-polyfill-provider": "^0.1.5" + } + }, "babel-plugin-react-docgen": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/babel-plugin-react-docgen/-/babel-plugin-react-docgen-4.2.1.tgz", @@ -23144,6 +23235,26 @@ "integrity": "sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=", "dev": true }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, "babel-preset-fbjs": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/babel-preset-fbjs/-/babel-preset-fbjs-3.3.0.tgz", @@ -23178,6 +23289,16 @@ "babel-plugin-syntax-trailing-function-commas": "^7.0.0-beta.0" } }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "babel-preset-minify": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz", @@ -23811,7 +23932,6 @@ "version": "4.15.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.15.0.tgz", "integrity": "sha512-IJ1iysdMkGmjjYeRlDU8PQejVwxvVO5QOfXH7ylW31GO6LwNRSmm/SgRXtNsEXqMLl2e+2H5eEJ7sfynF8TCaQ==", - "dev": true, "requires": { "caniuse-lite": "^1.0.30001164", "colorette": "^1.2.1", @@ -23823,32 +23943,27 @@ "caniuse-lite": { "version": "1.0.30001165", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001165.tgz", - "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==", - "dev": true + "integrity": "sha512-8cEsSMwXfx7lWSUMA2s08z9dIgsnR5NAqjXP23stdsU3AUWkCr/rr4s4OFtHXn5XXr6+7kam3QFVoYyXNPdJPA==" }, "colorette": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", - "dev": true + "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==" }, "electron-to-chromium": { "version": "1.3.619", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.619.tgz", - "integrity": "sha512-WFGatwtk7Fw0QcKCZzfGD72hvbcXV8kLY8aFuj0Ip0QRnOtyLYMsc+wXbSjb2w4lk1gcAeNU1/lQ20A+tvuypQ==", - "dev": true + "integrity": "sha512-WFGatwtk7Fw0QcKCZzfGD72hvbcXV8kLY8aFuj0Ip0QRnOtyLYMsc+wXbSjb2w4lk1gcAeNU1/lQ20A+tvuypQ==" }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" }, "node-releases": { "version": "1.1.67", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", - "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", - "dev": true + "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==" } } }, @@ -25748,6 +25863,54 @@ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw==", "dev": true }, + "core-js-compat": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.9.1.tgz", + "integrity": "sha512-jXAirMQxrkbiiLsCx9bQPJFA6llDadKMpYrBJQJ3/c4/vsPP/fAf29h24tviRlvwUL6AmY5CHLu2GvjuYviQqA==", + "requires": { + "browserslist": "^4.16.3", + "semver": "7.0.0" + }, + "dependencies": { + "browserslist": { + "version": "4.16.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.3.tgz", + "integrity": "sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw==", + "requires": { + "caniuse-lite": "^1.0.30001181", + "colorette": "^1.2.1", + "electron-to-chromium": "^1.3.649", + "escalade": "^3.1.1", + "node-releases": "^1.1.70" + } + }, + "caniuse-lite": { + "version": "1.0.30001203", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001203.tgz", + "integrity": "sha512-/I9tvnzU/PHMH7wBPrfDMSuecDeUKerjCPX7D0xBbaJZPxoT9m+yYxt0zCTkcijCkjTdim3H56Zm0i5Adxch4w==" + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==" + }, + "electron-to-chromium": { + "version": "1.3.692", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.692.tgz", + "integrity": "sha512-Ix+zDUAXWZuUzqKdhkgN5dP7ZM+IwMG4yAGFGDLpGJP/3vNEEwuHG1LIhtXUfW0FFV0j38t5PUv2n/3MFSRviQ==" + }, + "node-releases": { + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==" + }, + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==" + } + } + }, "core-js-pure": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.2.1.tgz", @@ -28352,6 +28515,11 @@ "integrity": "sha512-EmTr31wppcaIAgblChZiuN/l9Y7DPyw8Xtbg7fIVngn6zMW+IEBJDJngeKC3x6wr0V/vcA2wqeFnaw1bFJbDdA==", "dev": true }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -31229,9 +31397,9 @@ "dev": true }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==" + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" }, "get-caller-file": { "version": "1.0.3", @@ -33693,7 +33861,6 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -38420,8 +38587,7 @@ "json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { "version": "0.2.3", @@ -39313,6 +39479,11 @@ "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", "dev": true }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=" + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -48765,6 +48936,19 @@ "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", "dev": true }, + "regexpu-core": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", + "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "requires": { + "regenerate": "^1.4.0", + "regenerate-unicode-properties": "^8.2.0", + "regjsgen": "^0.5.1", + "regjsparser": "^0.6.4", + "unicode-match-property-ecmascript": "^1.0.4", + "unicode-match-property-value-ecmascript": "^1.2.0" + } + }, "regextras": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", @@ -48887,6 +49071,74 @@ "unified": "9.2.0" }, "dependencies": { + "@babel/core": { + "version": "7.12.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.12.9.tgz", + "integrity": "sha512-gTXYh3M5wb7FRXQy+FErKFAv90BnlOuNn1QkCK2lREoPAjrQCO49+HVSrFoe5uakFAF5eenS75KbO2vQiLrTMQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.10.4", + "@babel/generator": "^7.12.5", + "@babel/helper-module-transforms": "^7.12.1", + "@babel/helpers": "^7.12.5", + "@babel/parser": "^7.12.7", + "@babel/template": "^7.12.7", + "@babel/traverse": "^7.12.9", + "@babel/types": "^7.12.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.1", + "json5": "^2.1.2", + "lodash": "^4.17.19", + "resolve": "^1.3.2", + "semver": "^5.4.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.10.4.tgz", + "integrity": "sha512-O4KCvQA6lLiMU9l2eawBPMf1xPP8xPfB3iEQw150hOVTqj/rfXz0ThTb4HEzqQfs2Bmo5Ay8BzxfzVtBrr9dVg==", + "dev": true + }, + "@babel/plugin-proposal-object-rest-spread": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.12.1.tgz", + "integrity": "sha512-s6SowJIjzlhx8o7lsFx5zmY4At6CTtDvgNQDdPzkBQucle58A6b/TTeEBYtyDgmcXjUTM+vE8YOGHZzzbc/ioA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.0", + "@babel/plugin-transform-parameters": "^7.12.1" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.12.1.tgz", + "integrity": "sha512-1yRi7yAtB0ETgxdY9ti/p2TivUxJkTdhu/ZbF9MshVGqOx1TdB3b7xCXs49Fupgg50N45KcAsRP/ZqWjs9SRjg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "convert-source-map": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", + "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, "is-alphabetical": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", @@ -48905,6 +49157,21 @@ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", "dev": true }, + "json5": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", + "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "parse-entities": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", @@ -48943,6 +49210,12 @@ "xtend": "^4.0.1" } }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, "unified": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", diff --git a/package.json b/package.json index 60a94ce08904a4..1d42259d59ab1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "10.2.0-rc.1", + "version": "10.3.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", @@ -85,10 +85,10 @@ "devDependencies": { "@actions/core": "1.2.6", "@actions/github": "1.0.0", - "@babel/core": "7.12.9", - "@babel/plugin-syntax-jsx": "7.12.1", - "@babel/runtime-corejs3": "7.12.5", - "@babel/traverse": "7.12.9", + "@babel/core": "7.13.10", + "@babel/plugin-syntax-jsx": "7.12.13", + "@babel/runtime-corejs3": "7.13.10", + "@babel/traverse": "7.13.0", "@octokit/rest": "16.26.0", "@octokit/webhooks": "7.1.0", "@storybook/addon-a11y": "6.1.11", @@ -142,8 +142,8 @@ "@wordpress/stylelint-config": "file:packages/stylelint-config", "appium": "1.20.2", "babel-jest": "26.6.3", - "babel-loader": "8.1.0", - "babel-plugin-emotion": "10.0.33", + "babel-loader": "8.2.2", + "babel-plugin-emotion": "10.2.2", "babel-plugin-inline-json-import": "0.3.2", "babel-plugin-react-native-classname-to-style": "1.2.2", "babel-plugin-react-native-platform-specific-extensions": "1.1.1", diff --git a/packages/README.md b/packages/README.md index 8c1051cd603aee..147434da0365af 100644 --- a/packages/README.md +++ b/packages/README.md @@ -27,7 +27,7 @@ When creating a new package, you need to provide at least the following: "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/a11y/package.json b/packages/a11y/package.json index b808c0075a0799..90a10c4e5172f4 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -25,7 +25,7 @@ "react-native": "src/index", "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/dom-ready": "file:../dom-ready", "@wordpress/i18n": "file:../i18n" }, diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 3cc12b01cd09d3..88aa5eb0bc0a82 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/data": "file:../data", "@wordpress/hooks": "file:../hooks", "@wordpress/i18n": "file:../i18n", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index 2057f7c78084ed..b69ef5840fe9e4 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- Publish TypeScript definitions. + ## 3.22.0 (2021-03-17) ## 3.8.1 (2019-04-22) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index b3152710f8f7c6..d19dedf66ab44d 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -22,8 +22,9 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/i18n": "file:../i18n", "@wordpress/url": "file:../url" }, diff --git a/packages/api-fetch/src/index.js b/packages/api-fetch/src/index.js index a77a6295637a96..2b2c6b3508defe 100644 --- a/packages/api-fetch/src/index.js +++ b/packages/api-fetch/src/index.js @@ -23,7 +23,7 @@ import { * Default set of header values which should be sent with every request unless * explicitly provided through apiFetch options. * - * @type {Object} + * @type {Record} */ const DEFAULT_HEADERS = { // The backend uses the Accept header as a condition for considering an @@ -43,6 +43,9 @@ const DEFAULT_OPTIONS = { credentials: 'include', }; +/** + * @type {import('./types').ApiFetchMiddleware[]} + */ const middlewares = [ userLocaleMiddleware, namespaceEndpointMiddleware, @@ -50,10 +53,22 @@ const middlewares = [ fetchAllMiddleware, ]; +/** + * Register a middleware + * + * @param {import('./types').ApiFetchMiddleware} middleware + */ function registerMiddleware( middleware ) { middlewares.unshift( middleware ); } +/** + * Checks the status of a response, throwing the Response as an error if + * it is outside the 200 range. + * + * @param {Response} response + * @return {Response} The response if the status is in the 200 range. + */ const checkStatus = ( response ) => { if ( response.status >= 200 && response.status < 300 ) { return response; @@ -62,6 +77,11 @@ const checkStatus = ( response ) => { throw response; }; +/** @typedef {(options: import('./types').ApiFetchRequestProps) => Promise} FetchHandler*/ + +/** + * @type {FetchHandler} + */ const defaultFetchHandler = ( nextOptions ) => { const { url, path, data, parse = true, ...remainingOptions } = nextOptions; let { body, headers } = nextOptions; @@ -75,12 +95,16 @@ const defaultFetchHandler = ( nextOptions ) => { headers[ 'Content-Type' ] = 'application/json'; } - const responsePromise = window.fetch( url || path, { - ...DEFAULT_OPTIONS, - ...remainingOptions, - body, - headers, - } ); + const responsePromise = window.fetch( + // fall back to explicitly passing `window.location` which is the behavior if `undefined` is passed + url || path || window.location.href, + { + ...DEFAULT_OPTIONS, + ...remainingOptions, + body, + headers, + } + ); return ( responsePromise @@ -107,25 +131,34 @@ const defaultFetchHandler = ( nextOptions ) => { ); }; +/** @type {FetchHandler} */ let fetchHandler = defaultFetchHandler; /** * Defines a custom fetch handler for making the requests that will override * the default one using window.fetch * - * @param {Function} newFetchHandler The new fetch handler + * @param {FetchHandler} newFetchHandler The new fetch handler */ function setFetchHandler( newFetchHandler ) { fetchHandler = newFetchHandler; } +/** + * @template T + * @param {import('./types').ApiFetchRequestProps} options + * @return {Promise} A promise representing the request processed via the registered middlewares. + */ function apiFetch( options ) { // creates a nested function chain that calls all middlewares and finally the `fetchHandler`, // converting `middlewares = [ m1, m2, m3 ]` into: // ``` // opts1 => m1( opts1, opts2 => m2( opts2, opts3 => m3( opts3, fetchHandler ) ) ); // ``` - const enhancedHandler = middlewares.reduceRight( ( next, middleware ) => { + const enhancedHandler = middlewares.reduceRight( ( + /** @type {FetchHandler} */ next, + middleware + ) => { return ( workingOptions ) => middleware( workingOptions, next ); }, fetchHandler ); @@ -135,14 +168,18 @@ function apiFetch( options ) { } // If the nonce is invalid, refresh it and try again. - return window - .fetch( apiFetch.nonceEndpoint ) - .then( checkStatus ) - .then( ( data ) => data.text() ) - .then( ( text ) => { - apiFetch.nonceMiddleware.nonce = text; - return apiFetch( options ); - } ); + return ( + window + // @ts-ignore + .fetch( apiFetch.nonceEndpoint ) + .then( checkStatus ) + .then( ( data ) => data.text() ) + .then( ( text ) => { + // @ts-ignore + apiFetch.nonceMiddleware.nonce = text; + return apiFetch( options ); + } ) + ); } ); } diff --git a/packages/api-fetch/src/middlewares/fetch-all-middleware.js b/packages/api-fetch/src/middlewares/fetch-all-middleware.js index f062f8957f4fa8..a7779c9e925942 100644 --- a/packages/api-fetch/src/middlewares/fetch-all-middleware.js +++ b/packages/api-fetch/src/middlewares/fetch-all-middleware.js @@ -8,17 +8,32 @@ import { addQueryArgs } from '@wordpress/url'; */ import apiFetch from '..'; -// Apply query arguments to both URL and Path, whichever is present. +/** + * Apply query arguments to both URL and Path, whichever is present. + * + * @param {import('../types').ApiFetchRequestProps} props + * @param {Record} queryArgs + * @return {import('../types').ApiFetchRequestProps} The request with the modified query args + */ const modifyQuery = ( { path, url, ...options }, queryArgs ) => ( { ...options, url: url && addQueryArgs( url, queryArgs ), path: path && addQueryArgs( path, queryArgs ), } ); -// Duplicates parsing functionality from apiFetch. +/** + * Duplicates parsing functionality from apiFetch. + * + * @param {Response} response + * @return {Promise} Parsed response json. + */ const parseResponse = ( response ) => response.json ? response.json() : Promise.reject( response ); +/** + * @param {string | null} linkHeader + * @return {{ next?: string }} The parsed link header. + */ const parseLinkHeader = ( linkHeader ) => { if ( ! linkHeader ) { return {}; @@ -31,22 +46,34 @@ const parseLinkHeader = ( linkHeader ) => { : {}; }; +/** + * @param {Response} response + * @return {string | undefined} The next page URL. + */ const getNextPageUrl = ( response ) => { const { next } = parseLinkHeader( response.headers.get( 'link' ) ); return next; }; +/** + * @param {import('../types').ApiFetchRequestProps} options + * @return {boolean} True if the request contains an unbounded query. + */ const requestContainsUnboundedQuery = ( options ) => { const pathIsUnbounded = - options.path && options.path.indexOf( 'per_page=-1' ) !== -1; + !! options.path && options.path.indexOf( 'per_page=-1' ) !== -1; const urlIsUnbounded = - options.url && options.url.indexOf( 'per_page=-1' ) !== -1; + !! options.url && options.url.indexOf( 'per_page=-1' ) !== -1; return pathIsUnbounded || urlIsUnbounded; }; -// The REST API enforces an upper limit on the per_page option. To handle large -// collections, apiFetch consumers can pass `per_page=-1`; this middleware will -// then recursively assemble a full response array from all available pages. +/** + * The REST API enforces an upper limit on the per_page option. To handle large + * collections, apiFetch consumers can pass `per_page=-1`; this middleware will + * then recursively assemble a full response array from all available pages. + * + * @type {import('../types').ApiFetchMiddleware} + */ const fetchAllMiddleware = async ( options, next ) => { if ( options.parse === false ) { // If a consumer has opted out of parsing, do not apply middleware. @@ -81,7 +108,7 @@ const fetchAllMiddleware = async ( options, next ) => { } // Iteratively fetch all remaining pages until no "next" header is found. - let mergedResults = [].concat( results ); + let mergedResults = /** @type {any[]} */ ( [] ).concat( results ); while ( nextPage ) { const nextResponse = await apiFetch( { ...options, diff --git a/packages/api-fetch/src/middlewares/http-v1.js b/packages/api-fetch/src/middlewares/http-v1.js index cce48b5ed79422..8f8ced80c34152 100644 --- a/packages/api-fetch/src/middlewares/http-v1.js +++ b/packages/api-fetch/src/middlewares/http-v1.js @@ -1,7 +1,7 @@ /** * Set of HTTP methods which are eligible to be overridden. * - * @type {Set} + * @type {Set} */ const OVERRIDE_METHODS = new Set( [ 'PATCH', 'PUT', 'DELETE' ] ); @@ -21,12 +21,9 @@ const DEFAULT_METHOD = 'GET'; * API Fetch middleware which overrides the request method for HTTP v1 * compatibility leveraging the REST API X-HTTP-Method-Override header. * - * @param {Object} options Fetch options. - * @param {Function} next [description] - * - * @return {*} The evaluated result of the remaining middleware chain. + * @type {import('../types').ApiFetchMiddleware} */ -function httpV1Middleware( options, next ) { +const httpV1Middleware = ( options, next ) => { const { method = DEFAULT_METHOD } = options; if ( OVERRIDE_METHODS.has( method.toUpperCase() ) ) { options = { @@ -41,6 +38,6 @@ function httpV1Middleware( options, next ) { } return next( options ); -} +}; export default httpV1Middleware; diff --git a/packages/api-fetch/src/middlewares/media-upload.js b/packages/api-fetch/src/middlewares/media-upload.js index 5668c18cfbd792..fbfdeef9323291 100644 --- a/packages/api-fetch/src/middlewares/media-upload.js +++ b/packages/api-fetch/src/middlewares/media-upload.js @@ -14,22 +14,23 @@ import { /** * Middleware handling media upload failures and retries. * - * @param {Object} options Fetch options. - * @param {Function} next [description] - * - * @return {*} The evaluated result of the remaining middleware chain. + * @type {import('../types').ApiFetchMiddleware} */ -function mediaUploadMiddleware( options, next ) { +const mediaUploadMiddleware = ( options, next ) => { const isMediaUploadRequest = ( options.path && options.path.indexOf( '/wp/v2/media' ) !== -1 ) || ( options.url && options.url.indexOf( '/wp/v2/media' ) !== -1 ); if ( ! isMediaUploadRequest ) { - return next( options, next ); + return next( options ); } let retries = 0; const maxRetries = 5; + /** + * @param {string} attachmentId + * @return {Promise} Processed post response. + */ const postProcess = ( attachmentId ) => { retries++; return next( { @@ -78,6 +79,6 @@ function mediaUploadMiddleware( options, next ) { .then( ( response ) => parseResponseAndNormalizeError( response, options.parse ) ); -} +}; export default mediaUploadMiddleware; diff --git a/packages/api-fetch/src/middlewares/namespace-endpoint.js b/packages/api-fetch/src/middlewares/namespace-endpoint.js index 406c12926dd976..f6428c89a7f468 100644 --- a/packages/api-fetch/src/middlewares/namespace-endpoint.js +++ b/packages/api-fetch/src/middlewares/namespace-endpoint.js @@ -1,3 +1,6 @@ +/** + * @type {import('../types').ApiFetchMiddleware} + */ const namespaceAndEndpointMiddleware = ( options, next ) => { let path = options.path; let namespaceTrimmed, endpointTrimmed; diff --git a/packages/api-fetch/src/middlewares/nonce.js b/packages/api-fetch/src/middlewares/nonce.js index ed7381398216c0..703fb0dab981cf 100644 --- a/packages/api-fetch/src/middlewares/nonce.js +++ b/packages/api-fetch/src/middlewares/nonce.js @@ -1,14 +1,12 @@ /** * @param {string} nonce - * @return {import('../types').ApiFetchMiddleware} A middleware to enhance a request with a nonce. + * @return {import('../types').ApiFetchMiddleware & { nonce: string }} A middleware to enhance a request with a nonce. */ function createNonceMiddleware( nonce ) { /** - * @param {import('../types').ApiFetchRequestProps} options - * @param {(options: import('../types').ApiFetchRequestProps) => import('../types').ApiFetchRequestProps} next - * @return {import('../types').ApiFetchRequestProps} The enhanced request. + * @type {import('../types').ApiFetchMiddleware & { nonce: string }} */ - function middleware( options, next ) { + const middleware = ( options, next ) => { const { headers = {} } = options; // If an 'X-WP-Nonce' header (or any case-insensitive variation @@ -29,7 +27,7 @@ function createNonceMiddleware( nonce ) { 'X-WP-Nonce': middleware.nonce, }, } ); - } + }; middleware.nonce = nonce; diff --git a/packages/api-fetch/src/middlewares/preloading.js b/packages/api-fetch/src/middlewares/preloading.js index b2e787dcd6abf9..a0868de8f341ae 100644 --- a/packages/api-fetch/src/middlewares/preloading.js +++ b/packages/api-fetch/src/middlewares/preloading.js @@ -33,11 +33,15 @@ export function getStablePath( path ) { ); } +/** + * @param {Record} preloadedData + * @return {import('../types').ApiFetchMiddleware} Preloading middleware. + */ function createPreloadingMiddleware( preloadedData ) { const cache = Object.keys( preloadedData ).reduce( ( result, path ) => { result[ getStablePath( path ) ] = preloadedData[ path ]; return result; - }, {} ); + }, /** @type {Record} */ ( {} ) ); return ( options, next ) => { const { parse = true } = options; diff --git a/packages/api-fetch/src/middlewares/root-url.js b/packages/api-fetch/src/middlewares/root-url.js index 8d2ad636285476..3c73c5e4666650 100644 --- a/packages/api-fetch/src/middlewares/root-url.js +++ b/packages/api-fetch/src/middlewares/root-url.js @@ -3,6 +3,10 @@ */ import namespaceAndEndpointMiddleware from './namespace-endpoint'; +/** + * @param {string} rootURL + * @return {import('../types').ApiFetchMiddleware} Root URL middleware. + */ const createRootURLMiddleware = ( rootURL ) => ( options, next ) => { return namespaceAndEndpointMiddleware( options, ( optionsWithPath ) => { let url = optionsWithPath.url; diff --git a/packages/api-fetch/src/middlewares/user-locale.js b/packages/api-fetch/src/middlewares/user-locale.js index 1637a102514306..81fb7ebb25aad6 100644 --- a/packages/api-fetch/src/middlewares/user-locale.js +++ b/packages/api-fetch/src/middlewares/user-locale.js @@ -3,7 +3,10 @@ */ import { addQueryArgs, hasQueryArg } from '@wordpress/url'; -function userLocaleMiddleware( options, next ) { +/** + * @type {import('../types').ApiFetchMiddleware} + */ +const userLocaleMiddleware = ( options, next ) => { if ( typeof options.url === 'string' && ! hasQueryArg( options.url, '_locale' ) @@ -19,6 +22,6 @@ function userLocaleMiddleware( options, next ) { } return next( options ); -} +}; export default userLocaleMiddleware; diff --git a/packages/api-fetch/src/types.ts b/packages/api-fetch/src/types.ts index a65c11d3cbf4f5..93c466e8cd916b 100644 --- a/packages/api-fetch/src/types.ts +++ b/packages/api-fetch/src/types.ts @@ -1,4 +1,5 @@ -export interface ApiFetchRequestProps { +export interface ApiFetchRequestProps extends RequestInit { + // Override headers, we only accept it as an object due to the `nonce` middleware headers?: Record< string, string >; path?: string; url?: string; @@ -7,9 +8,11 @@ export interface ApiFetchRequestProps { */ parse?: boolean; data?: any; + namespace?: string; + endpoint?: string; } export type ApiFetchMiddleware = ( options: ApiFetchRequestProps, - next: ( nextOptions: ApiFetchRequestProps ) => ApiFetchRequestProps -) => ApiFetchRequestProps; + next: ( nextOptions: ApiFetchRequestProps ) => Promise< any > +) => Promise< any >; diff --git a/packages/api-fetch/src/utils/response.js b/packages/api-fetch/src/utils/response.js index d28413862b6c63..0a6ce48da900fe 100644 --- a/packages/api-fetch/src/utils/response.js +++ b/packages/api-fetch/src/utils/response.js @@ -9,7 +9,7 @@ import { __ } from '@wordpress/i18n'; * @param {Response} response * @param {boolean} shouldParseResponse * - * @return {Promise} Parsed response + * @return {Promise | null | Response} Parsed response. */ const parseResponse = ( response, shouldParseResponse = true ) => { if ( shouldParseResponse ) { @@ -23,6 +23,13 @@ const parseResponse = ( response, shouldParseResponse = true ) => { return response; }; +/** + * Calls the `json` function on the Response, throwing an error if the response + * doesn't have a json function or if parsing the json itself fails. + * + * @param {Response} response + * @return {Promise} Parsed response. + */ const parseJsonAndNormalizeError = ( response ) => { const invalidJsonError = { code: 'invalid_json', @@ -44,7 +51,7 @@ const parseJsonAndNormalizeError = ( response ) => { * @param {Response} response * @param {boolean} shouldParseResponse * - * @return {Promise} Parsed response. + * @return {Promise} Parsed response. */ export const parseResponseAndNormalizeError = ( response, @@ -55,6 +62,13 @@ export const parseResponseAndNormalizeError = ( ).catch( ( res ) => parseAndThrowError( res, shouldParseResponse ) ); }; +/** + * Parses a response, throwing an error if parsing the response fails. + * + * @param {Response} response + * @param {boolean} shouldParseResponse + * @return {Promise} Parsed response. + */ export function parseAndThrowError( response, shouldParseResponse = true ) { if ( ! shouldParseResponse ) { throw response; diff --git a/packages/api-fetch/tsconfig.json b/packages/api-fetch/tsconfig.json index 615ec508e64b0d..744a19d13fa97e 100644 --- a/packages/api-fetch/tsconfig.json +++ b/packages/api-fetch/tsconfig.json @@ -4,5 +4,14 @@ "rootDir": "src", "declarationDir": "build-types" }, - "include": [ "src/types.ts", "src/middlewares/nonce.js" ] + "references": [ + { "path": "../i18n" }, + { "path": "../url" } + ], + "include": [ + "src/**/*", + ], + "exclude": [ + "**/test/**/*", + ] } diff --git a/packages/autop/package.json b/packages/autop/package.json index 149e4dd11626da..b798cca499b356 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index 0d84a130d8b3e6..b966b9757147ea 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Enhancements + +- The bundled `@babel/core` dependency has been updated from requiring `^7.12.9` to requiring `^7.13.10` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). +- The bundled `@babel/plugin-transform-runtime` dependency has been updated from requiring `^7.12.1` to requiring `^7.13.10` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). +- The bundled `@babel/preset-env` dependency has been updated from requiring `^7.12.7` to requiring `^7.13.10` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). +- The bundled `@babel/preset-typescript` dependency has been updated from requiring `^7.12.7` to requiring `^7.13.10` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). +- The bundled `@babel/runtime` dependency has been updated from requiring `^7.12.5` to requiring `^7.13.10` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). + ## 5.1.0 (2021-03-17) ### New Features diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 2b451b3c4725fd..9757bf5caa02fd 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -28,12 +28,12 @@ ], "main": "index.js", "dependencies": { - "@babel/core": "^7.12.9", + "@babel/core": "^7.13.10", "@babel/plugin-transform-react-jsx": "^7.12.7", - "@babel/plugin-transform-runtime": "^7.12.1", - "@babel/preset-env": "^7.12.7", - "@babel/preset-typescript": "^7.12.7", - "@babel/runtime": "^7.12.5", + "@babel/plugin-transform-runtime": "^7.13.10", + "@babel/preset-env": "^7.13.10", + "@babel/preset-typescript": "^7.13.0", + "@babel/runtime": "^7.13.10", "@wordpress/babel-plugin-import-jsx-pragma": "file:../babel-plugin-import-jsx-pragma", "@wordpress/browserslist-config": "file:../browserslist-config", "@wordpress/element": "file:../element", diff --git a/packages/babel-preset-default/test/__snapshots__/index.js.snap b/packages/babel-preset-default/test/__snapshots__/index.js.snap index 9f7b9d7e009d99..667e2b65948139 100644 --- a/packages/babel-preset-default/test/__snapshots__/index.js.snap +++ b/packages/babel-preset-default/test/__snapshots__/index.js.snap @@ -2,9 +2,9 @@ exports[`Babel preset default transpilation works properly 1`] = ` "import _asyncToGenerator from \\"@babel/runtime/helpers/asyncToGenerator\\"; -import _regeneratorRuntime from \\"@babel/runtime/regenerator\\"; import _awaitAsyncGenerator from \\"@babel/runtime/helpers/awaitAsyncGenerator\\"; import _wrapAsyncGenerator from \\"@babel/runtime/helpers/wrapAsyncGenerator\\"; +import _regeneratorRuntime from \\"@babel/runtime/regenerator\\"; describe('Babel preset default', function () { function foo() { return _foo.apply(this, arguments); diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index b38d37c505e48e..030645e2f81fa3 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -175,20 +175,6 @@ } } - /* Mobile menu opened. */ - @media (max-width: #{ ($break-medium + 1) }) { - .auto-fold .wp-responsive-open #{$selector} { - left: $admin-sidebar-width-big; - } - } - - /* In small screens with responsive menu expanded there is small white space. */ - @media (max-width: #{ ($break-small) }) { - .auto-fold .wp-responsive-open #{$selector} { - margin-left: -18px; - } - } - body.is-fullscreen-mode #{$selector} { left: 0 !important; } @@ -224,19 +210,23 @@ @if $property == "transition" { @media (prefers-reduced-motion: reduce) { transition-duration: 0s; + transition-delay: 0s; } } @else if $property == "animation" { @media (prefers-reduced-motion: reduce) { animation-duration: 1ms; + animation-delay: 0s; } } @else { @media (prefers-reduced-motion: reduce) { transition-duration: 0s; + transition-delay: 0s; animation-duration: 1ms; + animation-delay: 0s; } } @@ -694,21 +684,3 @@ } /* stylelint-enable function-comma-space-after */ } - -/** - * These are default block editor widths in case the theme doesn't provide them. - */ -@mixin default-block-widths { - - .wp-block { - max-width: $content-width; - - &[data-align="wide"] { - max-width: $wide-content-width; - } - - &[data-align="full"] { - max-width: none; - } - } -} diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index 74d8042cae306d..dda9168fc25f66 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -63,7 +63,7 @@ $spinner-size: 18px; */ $shadow-popover: 0 2px 6px rgba($black, 0.05); -$shadow-modal: 0 3px 30px rgba($black, 0.2); +$shadow-modal: 0 10px 10px rgba($black, 0.25); /** diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 0967b058d09974..d42cf85994fe3c 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -105,8 +105,8 @@ $z-layers: ( // Show the navigation toggle above the skeleton header ".edit-site-navigation-toggle": 31, - // Show the FSE template previews above the editor (same z-index as the skeleton header) - ".edit-site-navigation-panel__preview": 30, + // Show the FSE template previews above the editor and any open block toolbars + ".edit-site-navigation-panel__preview": 32, // Show notices below expanded editor bar // .edit-post-header { z-index: 30 } diff --git a/packages/blob/package.json b/packages/blob/package.json index dccbd09ed7fec5..8e6afc34e1ce81 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 6cb2d5a800e101..3842faaf1688dd 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/block-directory/src/plugins/get-install-missing/index.js b/packages/block-directory/src/plugins/get-install-missing/index.js index c264d3a48d484a..c228b53c0b35b3 100644 --- a/packages/block-directory/src/plugins/get-install-missing/index.js +++ b/packages/block-directory/src/plugins/get-install-missing/index.js @@ -66,7 +66,7 @@ const ModifiedWarning = ( { originalBlock, ...props } ) => { let messageHTML = sprintf( /* translators: %s: block name */ __( - 'Your site doesnโ€™t include support for the %s block. You can try installing the block or remove it entirely!' + 'Your site doesnโ€™t include support for the %s block. You can try installing the block or remove it entirely.' ), originalBlock.title || originalName ); diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 55653a083dd7c6..be0718cd87cb66 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -76,6 +76,10 @@ registerCoreBlocks(); +# **AlignmentControl** + +Undocumented declaration. + # **AlignmentToolbar** Undocumented declaration. @@ -214,11 +218,13 @@ _Returns_ Undocumented declaration. -# **BlockVerticalAlignmentToolbar** +# **BlockVerticalAlignmentControl** -_Related_ +Undocumented declaration. -- +# **BlockVerticalAlignmentToolbar** + +Undocumented declaration. # **ButtonBlockerAppender** @@ -406,6 +412,10 @@ _Related_ - +# **JustifyContentControl** + +Undocumented declaration. + # **JustifyToolbar** Undocumented declaration. @@ -490,6 +500,7 @@ _Type Definition_ _Properties_ - _alignWide_ `boolean`: Enable/Disable Wide/Full Alignments +- _supportsLayout_ `boolean`: Enable/disable layouts support in container blocks. - _availableLegacyWidgets_ `Array`: Array of objects representing the legacy widgets available. - _imageEditing_ `boolean`: Image Editing settings set to false to disable. - _imageSizes_ `Array`: Available image sizes diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index d76e4f2f1f2fd3..ca8c954dc53e2d 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -28,7 +28,7 @@ "{src,build,build-module}/{index.js,store/index.js,hooks/**}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/blob": "file:../blob", "@wordpress/blocks": "file:../blocks", diff --git a/packages/block-editor/src/components/alignment-toolbar/README.md b/packages/block-editor/src/components/alignment-control/README.md similarity index 83% rename from packages/block-editor/src/components/alignment-toolbar/README.md rename to packages/block-editor/src/components/alignment-control/README.md index e400bb1547dd07..f78cd0299095c1 100644 --- a/packages/block-editor/src/components/alignment-toolbar/README.md +++ b/packages/block-editor/src/components/alignment-control/README.md @@ -1,6 +1,6 @@ -## Alignment Toolbar +## Alignment Control -The `AlignmentToolbar` component renders a toolbar that displays alignment options for the selected block. +The `AlignmentControl` component renders a dropdown mmmenu that displays alignment options for the selected block. This component is mostly used for blocks that display text, such as Heading, Paragraph, Post Author, Post Comments, Verse, Quote, Post Title, etc... And the available alignment options are `left`, `center` or `right` alignment. @@ -18,11 +18,11 @@ This component is mostly used for blocks that display text, such as Heading, Par Renders an alignment toolbar with alignments options. ```jsx -import { AlignmentToolbar } from '@wordpress/block-editor'; +import { AlignmentControl } from '@wordpress/block-editor'; const MyAlignmentToolbar = () => ( - - + { setAttributes( { textAlign: nextAlign } ); @@ -31,7 +31,7 @@ const MyAlignmentToolbar = () => ( ); ``` -_Note:_ In this example that we render `AlignmentToolbar` as a child of the `BlockControls` component. +_Note:_ In this example that we render `AlignmentControl` as a child of the `BlockControls` component. ### Props diff --git a/packages/block-editor/src/components/alignment-control/index.js b/packages/block-editor/src/components/alignment-control/index.js new file mode 100644 index 00000000000000..d8c4470a03bd0c --- /dev/null +++ b/packages/block-editor/src/components/alignment-control/index.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import AlignmentUI from './ui'; + +export function AlignmentControl( props ) { + return ; +} + +export function AlignmentToolbar( props ) { + return ; +} diff --git a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/alignment-control/test/__snapshots__/index.js.snap similarity index 95% rename from packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap rename to packages/block-editor/src/components/alignment-control/test/__snapshots__/index.js.snap index c6699db92e4961..4f549af0b2dc11 100644 --- a/packages/block-editor/src/components/alignment-toolbar/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/alignment-control/test/__snapshots__/index.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`AlignmentToolbar should allow custom alignment controls to be specified 1`] = ` +exports[`AlignmentUI should allow custom alignment controls to be specified 1`] = ` `; -exports[`AlignmentToolbar should match snapshot 1`] = ` +exports[`AlignmentUI should match snapshot 1`] = ` { +describe( 'AlignmentUI', () => { const alignment = 'left'; const onChangeSpy = jest.fn(); const wrapper = shallow( - + ); const controls = wrapper.props().controls; @@ -53,7 +53,8 @@ describe( 'AlignmentToolbar', () => { test( 'should allow custom alignment controls to be specified', () => { const wrapperCustomControls = shallow( - onChange( value === align ? undefined : align ); } @@ -57,9 +57,11 @@ export function AlignmentToolbar( props ) { return isRTL() ? alignRight : alignLeft; } + const UIComponent = isToolbar ? ToolbarGroup : DropdownMenu; + const extraProps = isToolbar ? { isCollapsed } : { isToolbarButton }; + return ( - ); } -export default AlignmentToolbar; +export default AlignmentUI; diff --git a/packages/block-editor/src/components/block-alignment-control/ui.js b/packages/block-editor/src/components/block-alignment-control/ui.js index 0ac3f4345e8e7f..a08f7d1b9ca755 100644 --- a/packages/block-editor/src/components/block-alignment-control/ui.js +++ b/packages/block-editor/src/components/block-alignment-control/ui.js @@ -3,7 +3,6 @@ */ import { __ } from '@wordpress/i18n'; import { DropdownMenu, ToolbarGroup } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; import { positionCenter, positionLeft, @@ -15,8 +14,7 @@ import { /** * Internal dependencies */ -import { useLayout } from '../inner-blocks/layout'; -import { store as blockEditorStore } from '../../store'; +import useAvailableAlignments from './use-available-alignments'; const BLOCK_ALIGNMENTS_CONTROLS = { left: { @@ -41,9 +39,7 @@ const BLOCK_ALIGNMENTS_CONTROLS = { }, }; -const DEFAULT_CONTROLS = [ 'left', 'center', 'right', 'wide', 'full' ]; const DEFAULT_CONTROL = 'center'; -const WIDE_CONTROLS = [ 'wide', 'full' ]; const POPOVER_PROPS = { isAlternate: true, @@ -52,32 +48,12 @@ const POPOVER_PROPS = { function BlockAlignmentUI( { value, onChange, - controls = DEFAULT_CONTROLS, + controls, isToolbar, isCollapsed = true, isToolbarButton = true, } ) { - const { wideControlsEnabled = false } = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - const settings = getSettings(); - return { - wideControlsEnabled: settings.alignWide, - }; - }, [] ); - const layout = useLayout(); - const supportsAlignments = layout.type === 'default'; - - if ( ! supportsAlignments ) { - return null; - } - - const { alignments: availableAlignments = DEFAULT_CONTROLS } = layout; - const enabledControls = controls.filter( - ( control ) => - ( wideControlsEnabled || ! WIDE_CONTROLS.includes( control ) ) && - availableAlignments.includes( control ) - ); - + const enabledControls = useAvailableAlignments( controls ); if ( enabledControls.length === 0 ) { return null; } diff --git a/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js b/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js new file mode 100644 index 00000000000000..64b85323a4c526 --- /dev/null +++ b/packages/block-editor/src/components/block-alignment-control/use-available-alignments.js @@ -0,0 +1,39 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { useLayout } from '../block-list/layout'; +import { store as blockEditorStore } from '../../store'; + +const DEFAULT_CONTROLS = [ 'left', 'center', 'right', 'wide', 'full' ]; +const WIDE_CONTROLS = [ 'wide', 'full' ]; + +export default function useAvailableAlignments( controls = DEFAULT_CONTROLS ) { + const { wideControlsEnabled = false } = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + const settings = getSettings(); + return { + wideControlsEnabled: settings.alignWide, + }; + }, [] ); + const layout = useLayout(); + const supportsAlignments = layout.type === 'default'; + + if ( ! supportsAlignments ) { + return []; + } + const { alignments: availableAlignments = DEFAULT_CONTROLS } = layout; + const enabledControls = controls.filter( + ( control ) => + ( layout.alignments || // Ignore the global wideAlignment check if the layout explicitely defines alignments. + wideControlsEnabled || + ! WIDE_CONTROLS.includes( control ) ) && + availableAlignments.includes( control ) + ); + + return enabledControls; +} diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/README.md b/packages/block-editor/src/components/block-alignment-matrix-control/README.md new file mode 100644 index 00000000000000..377f9f368dae4d --- /dev/null +++ b/packages/block-editor/src/components/block-alignment-matrix-control/README.md @@ -0,0 +1,63 @@ +# Alignment Matrix Control + +The alignment matrix control allows users to quickly adjust inner block alignment; this is in contrast to the alignment toolbar that aligns the frame block. + +![Button components](https://i.imgur.com/PxYkgL5.png) + +## Table of contents + +- [Alignment Matrix Control](#alignment-matrix-control) + - [Table of contents](#table-of-contents) + - [Design guidelines](#design-guidelines) + - [Usage](#usage) + - [Development guidelines](#development-guidelines) + - [Usage](#usage-1) + - [Props](#props) + +## Design guidelines + +### Usage + +The alignment matrix is a specialized tool, and it's used in the cover block. + +![Cover](https://i.imgur.com/nJjqen8.png) + +As an example, here's the matrix alignment tool in action. + +![center](https://i.imgur.com/0Ce1fZm.png) + +![rop_right](https://i.imgur.com/yGGf6IP.png) + +## Development guidelines + +### Usage + +```jsx +// This is a paraphrased example from the cover block +import { + BlockControls, + __experimentalBlockAlignmentMatrixControl as BlockAlignmentMatrixControl +} from "@wordpress/block-editor"; + +const controls = ( + <> + + + setAttributes( { contentPosition: nextPosition } ) + } + /> + + +} +``` + +### Props + +| Name | Type | Default | Description | +| ---------- | ---------- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `label` | `string` | `Change matrix alignment` | concise description of tool's functionality. | +| `onChange` | `function` | `noop` | the function to execute upon a user's change of the matrix state | +| `value` | `string` | `center` | describes the content alignment location and can be `top`, `right`, `bottom`, `left`, `topRight`, `bottomRight`, `bottomLeft`, `topLeft` | diff --git a/packages/block-editor/src/components/block-alignment-matrix-toolbar/index.js b/packages/block-editor/src/components/block-alignment-matrix-control/index.js similarity index 70% rename from packages/block-editor/src/components/block-alignment-matrix-toolbar/index.js rename to packages/block-editor/src/components/block-alignment-matrix-control/index.js index 8189d68f665844..5ad9dbffe64119 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-toolbar/index.js +++ b/packages/block-editor/src/components/block-alignment-matrix-control/index.js @@ -10,11 +10,10 @@ import { DOWN } from '@wordpress/keycodes'; import { ToolbarButton, Dropdown, - ToolbarGroup, __experimentalAlignmentMatrixControl as AlignmentMatrixControl, } from '@wordpress/components'; -export function BlockAlignmentMatrixToolbar( props ) { +function BlockAlignmentMatrixControl( props ) { const { label = __( 'Change matrix alignment' ), onChange = noop, @@ -23,7 +22,7 @@ export function BlockAlignmentMatrixToolbar( props ) { } = props; const icon = ; - const className = 'block-editor-block-alignment-matrix-toolbar'; + const className = 'block-editor-block-alignment-matrix-control'; const popoverClassName = `${ className }__popover`; const isAlternate = true; @@ -42,18 +41,16 @@ export function BlockAlignmentMatrixToolbar( props ) { }; return ( - - - + ); } } renderContent={ () => ( @@ -67,4 +64,4 @@ export function BlockAlignmentMatrixToolbar( props ) { ); } -export default BlockAlignmentMatrixToolbar; +export default BlockAlignmentMatrixControl; diff --git a/packages/block-editor/src/components/block-alignment-matrix-toolbar/style.scss b/packages/block-editor/src/components/block-alignment-matrix-control/style.scss similarity index 65% rename from packages/block-editor/src/components/block-alignment-matrix-toolbar/style.scss rename to packages/block-editor/src/components/block-alignment-matrix-control/style.scss index 1309de2961bfa2..bc2fa4837bece2 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-toolbar/style.scss +++ b/packages/block-editor/src/components/block-alignment-matrix-control/style.scss @@ -1,4 +1,4 @@ -.block-editor-block-alignment-matrix-toolbar__popover { +.block-editor-block-alignment-matrix-control__popover { .components-popover__content { min-width: 0; width: auto; @@ -7,5 +7,4 @@ padding: $grid-unit; } } - } diff --git a/packages/block-editor/src/components/block-alignment-matrix-toolbar/README.md b/packages/block-editor/src/components/block-alignment-matrix-toolbar/README.md deleted file mode 100644 index d28c9229aadc8e..00000000000000 --- a/packages/block-editor/src/components/block-alignment-matrix-toolbar/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Alignment Matrix Toolbar - -The alignment matrix toolbar allows users to quickly adjust inner block alignment; this is in contrast to the alignment toolbar that aligns the frame block. - -![Button components](https://i.imgur.com/PxYkgL5.png) - -## Table of contents -- [Alignment Matrix Toolbar](#alignment-matrix-toolbar) - - [Table of contents](#table-of-contents) - - [Design guidelines](#design-guidelines) - - [Usage](#usage) - - [Development guidelines](#development-guidelines) - - [Usage](#usage-1) - - [Props](#props) - -## Design guidelines - -### Usage - -The alignment matrix is a specialized tool, and it's used in the cover block. - -![Cover](https://i.imgur.com/nJjqen8.png) - -As an example, here's the matrix alignment tool in action. - -![center](https://i.imgur.com/0Ce1fZm.png) - - -![rop_right](https://i.imgur.com/yGGf6IP.png) - -## Development guidelines - -### Usage - -```jsx -// This is a paraphrased example from the cover block -import { - BlockControls, - __experimentalBlockAlignmentMatrixToolbar as BlockAlignmentMatrixToolbar -} from "@wordpress/block-editor"; - -const controls = ( - <> - - - setAttributes( { contentPosition: nextPosition } ) - } - /> - - -} -``` - -### Props - - -Name | Type | Default | Description ---- | --- | --- | --- -`label` | `string` | `Change matrix alignment` | concise description of tool's functionality. -`onChange` | `function` | `noop` | the function to execute upon a user's change of the matrix state -`value` | `string` | `center` | describes the content alignment location and can be `top`, `right`, `bottom`, `left`, `topRight`, `bottomRight`, `bottomLeft`, `topLeft` diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index 60f8616b5f49dd..21832629b9557f 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -11,6 +11,7 @@ import BlockIcon from '../block-icon'; function BlockCard( { title, icon, description, blockType } ) { if ( blockType ) { deprecated( '`blockType` property in `BlockCard component`', { + since: '5.7', alternative: '`title, icon and description` properties', } ); ( { title, icon, description } = blockType ); diff --git a/packages/block-editor/src/components/block-card/style.scss b/packages/block-editor/src/components/block-card/style.scss index e670b50d45b4f3..07bd4d1ef1d8ed 100644 --- a/packages/block-editor/src/components/block-card/style.scss +++ b/packages/block-editor/src/components/block-card/style.scss @@ -5,12 +5,15 @@ .block-editor-block-card__content { flex-grow: 1; + margin-bottom: $grid-unit-05; } .block-editor-block-card__title { font-weight: 500; + &.block-editor-block-card__title { - margin: 0 0 5px; + line-height: $button-size-small; + margin: 0 0 $grid-unit-05; } } @@ -19,10 +22,9 @@ } .block-editor-block-card .block-editor-block-icon { - flex: 0 0 $button-size; - margin-left: -2px; - margin-right: 10px; - padding: 0 3px; - width: $button-size; + flex: 0 0 $button-size-small; + margin-left: 0; + margin-right: $grid-unit-15; + width: $button-size-small; height: $button-size-small; } diff --git a/packages/block-editor/src/components/block-controls/fill.js b/packages/block-editor/src/components/block-controls/fill.js new file mode 100644 index 00000000000000..f66636e2a62654 --- /dev/null +++ b/packages/block-editor/src/components/block-controls/fill.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { isEmpty } from 'lodash'; + +/** + * WordPress dependencies + */ +import { + __experimentalToolbarContext as ToolbarContext, + ToolbarGroup, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import useDisplayBlockControls from '../use-display-block-controls'; +import groups from './groups'; + +export default function BlockControlsFill( { + group = 'default', + controls, + children, +} ) { + if ( ! useDisplayBlockControls() ) { + return null; + } + const Fill = groups[ group ].Fill; + + return ( + + { ( fillProps ) => { + // Children passed to BlockControlsFill will not have access to any + // React Context whose Provider is part of the BlockControlsSlot tree. + // So we re-create the Provider in this subtree. + const value = ! isEmpty( fillProps ) ? fillProps : null; + return ( + + { group === 'default' && ( + + ) } + { children } + + ); + } } + + ); +} diff --git a/packages/block-editor/src/components/block-controls/groups.js b/packages/block-editor/src/components/block-controls/groups.js new file mode 100644 index 00000000000000..42a94a4ab7a81b --- /dev/null +++ b/packages/block-editor/src/components/block-controls/groups.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { createSlotFill } from '@wordpress/components'; + +const BlockControlsDefault = createSlotFill( 'BlockControls' ); +const BlockControlsBlock = createSlotFill( 'BlockControlsBlock' ); +const BlockControlsInline = createSlotFill( 'BlockFormatControls' ); +const BlockControlsOther = createSlotFill( 'BlockControlsOther' ); + +const groups = { + default: BlockControlsDefault, + block: BlockControlsBlock, + inline: BlockControlsInline, + other: BlockControlsOther, +}; + +export default groups; diff --git a/packages/block-editor/src/components/block-controls/index.js b/packages/block-editor/src/components/block-controls/index.js index 297d3839df4aa9..f6c833715d8893 100644 --- a/packages/block-editor/src/components/block-controls/index.js +++ b/packages/block-editor/src/components/block-controls/index.js @@ -1,55 +1,19 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - -/** - * WordPress dependencies - */ -import { useContext } from '@wordpress/element'; -import { - __experimentalToolbarContext as ToolbarContext, - createSlotFill, - ToolbarGroup, -} from '@wordpress/components'; - /** * Internal dependencies */ -import useDisplayBlockControls from '../use-display-block-controls'; - -const { Fill, Slot } = createSlotFill( 'BlockControls' ); - -function BlockControlsSlot( props ) { - const accessibleToolbarState = useContext( ToolbarContext ); - return ; -} - -function BlockControlsFill( { controls, children } ) { - if ( ! useDisplayBlockControls() ) { - return null; - } - - return ( - - { ( fillProps ) => { - // Children passed to BlockControlsFill will not have access to any - // React Context whose Provider is part of the BlockControlsSlot tree. - // So we re-create the Provider in this subtree. - const value = ! isEmpty( fillProps ) ? fillProps : null; - return ( - - - { children } - - ); - } } - - ); -} +import BlockControlsFill from './fill'; +import BlockControlsSlot from './slot'; const BlockControls = BlockControlsFill; BlockControls.Slot = BlockControlsSlot; +// This is just here for backward compatibility +export const BlockFormatControls = ( props ) => { + return ; +}; +BlockFormatControls.Slot = ( props ) => { + return ; +}; + export default BlockControls; diff --git a/packages/block-editor/src/components/block-controls/slot.js b/packages/block-editor/src/components/block-controls/slot.js new file mode 100644 index 00000000000000..72621eb21c7358 --- /dev/null +++ b/packages/block-editor/src/components/block-controls/slot.js @@ -0,0 +1,45 @@ +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; +import { + __experimentalToolbarContext as ToolbarContext, + ToolbarGroup, + __experimentalUseSlot as useSlot, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import groups from './groups'; + +export default function BlockControlsSlot( { group = 'default', ...props } ) { + const accessibleToolbarState = useContext( ToolbarContext ); + const Slot = groups[ group ].Slot; + const slot = useSlot( Slot.__unstableName ); + const hasFills = Boolean( slot.fills && slot.fills.length ); + + if ( ! hasFills ) { + return null; + } + + if ( group === 'default' ) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/packages/block-editor/src/components/block-controls/slot.native.js b/packages/block-editor/src/components/block-controls/slot.native.js new file mode 100644 index 00000000000000..7ec46a8ef2251c --- /dev/null +++ b/packages/block-editor/src/components/block-controls/slot.native.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { useContext } from '@wordpress/element'; +import { + __experimentalToolbarContext as ToolbarContext, + ToolbarGroup, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import groups from './groups'; + +export default function BlockControlsSlot( { group = 'default', ...props } ) { + const accessibleToolbarState = useContext( ToolbarContext ); + const Slot = groups[ group ].Slot; + + if ( group === 'default' ) { + return ; + } + + return ( + + { ( fills ) => { + if ( ! fills.length ) { + return null; + } + return { fills }; + } } + + ); +} diff --git a/packages/block-editor/src/components/block-draggable/style.scss b/packages/block-editor/src/components/block-draggable/style.scss index 437ed65a936a88..b33fced9af2ab4 100644 --- a/packages/block-editor/src/components/block-draggable/style.scss +++ b/packages/block-editor/src/components/block-draggable/style.scss @@ -8,14 +8,12 @@ .block-editor-block-draggable-chip { background-color: $gray-900; border-radius: $radius-block-ui; - border: $border-width solid $gray-900; - box-shadow: 0 4px 6px rgba($black, 0.3); + box-shadow: 0 6px 8px rgba($black, 0.3); color: $white; cursor: grabbing; display: inline-flex; height: $block-toolbar-height; - min-width: $button-size * 2; - padding: 0 $grid-unit-15; + padding: 0 ( $grid-unit-15 + $border-width ); user-select: none; svg { @@ -24,6 +22,21 @@ .block-editor-block-draggable-chip__content { margin: auto; + justify-content: flex-start; + + > .components-flex__item { + margin-right: $grid-unit-15 / 2; + + &:last-child { + margin-right: 0; + } + } + + // Drag handle is smaller than the others. + .block-editor-block-icon svg { + min-width: 18px; + min-height: 18px; + } } .components-flex__item { diff --git a/packages/block-editor/src/components/block-format-controls/README.md b/packages/block-editor/src/components/block-format-controls/README.md deleted file mode 100644 index 856c5bb5e66824..00000000000000 --- a/packages/block-editor/src/components/block-format-controls/README.md +++ /dev/null @@ -1,29 +0,0 @@ -BlockFormatControls -============ - -`BlockFormatControls` is a slot & fill component that is the basis of all the formats, e.g., bold, italic, link, etc., that appear on the block format toolbar. -Under normal circumstances, the component should not be used directly. When implementing a custom format for the block editor, the API's wp.formatLibrary APIs should be used instead. These API's deal with much of the complexity required for implementing a custom format ( e.g., know which part of the text is selected etc.). `BlockFormatControls` could be considered when there is a need a very custom format with a special UI, and the wp.formatLibrary package does not fit the use-case. - -`BlockFormatControls` does not receive any properties and renders the children passed to it, as fills of the format toolbar slot. -E.g: To add a custom button to the format toolbar one can use the following sample: -```jsx - - - - { ( toggleProps ) => ( - - ) } - - - -``` - -Note that the component does not deal with anything related to applying a format. It just renders the children inside the format toolbar. diff --git a/packages/block-editor/src/components/block-format-controls/index.js b/packages/block-editor/src/components/block-format-controls/index.js deleted file mode 100644 index 73628e3415fc32..00000000000000 --- a/packages/block-editor/src/components/block-format-controls/index.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * External dependencies - */ -import { isEmpty } from 'lodash'; - -/** - * WordPress dependencies - */ -import { useContext } from '@wordpress/element'; -import { - __experimentalToolbarContext as ToolbarContext, - createSlotFill, -} from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; - -const { Fill, Slot } = createSlotFill( 'BlockFormatControls' ); - -function BlockFormatControlsSlot( props ) { - const accessibleToolbarState = useContext( ToolbarContext ); - return ; -} - -function BlockFormatControlsFill( props ) { - const { isSelected } = useBlockEditContext(); - if ( ! isSelected ) { - return null; - } - - return ( - - { ( fillProps ) => { - const value = ! isEmpty( fillProps ) ? fillProps : null; - return ( - - { props.children } - - ); - } } - - ); -} - -const BlockFormatControls = BlockFormatControlsFill; - -BlockFormatControls.Slot = BlockFormatControlsSlot; - -export default BlockFormatControls; diff --git a/packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md b/packages/block-editor/src/components/block-full-height-alignment-control/README.md similarity index 89% rename from packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md rename to packages/block-editor/src/components/block-full-height-alignment-control/README.md index 6febd1fc272f3a..3768bece723134 100644 --- a/packages/block-editor/src/components/block-full-height-alignment-toolbar/README.md +++ b/packages/block-editor/src/components/block-full-height-alignment-control/README.md @@ -1,3 +1,3 @@ -# Full Height Toolbar Toolbar +# Full Height Toolbar Control Unlike the block alignment options, `Full Height Alignment` can be applied to a block in an independent way. But also, it works very well together with these block alignment options, where the combination empowers the design-layout capability. \ No newline at end of file diff --git a/packages/block-editor/src/components/block-full-height-alignment-control/index.js b/packages/block-editor/src/components/block-full-height-alignment-control/index.js new file mode 100644 index 00000000000000..98384414ae4ba4 --- /dev/null +++ b/packages/block-editor/src/components/block-full-height-alignment-control/index.js @@ -0,0 +1,25 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { ToolbarButton } from '@wordpress/components'; +import { fullscreen } from '@wordpress/icons'; + +function BlockFullHeightAlignmentControl( { + isActive, + label = __( 'Toggle full height' ), + onToggle, + isDisabled, +} ) { + return ( + onToggle( ! isActive ) } + disabled={ isDisabled } + /> + ); +} + +export default BlockFullHeightAlignmentControl; diff --git a/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js b/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js deleted file mode 100644 index adc213859bd5b3..00000000000000 --- a/packages/block-editor/src/components/block-full-height-alignment-toolbar/index.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { ToolbarGroup, ToolbarButton } from '@wordpress/components'; -import { fullscreen } from '@wordpress/icons'; - -function BlockFullHeightAlignmentToolbar( { - isActive, - label = __( 'Toggle full height' ), - onToggle, - isDisabled, -} ) { - return ( - - onToggle( ! isActive ) } - disabled={ isDisabled } - /> - - ); -} - -export default BlockFullHeightAlignmentToolbar; diff --git a/packages/block-editor/src/components/block-inspector/style.scss b/packages/block-editor/src/components/block-inspector/style.scss index 94f6fdc3897839..393121ce4d8bd4 100644 --- a/packages/block-editor/src/components/block-inspector/style.scss +++ b/packages/block-editor/src/components/block-inspector/style.scss @@ -1,4 +1,15 @@ .block-editor-block-inspector { + p { + margin-top: 0; + } + + h2, + h3 { + font-size: $default-font-size; + color: $gray-900; + margin-bottom: 1.5em; + } + .components-base-control { margin-bottom: #{ $grid-unit-30 }; diff --git a/packages/block-editor/src/components/block-list/block-selection-button.js b/packages/block-editor/src/components/block-list/block-selection-button.js index 0c1c7310496635..e3f2353b8173f9 100644 --- a/packages/block-editor/src/components/block-list/block-selection-button.js +++ b/packages/block-editor/src/components/block-list/block-selection-button.js @@ -6,7 +6,8 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; +import { dragHandle } from '@wordpress/icons'; +import { Button, Flex, FlexItem } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect, useRef } from '@wordpress/element'; import { @@ -27,12 +28,16 @@ import { } from '@wordpress/blocks'; import { speak } from '@wordpress/a11y'; import { focus } from '@wordpress/dom'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ import BlockTitle from '../block-title'; +import BlockIcon from '../block-icon'; import { store as blockEditorStore } from '../../store'; +import BlockDraggable from '../block-draggable'; +import useBlockDisplayInformation from '../use-block-display-information'; /** * Returns true if the user is using windows. @@ -88,6 +93,7 @@ function selector( select ) { * @return {WPComponent} The component to be rendered. */ function BlockSelectionButton( { clientId, rootClientId, blockElement } ) { + const blockInformation = useBlockDisplayInformation( clientId ); const selected = useSelect( ( select ) => { const { @@ -256,16 +262,45 @@ function BlockSelectionButton( { clientId, rootClientId, blockElement } ) { } ); + const dragHandleLabel = __( 'Drag' ); + return (
- + + + + + + { ( draggableProps ) => ( + + +
); } diff --git a/packages/block-editor/src/components/block-list/block-selection-button.native.js b/packages/block-editor/src/components/block-list/block-selection-button.native.js index 699988f4028826..97a790482c7654 100644 --- a/packages/block-editor/src/components/block-list/block-selection-button.native.js +++ b/packages/block-editor/src/components/block-list/block-selection-button.native.js @@ -15,17 +15,18 @@ import { View, Text, TouchableOpacity } from 'react-native'; * Internal dependencies */ import BlockTitle from '../block-title'; +import useBlockDisplayInformation from '../use-block-display-information'; import SubdirectorSVG from './subdirectory-icon'; import { store as blockEditorStore } from '../../store'; import styles from './block-selection-button.scss'; const BlockSelectionButton = ( { clientId, - blockIcon, rootClientId, rootBlockIcon, isRTL, } ) => { + const blockInformation = useBlockDisplayInformation( clientId ); return ( { deprecated( 'wp.blockEditor.__experimentalBlock', { + since: '5.6', alternative: 'wp.blockEditor.useBlockProps', } ); const blockProps = useBlockProps( { ...props, ref } ); diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 2e1b5ba41314f2..07289cce385f0f 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -15,12 +15,7 @@ import { hasBlockSupport, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; -import { - withDispatch, - withSelect, - useDispatch, - useSelect, -} from '@wordpress/data'; +import { withDispatch, withSelect, useDispatch } from '@wordpress/data'; import { compose, pure, ifCondition } from '@wordpress/compose'; /** @@ -88,21 +83,6 @@ function BlockListBlock( { } ) { const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); - const isTypingWithinBlock = useSelect( - ( select ) => { - const { isTyping, hasSelectedInnerBlock } = select( - blockEditorStore - ); - return ( - // We only care about this prop when the block is selected - // Thus to avoid unnecessary rerenders we avoid updating the - // prop if the block is not selected. - ( isSelected || hasSelectedInnerBlock( clientId, true ) ) && - isTyping() - ); - }, - [ clientId, isSelected ] - ); // We wrap the BlockEdit component in a div that hides it when editing in // HTML mode. This allows us to render all of the ancillary pieces @@ -183,10 +163,7 @@ function BlockListBlock( { isSelected, index, // The wp-block className is important for editor styles. - className: classnames( className, { - 'wp-block': ! isAligned, - 'is-typing': isTypingWithinBlock, - } ), + className: classnames( className, { 'wp-block': ! isAligned } ), wrapperProps: omit( wrapperProps, [ 'data-align' ] ), }; const memoizedValue = useMemo( () => value, Object.values( value ) ); @@ -209,12 +186,10 @@ function BlockListBlock( { const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { const { isBlockSelected, - isFirstMultiSelectedBlock, getBlockMode, isSelectionEnabled, getTemplateLock, __unstableGetBlockWithoutInnerBlocks, - getMultiSelectedBlockClientIds, } = select( blockEditorStore ); const block = __unstableGetBlockWithoutInnerBlocks( clientId ); const isSelected = isBlockSelected( clientId ); @@ -224,15 +199,10 @@ const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { // the state. It happens now because the order in withSelect rendering // is not correct. const { name, attributes, isValid } = block || {}; - const isFirstMultiSelected = isFirstMultiSelectedBlock( clientId ); // Do not add new properties here, use `useSelect` instead to avoid // leaking new props to the public API (editor.BlockListBlock filter). return { - isFirstMultiSelected, - multiSelectedClientIds: isFirstMultiSelected - ? getMultiSelectedBlockClientIds() - : undefined, mode: getBlockMode( clientId ), isSelectionEnabled: isSelectionEnabled(), isLocked: !! templateLock, @@ -262,13 +232,13 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { // leaking new props to the public API (editor.BlockListBlock filter). return { setAttributes( newAttributes ) { - const { - clientId, - isFirstMultiSelected, - multiSelectedClientIds, - } = ownProps; - const clientIds = isFirstMultiSelected - ? multiSelectedClientIds + const { getMultiSelectedBlockClientIds } = select( + blockEditorStore + ); + const multiSelectedBlockClientIds = getMultiSelectedBlockClientIds(); + const { clientId } = ownProps; + const clientIds = multiSelectedBlockClientIds.length + ? multiSelectedBlockClientIds : [ clientId ]; updateBlockAttributes( clientIds, newAttributes ); diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index f1da838a8c7526..8e03ebf6b65942 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { AsyncModeProvider, useSelect } from '@wordpress/data'; import { useRef, createContext, useState } from '@wordpress/element'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -19,15 +20,39 @@ import useInsertionPoint from './insertion-point'; import BlockPopover from './block-popover'; import { store as blockEditorStore } from '../../store'; import { useScrollSelectionIntoView } from '../selection-scroll-into-view'; +import { usePreParsePatterns } from '../../utils/pre-parse-patterns'; +import { LayoutProvider, defaultLayout } from './layout'; export const BlockNodes = createContext(); export const SetBlockNodes = createContext(); -export default function BlockList( { className } ) { +export default function BlockList( { className, __experimentalLayout } ) { const ref = useRef(); const [ blockNodes, setBlockNodes ] = useState( {} ); const insertionPoint = useInsertionPoint( ref ); useScrollSelectionIntoView( ref ); + usePreParsePatterns(); + + const isLargeViewport = useViewportMatch( 'medium' ); + const { + isTyping, + isOutlineMode, + isFocusMode, + isNavigationMode, + } = useSelect( ( select ) => { + const { + isTyping: _isTyping, + getSettings, + isNavigationMode: _isNavigationMode, + } = select( blockEditorStore ); + const { outlineMode, focusMode } = getSettings(); + return { + isTyping: _isTyping(), + isOutlineMode: outlineMode, + isFocusMode: focusMode, + isNavigationMode: _isNavigationMode(), + }; + }, [] ); return ( @@ -37,11 +62,20 @@ export default function BlockList( { className } ) { ref={ ref } className={ classnames( 'block-editor-block-list__layout is-root-container', - className + className, + { + 'is-typing': isTyping, + 'is-outline-mode': isOutlineMode, + 'is-focus-mode': isFocusMode && isLargeViewport, + 'is-navigate-mode': isNavigationMode, + } ) } > - +
@@ -53,6 +87,7 @@ function Items( { rootClientId, renderAppender, __experimentalAppenderTagName, + __experimentalLayout: layout = defaultLayout, wrapperRef, } ) { function selector( select ) { @@ -88,7 +123,7 @@ function Items( { const isAppenderDropTarget = dropTargetIndex === blockClientIds.length; return ( - <> + { blockClientIds.map( ( clientId, index ) => { const isBlockInSelection = hasMultiSelection ? multiSelectedBlockClientIds.includes( clientId ) @@ -129,7 +164,7 @@ function Items( { isAppenderDropTarget && orientation === 'horizontal', } ) } /> - + ); } diff --git a/packages/block-editor/src/components/block-list/insertion-point.js b/packages/block-editor/src/components/block-list/insertion-point.js index a4f33a80bd8dad..d759d568efd3e9 100644 --- a/packages/block-editor/src/components/block-list/insertion-point.js +++ b/packages/block-editor/src/components/block-list/insertion-point.js @@ -200,6 +200,18 @@ function InsertionPointPopover( { } } + // Only show the inserter when there's a `nextElement` (a block after the + // insertion point). At the end of the block list the trailing appender + // should serve the purpose of inserting blocks. + const showInsertionPointInserter = + ! isHidden && nextElement && ( isInserterShown || isInserterForced ); + + // Show the indicator if the insertion point inserter is visible, or if + // the `showInsertionPoint` state is `true`. The latter is generally true + // when hovering blocks for insertion in the block library. + const showInsertionPointIndicator = + showInsertionPointInserter || ( ! isHidden && showInsertionPoint ); + /* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */ // While ideally it would be enough to capture the // bubbling focus event from the Inserter, due to the @@ -224,13 +236,10 @@ function InsertionPointPopover( { className={ className } style={ style } > - { ! isHidden && - ( showInsertionPoint || - isInserterShown || - isInserterForced ) && ( -
- ) } - { ! isHidden && ( isInserterShown || isInserterForced ) && ( + { showInsertionPointIndicator && ( +
+ ) } + { showInsertionPointInserter && ( { return ( - ( orientation === 'vertical' && + ( blockEl.classList.contains( 'wp-block' ) && + orientation === 'vertical' && blockEl.offsetTop > offsetTop ) || - ( orientation === 'horizontal' && + ( blockEl.classList.contains( 'wp-block' ) && + orientation === 'horizontal' && blockEl.offsetLeft > offsetLeft ) ); } ); diff --git a/packages/block-editor/src/components/block-list/layout.js b/packages/block-editor/src/components/block-list/layout.js new file mode 100644 index 00000000000000..3f898ae12c02fe --- /dev/null +++ b/packages/block-editor/src/components/block-list/layout.js @@ -0,0 +1,73 @@ +/** + * WordPress dependencies + */ +import { createContext, useContext } from '@wordpress/element'; + +export const defaultLayout = { type: 'default' }; + +const Layout = createContext( defaultLayout ); + +function appendSelectors( selectors, append ) { + // Ideally we shouldn't need the `.editor-styles-wrapper` increased specificity here + // The problem though is that we have a `.editor-styles-wrapper p { margin: reset; }` style + // it's used to reset the default margin added by wp-admin to paragraphs + // so we need this to be higher speficity otherwise, it won't be applied to paragraphs inside containers + // When the post editor is fully iframed, this extra classname could be removed. + + return selectors + .split( ',' ) + .map( + ( subselector ) => + `.editor-styles-wrapper ${ subselector } ${ append }` + ) + .join( ',' ); +} + +/** + * Allows to define the layout. + */ +export const LayoutProvider = Layout.Provider; + +/** + * React hook used to retrieve the layout config. + */ +export function useLayout() { + return useContext( Layout ); +} + +export function LayoutStyle( { selector, layout = {} } ) { + const { contentSize, wideSize } = layout; + + let style = + !! contentSize || !! wideSize + ? ` + ${ appendSelectors( selector, '> *' ) } { + max-width: ${ contentSize ?? wideSize }; + margin-left: auto; + margin-right: auto; + } + + ${ appendSelectors( selector, '> [data-align="wide"]' ) } { + max-width: ${ wideSize ?? contentSize }; + } + + ${ appendSelectors( selector, '> [data-align="full"]' ) } { + max-width: none; + } + ` + : ''; + + style += ` + ${ appendSelectors( selector, '> [data-align="left"]' ) } { + float: left; + margin-right: 2em; + } + + ${ appendSelectors( selector, '> [data-align="right"]' ) } { + float: right; + margin-left: 2em; + } + `; + + return ; +} diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index a5beff931b5cdb..443903fa8e976f 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -39,13 +39,20 @@ * Cross-Block Selection */ +// Note to developers refactoring this, please test navigation mode, and +// multi selection and hovering the block switcher to highlight the block. .block-editor-block-list__layout { position: relative; + // Select tool/navigation mode shows the default cursor until an additional click edits. + &.is-navigate-mode { + cursor: default; + } + // The primary indicator of selection in text is the native selection marker. // When selecting multiple blocks, we provide an additional selection indicator. - .is-navigate-mode & .block-editor-block-list__block.is-selected, - .is-navigate-mode & .block-editor-block-list__block.is-hovered, + &.is-navigate-mode .block-editor-block-list__block.is-selected, + &.is-navigate-mode .block-editor-block-list__block.is-hovered, .block-editor-block-list__block.is-highlighted, .block-editor-block-list__block.is-multi-selected { @@ -61,8 +68,7 @@ right: $border-width; // Everything else. - // 2px outside. - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color); border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. // Windows High Contrast mode will show this outline. @@ -70,7 +76,7 @@ // Show a lighter color for dark themes. .is-dark-theme & { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) $dark-theme-focus; + box-shadow: 0 0 0 $border-width $dark-theme-focus; } } @@ -82,10 +88,13 @@ } } - .is-navigate-mode & .block-editor-block-list__block.is-hovered:not(.is-selected)::after { + &.is-navigate-mode .block-editor-block-list__block.is-hovered:not(.is-selected)::after { box-shadow: 0 0 0 1px $gray-600; } + .block-editor-block-list__block.is-highlighted::after, + .block-editor-block-list__block.is-multi-selected::after, + &.is-navigate-mode .block-editor-block-list__block.is-selected::after, & .is-block-moving-mode.block-editor-block-list__block.has-child-selected { box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); outline: var(--wp-admin-border-width-focus) solid transparent; @@ -194,67 +203,6 @@ } } - &.is-outline-mode.is-selected:not(.is-typing) { - &::after { - box-shadow: 0 0 0 $border-width $gray-900; // Selected not focussed - top: $border-width; - left: $border-width; - right: $border-width; - bottom: $border-width; - border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. - } - - &:focus { - &::after { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - } - - &.is-outline-mode.is-hovered:not(.is-typing) { - cursor: default; - - &::after { - top: $border-width; - left: $border-width; - right: $border-width; - bottom: $border-width; - box-shadow: 0 0 0 $border-width var(--wp-admin-theme-color); - border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. - } - } - - &.is-outline-mode.is-selected.is-hovered { - cursor: unset; - } - - // Spotlight mode. - &.is-focus-mode:not(.is-multi-selected) { - opacity: 0.5; - transition: opacity 0.1s linear; - @include reduce-motion("transition"); - - &:not(.is-focused) .block-editor-block-list__block, - &.is-focused { - opacity: 1; - } - } - - // Active entity spotlight. - &.has-active-entity:not(.is-focus-mode) { - opacity: 0.5; - transition: opacity 0.1s linear; - @include reduce-motion("transition"); - - &.is-active-entity, - &.has-child-selected, - &:not(.has-child-selected) .block-editor-block-list__block, - &.is-active-entity .block-editor-block-list__block, - .is-active-entity .block-editor-block-list__block { - opacity: 1; - } - } - /** * Block styles and alignments */ @@ -321,11 +269,6 @@ box-shadow: 0 0 0 1px var(--wp-admin-theme-color); } - // Select tool/navigation mode shows the default cursor until an additional click edits. - .is-navigate-mode & { - cursor: default; - } - // Clear floats. &[data-clear="true"] { float: none; @@ -341,57 +284,80 @@ } } -// Extra specificity needed to override default element margins like lists (ul). -.block-editor-block-list__layout .wp-block { - margin-left: auto; - margin-right: auto; -} +.is-outline-mode:not(.is-typing) .block-editor-block-list__block { + &.is-hovered { + cursor: default; -.wp-block { - // Alignments. - &[data-align="left"], - &[data-align="right"] { - width: 100%; + &::after { + top: $border-width; + left: $border-width; + right: $border-width; + bottom: $border-width; + box-shadow: 0 0 0 $border-width $gray-900; + border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. + } + } - // When images are floated, the block itself should collapse to zero height. - height: 0; + &.is-selected { + cursor: unset; - &::before { - content: none; + &::after { + box-shadow: 0 0 0 $border-width $gray-900; // Selected not focussed + top: $border-width; + left: $border-width; + right: $border-width; + bottom: $border-width; + border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. } - } - &[data-align="left"] > *, - &[data-align="right"] > * { - // Without z-index, won't be clickable as "above" adjacent content. - z-index: z-index("{core/image aligned left or right} .wp-block"); + &:focus { + &::after { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } } +} - // Left. - &[data-align="left"] > * { - // This is in the editor only; the image should be floated on the frontend. - /*!rtl:begin:ignore*/ - float: left; - margin-right: 2em; - /*!rtl:end:ignore*/ - } +// Spotlight mode. Fade out blocks unless they contain a selected block. +.is-focus-mode .block-editor-block-list__block:not(.has-child-selected) { + opacity: 0.5; + transition: opacity 0.1s linear; + @include reduce-motion("transition"); - // Right. - &[data-align="right"] > * { - // Right: This is in the editor only; the image should be floated on the frontend. - /*!rtl:begin:ignore*/ - float: right; - margin-left: 2em; - /*!rtl:end:ignore*/ + // Nested blocks should never be faded. If the parent block is already faded + // out, it shouldn't be faded out more. If the parent block in not faded + // out, it shouldn't be faded out either because the block as a whole, + // including inner blocks, should be focused. + .block-editor-block-list__block, + &.is-selected, + &.is-multi-selected { + opacity: 1; } +} + +// Active entity spotlight. +// Disable if focus mode is active. +.is-root-container:not(.is-focus-mode) .block-editor-block-list__block.has-active-entity { + opacity: 0.5; + transition: opacity 0.1s linear; + @include reduce-motion("transition"); - // Wide and full-wide. - &[data-align="full"], - &[data-align="wide"] { - clear: both; + &.is-active-entity, + &.has-child-selected, + &:not(.has-child-selected) .block-editor-block-list__block, + &.is-active-entity .block-editor-block-list__block, + .is-active-entity .block-editor-block-list__block { + opacity: 1; } } +.wp-block[data-align="left"] > *, +.wp-block[data-align="right"] > * { + // Without z-index, won't be clickable as "above" adjacent content. + z-index: z-index("{core/image aligned left or right} .wp-block"); +} + + /** * In-Canvas Inserter */ @@ -581,6 +547,10 @@ .block-editor-block-mover.is-horizontal .block-editor-block-mover-button.block-editor-block-mover-button { min-width: $block-toolbar-height/2; width: $block-toolbar-height/2; + + svg { + min-width: $block-toolbar-height/2; + } } } @@ -614,28 +584,64 @@ */ .block-editor-block-list__block-selection-button { - display: block; + display: inline-flex; + padding: 0 ( $grid-unit-15 + $border-width ); z-index: z-index(".block-editor-block-list__block-selection-button"); - // The button here has a special style to appear as a toolbar. - .components-button { - font-size: $default-font-size; - height: $block-toolbar-height - $border-width - $border-width; - padding: $grid-unit-15 $grid-unit-20; + // Dark block UI appearance. + border-radius: $radius-block-ui; + background-color: $gray-900; - // Position this to align with the block toolbar. - position: relative; - top: -$border-width; + font-size: $default-font-size; + height: $block-toolbar-height; - // Block UI appearance. - box-shadow: 0 0 0 $border-width $gray-900; - border-radius: $radius-block-ui - $border-width; // Border is outset, so subtract the width to achieve correct radius. - background-color: $white; + .block-editor-block-list__block-selection-button__content { + margin: auto; + display: inline-flex; + align-items: center; + + > .components-flex__item { + margin-right: $grid-unit-15 / 2; + } + } + .components-button.has-icon.block-selection-button_drag-handle { + cursor: grab; + padding: 0; + height: $grid-unit-30; + min-width: $grid-unit-30; + + // Drag handle is smaller than the others. + svg { + min-width: 18px; + min-height: 18px; + } + } + + .block-editor-block-icon { + font-size: $default-font-size; + color: $white; + height: $block-toolbar-height; + } + + // The button here has a special style to appear as a toolbar. + .components-button { + min-width: $button-size; + color: $white; + height: $block-toolbar-height; // When button is focused, it receives a box-shadow instead of the border. &:focus { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + box-shadow: none; + border: none; + } + + &:active { + color: $white; } + display: flex; + } + .block-selection-button_select-button.components-button { + padding: 0; } } diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index c123a64967a004..44c70c213780d9 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -27,6 +27,7 @@ import { useBlockDefaultClassName } from './use-block-default-class-name'; import { useBlockCustomClassName } from './use-block-custom-class-name'; import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-names'; import { useEventHandlers } from './use-event-handlers'; +import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockNodes } from './use-block-nodes'; import { store as blockEditorStore } from '../../../store'; @@ -103,6 +104,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useFocusFirstElement( clientId ), useBlockNodes( clientId ), useEventHandlers( clientId ), + useNavModeExit( clientId ), useIsHovered(), useMovingAnimation( { isSelected: isPartOfSelection, diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js index d16bb5a4a90638..de6b55f7af4a29 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js @@ -7,7 +7,6 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; import { isReusableBlock, getBlockType } from '@wordpress/blocks'; /** @@ -23,7 +22,6 @@ import { store as blockEditorStore } from '../../../store'; * @return {string} The class names. */ export function useBlockClassNames( clientId ) { - const isLargeViewport = useViewportMatch( 'medium' ); return useSelect( ( select ) => { const { @@ -37,8 +35,6 @@ export function useBlockClassNames( clientId ) { __experimentalGetActiveBlockIdByBlockNames: getActiveBlockIdByBlockNames, } = select( blockEditorStore ); const { - focusMode, - outlineMode, __experimentalSpotlightEntityBlocks: spotlightEntityBlocks, } = getSettings(); const isDragging = isBlockBeingDragged( clientId ); @@ -59,18 +55,12 @@ export function useBlockClassNames( clientId ) { 'is-multi-selected': isBlockMultiSelected( clientId ), 'is-reusable': isReusableBlock( getBlockType( name ) ), 'is-dragging': isDragging, - 'is-focused': - focusMode && - isLargeViewport && - ( isSelected || isAncestorOfSelectedBlock ), - 'is-focus-mode': focusMode && isLargeViewport, - 'is-outline-mode': outlineMode, 'has-child-selected': isAncestorOfSelectedBlock && ! isDragging, 'has-active-entity': activeEntityBlockId, // Determine if there is an active entity area to spotlight. 'is-active-entity': activeEntityBlockId === clientId, } ); }, - [ clientId, isLargeViewport ] + [ clientId ] ); } diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js index 8160c4ffe38b15..5ac6be5aab0681 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js @@ -68,16 +68,14 @@ export function useFocusFirstElement( clientId ) { return; } + if ( ! ref.current ) { + return; + } + const { ownerDocument } = ref.current; - // Focus is captured by the wrapper node, so while focus transition - // should only consider tabbables within editable display, since it - // may be the wrapper itself or a side control which triggered the - // focus event, don't unnecessary transition to an inner tabbable. - if ( - ownerDocument.activeElement && - isInsideRootBlock( ref.current, ownerDocument.activeElement ) - ) { + // Do not focus the block if it already contains the active element. + if ( ref.current.contains( ownerDocument.activeElement ) ) { return; } diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js b/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js new file mode 100644 index 00000000000000..a051d1cce1bc46 --- /dev/null +++ b/packages/block-editor/src/components/block-list/use-block-props/use-nav-mode-exit.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { useRefEffect } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../../store'; + +/** + * Allows navigation mode to be exited by clicking in the selected block. + * + * @param {string} clientId Block client ID. + */ +export function useNavModeExit( clientId ) { + const isEnabled = useSelect( ( select ) => { + const { isNavigationMode, isBlockSelected } = select( + blockEditorStore + ); + return isNavigationMode() && isBlockSelected( clientId ); + }, [] ); + const { setNavigationMode } = useDispatch( blockEditorStore ); + + return useRefEffect( + ( node ) => { + if ( ! isEnabled ) { + return; + } + + function onMouseDown() { + setNavigationMode( false ); + } + + node.addEventListener( 'mousedown', onMouseDown ); + + return () => { + node.addEventListener( 'mousedown', onMouseDown ); + }; + }, + [ isEnabled ] + ); +} diff --git a/packages/block-editor/src/components/block-mover/style.scss b/packages/block-editor/src/components/block-mover/style.scss index 2fb4f47bd5ab8b..37ae058c7ba6f3 100644 --- a/packages/block-editor/src/components/block-mover/style.scss +++ b/packages/block-editor/src/components/block-mover/style.scss @@ -36,6 +36,11 @@ width: $block-toolbar-height - $grid-unit-15 / 2; padding-right: $grid-unit-15 - $border-width !important; padding-left: $grid-unit-15 / 2 !important; + + // Extra specificity to override standard toolbar button styles. + &.block-editor-block-mover-button { + min-width: $block-toolbar-height - $grid-unit-15 / 2; + } } // Focus style. diff --git a/packages/block-editor/src/components/block-navigation/block-slot.js b/packages/block-editor/src/components/block-navigation/block-slot.js index 9be9795317e6c7..9615d214b2c1b4 100644 --- a/packages/block-editor/src/components/block-navigation/block-slot.js +++ b/packages/block-editor/src/components/block-navigation/block-slot.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { getBlockType } from '@wordpress/blocks'; import { Fill, Slot, VisuallyHidden } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; import { Children, cloneElement, @@ -24,12 +25,17 @@ import BlockIcon from '../block-icon'; import { BlockListBlockContext } from '../block-list/block'; import BlockNavigationBlockSelectButton from './block-select-button'; import { getBlockPositionDescription } from './utils'; +import { store as blockEditorStore } from '../../store'; const getSlotName = ( clientId ) => `BlockNavigationBlock-${ clientId }`; function BlockNavigationBlockSlot( props, ref ) { - const instanceId = useInstanceId( BlockNavigationBlockSlot ); const { clientId } = props.block; + const { name } = useSelect( + ( select ) => select( blockEditorStore ).getBlockName( clientId ), + [ clientId ] + ); + const instanceId = useInstanceId( BlockNavigationBlockSlot ); return ( @@ -45,7 +51,6 @@ function BlockNavigationBlockSlot( props, ref ) { const { className, - block, isSelected, position, siblingBlockCount, @@ -54,7 +59,6 @@ function BlockNavigationBlockSlot( props, ref ) { onFocus, } = props; - const { name } = block; const blockType = getBlockType( name ); const descriptionId = `block-navigation-block-slot__${ instanceId }`; const blockPositionDescription = getBlockPositionDescription( diff --git a/packages/block-editor/src/components/block-navigation/block.js b/packages/block-editor/src/components/block-navigation/block.js index d191f692b77343..42e970cc7c0762 100644 --- a/packages/block-editor/src/components/block-navigation/block.js +++ b/packages/block-editor/src/components/block-navigation/block.js @@ -33,6 +33,8 @@ import { store as blockEditorStore } from '../../store'; export default function BlockNavigationBlock( { block, isSelected, + isBranchSelected, + isLastOfSelectedBranch, onClick, position, level, @@ -112,12 +114,19 @@ export default function BlockNavigationBlock( { highlightBlock( clientId, false ); }; + const classes = classnames( { + 'is-selected': isSelected, + 'is-branch-selected': + withExperimentalPersistentListViewFeatures && isBranchSelected, + 'is-last-of-selected-branch': + withExperimentalPersistentListViewFeatures && + isLastOfSelectedBranch, + 'is-dragging': isDragging, + } ); + return ( showAppender && ! isTreeRoot && - selectedBlockClientId === parentClientId; + isClientIdSelected( parentClientId, selectedBlockClientIds ); const hasAppender = itemHasAppender( parentBlockClientId ); // Add +1 to the rowCount to take the block appender into account. const blockCount = filteredBlocks.length; @@ -53,13 +55,30 @@ export default function BlockNavigationBranch( props ) { const hasNestedBlocks = showNestedBlocks && !! innerBlocks && !! innerBlocks.length; const hasNestedAppender = itemHasAppender( clientId ); + const hasNestedBranch = hasNestedBlocks || hasNestedAppender; + + const isSelected = isClientIdSelected( + clientId, + selectedBlockClientIds + ); + const isSelectedBranch = + isBranchSelected || ( isSelected && hasNestedBranch ); + + // Logic needed to target the last item of a selected branch which might be deeply nested. + // This is currently only needed for styling purposes. See: `.is-last-of-selected-branch`. + const isLastBlock = index === blockCount - 1; + const isLast = isSelected || ( isLastOfBranch && isLastBlock ); + const isLastOfSelectedBranch = + isLastOfBranch && ! hasNestedBranch && isLastBlock; return ( - { ( hasNestedBlocks || hasNestedAppender ) && ( + { hasNestedBranch && ( { + const { + getBlockHierarchyRootClientId, + getSelectedBlockClientId, + __unstableGetClientIdsTree, + __unstableGetClientIdWithClientIdsTree, + } = select( blockEditorStore ); + + const _selectedBlockClientId = getSelectedBlockClientId(); + const _rootBlocks = __unstableGetClientIdsTree(); + const _rootBlock = + selectedBlockClientId && ! isArray( selectedBlockClientId ) + ? __unstableGetClientIdWithClientIdsTree( + getBlockHierarchyRootClientId( + _selectedBlockClientId + ) + ) + : null; + + return { + rootBlock: _rootBlock, + rootBlocks: _rootBlocks, + selectedBlockClientId: _selectedBlockClientId, + }; + } + ); + const { selectBlock } = useDispatch( blockEditorStore ); + + function selectEditorBlock( clientId ) { + selectBlock( clientId ); + onSelect( clientId ); + } + if ( ! rootBlocks || rootBlocks.length === 0 ) { return null; } const hasHierarchy = rootBlock && - ( rootBlock.clientId !== selectedBlockClientId || + ( ! isClientIdSelected( rootBlock.clientId, selectedBlockClientId ) || ( rootBlock.innerBlocks && rootBlock.innerBlocks.length !== 0 ) ); return ( @@ -40,40 +71,11 @@ function BlockNavigation( {
); } - -export default compose( - withSelect( ( select ) => { - const { - getSelectedBlockClientId, - getBlockHierarchyRootClientId, - __unstableGetBlockWithBlockTree, - __unstableGetBlockTree, - } = select( blockEditorStore ); - const selectedBlockClientId = getSelectedBlockClientId(); - return { - rootBlocks: __unstableGetBlockTree(), - rootBlock: selectedBlockClientId - ? __unstableGetBlockWithBlockTree( - getBlockHierarchyRootClientId( selectedBlockClientId ) - ) - : null, - selectedBlockClientId, - }; - } ), - withDispatch( ( dispatch, { onSelect = noop } ) => { - return { - selectBlock( clientId ) { - dispatch( blockEditorStore ).selectBlock( clientId ); - onSelect( clientId ); - }, - }; - } ) -)( BlockNavigation ); diff --git a/packages/block-editor/src/components/block-navigation/style.scss b/packages/block-editor/src/components/block-navigation/style.scss index 546dab181208ef..e4cefe457e0dde 100644 --- a/packages/block-editor/src/components/block-navigation/style.scss +++ b/packages/block-editor/src/components/block-navigation/style.scss @@ -15,6 +15,12 @@ border-collapse: collapse; padding: 0; margin: 0; + + // Move upwards when in modal. + .components-modal__content & { + margin: (-$grid-unit-15) (-$grid-unit-15 / 2) 0; + width: calc(100% + #{ $grid-unit-15 }); + } } .block-editor-block-navigation-leaf { @@ -28,6 +34,21 @@ color: $white; } + &.is-branch-selected.is-selected .block-editor-block-navigation-block-contents { + border-radius: 2px 2px 0 0; + } + &.is-branch-selected:not(.is-selected) .block-editor-block-navigation-block-contents { + background: $gray-100; + border-radius: 0; + + &:hover { + background: $gray-300; + } + } + &.is-branch-selected.is-last-of-selected-branch .block-editor-block-navigation-block-contents { + border-radius: 0 0 2px 2px; + } + &.is-dragging { display: none; } @@ -49,6 +70,9 @@ } &:focus { + box-shadow: + inset 0 0 0 1px $white, + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); z-index: 1; } @@ -116,7 +140,11 @@ vertical-align: top; @include reduce-motion("transition"); + // Show on hover, visible, and show above to keep the hit area size. + &:hover, &.is-visible { + position: relative; + z-index: 1; opacity: 1; @include edit-post__fade-in-animation; } @@ -140,29 +168,31 @@ align-items: center; } - // Keep the tap target large but the focus target small + // Keep the tap target large but the focus target small. .block-editor-block-mover-button { position: relative; width: $button-size; height: $button-size-small; - // Position the icon + // Position the icon. svg { position: relative; height: $button-size-small; } &.is-up-button { + margin-top: -$grid-unit-15 / 2; align-items: flex-end; svg { - bottom: -4px; + bottom: -$grid-unit-05; } } &.is-down-button { + margin-bottom: -$grid-unit-15 / 2; align-items: flex-start; svg { - top: -4px; + top: -$grid-unit-05; } } @@ -231,5 +261,5 @@ .block-editor-block-navigator-indentation { flex-shrink: 0; - width: 18px; + width: $grid-unit-30 + $grid-unit-05; // Indent a full icon size, plus 4px which optically aligns child icons to the text label above. } diff --git a/packages/block-editor/src/components/block-navigation/tree.js b/packages/block-editor/src/components/block-navigation/tree.js index 9fc9e42ab93a65..e159c30973903b 100644 --- a/packages/block-editor/src/components/block-navigation/tree.js +++ b/packages/block-editor/src/components/block-navigation/tree.js @@ -5,7 +5,6 @@ import { __experimentalTreeGrid as TreeGrid } from '@wordpress/components'; import { useMemo, useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ diff --git a/packages/block-editor/src/components/block-navigation/utils.js b/packages/block-editor/src/components/block-navigation/utils.js index 51d3a015dc81bb..50bb561e42a8a6 100644 --- a/packages/block-editor/src/components/block-navigation/utils.js +++ b/packages/block-editor/src/components/block-navigation/utils.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { isArray } from 'lodash'; + /** * WordPress dependencies */ @@ -11,3 +16,17 @@ export const getBlockPositionDescription = ( position, siblingCount, level ) => siblingCount, level ); + +/** + * Returns true if the client ID occurs within the block selection or multi-selection, + * or false otherwise. + * + * @param {string} clientId Block client ID. + * @param {string|string[]} selectedBlockClientIds Selected block client ID, or an array of multi-selected blocks client IDs. + * + * @return {boolean} Whether the block is in multi-selection set. + */ +export const isClientIdSelected = ( clientId, selectedBlockClientIds ) => + isArray( selectedBlockClientIds ) && selectedBlockClientIds.length + ? selectedBlockClientIds.indexOf( clientId ) !== -1 + : selectedBlockClientIds === clientId; diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js index 1601cbb728b57b..b3169619f0d70d 100644 --- a/packages/block-editor/src/components/block-patterns-list/index.js +++ b/packages/block-editor/src/components/block-patterns-list/index.js @@ -1,8 +1,6 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; -import { parse } from '@wordpress/blocks'; import { VisuallyHidden, __unstableComposite as Composite, @@ -11,16 +9,22 @@ import { } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import BlockPreview from '../block-preview'; import InserterDraggableBlocks from '../inserter-draggable-blocks'; +import { store as blockEditorStore } from '../../store'; function BlockPattern( { isDraggable, pattern, onClick, composite } ) { - const { content, viewportWidth } = pattern; - const blocks = useMemo( () => parse( content ), [ content ] ); + const { name, viewportWidth } = pattern; + const { blocks } = useSelect( + ( select ) => + select( blockEditorStore ).__experimentalGetParsedPattern( name ), + [ name ] + ); const instanceId = useInstanceId( BlockPattern ); const descriptionId = `block-editor-block-patterns-list__item-description-${ instanceId }`; diff --git a/packages/block-editor/src/components/block-preview/index.js b/packages/block-editor/src/components/block-preview/index.js index 1c293fc15a2ed6..dfec2f64fab284 100644 --- a/packages/block-editor/src/components/block-preview/index.js +++ b/packages/block-editor/src/components/block-preview/index.js @@ -24,10 +24,15 @@ export function BlockPreview( { __experimentalLive = false, __experimentalOnClick, } ) { - const settings = useSelect( + const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), [] ); + const settings = useMemo( () => { + const _settings = { ...originalSettings }; + _settings.__experimentalBlockPatterns = []; + return _settings; + }, [ originalSettings ] ); const renderedBlocks = useMemo( () => castArray( blocks ), [ blocks ] ); if ( ! blocks || blocks.length === 0 ) { return null; diff --git a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js index e17301a31a52f2..8bf1b3082b1595 100644 --- a/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js +++ b/packages/block-editor/src/components/block-settings-menu/block-settings-dropdown.js @@ -7,18 +7,14 @@ import { castArray, flow, noop } from 'lodash'; * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { - DropdownMenu, - MenuGroup, - MenuItem, - ClipboardButton, -} from '@wordpress/components'; +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { moreVertical } from '@wordpress/icons'; import { Children, cloneElement, useCallback } from '@wordpress/element'; import { serialize } from '@wordpress/blocks'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { useCopyToClipboard } from '@wordpress/compose'; /** * Internal dependencies @@ -35,6 +31,11 @@ const POPOVER_PROPS = { isAlternate: true, }; +function CopyMenuItem( { blocks, onCopy } ) { + const ref = useCopyToClipboard( () => serialize( blocks ), onCopy ); + return { __( 'Copy' ) }; +} + export function BlockSettingsDropdown( { clientIds, __experimentalSelectBlock, @@ -112,14 +113,10 @@ export function BlockSettingsDropdown( { clientId={ firstBlockClientId } /> ) } - serialize( blocks ) } - role="menuitem" - className="components-menu-item__button" + - { __( 'Copy' ) } - + /> { canDuplicate && ( - + select( blocksStore ).getBlockType( name ), @@ -43,7 +41,7 @@ export default function BlockStylesMenu( { }, innerBlocks: blockType.example.innerBlocks, } ) - : cloneBlock( blockType, { + : cloneBlock( hoveredBlock, { className: hoveredClassName, } ) } diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index db07a42a6f56e5..599c936eeeb73a 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -19,7 +19,6 @@ import BlockMover from '../block-mover'; import BlockParentSelector from '../block-parent-selector'; import BlockSwitcher from '../block-switcher'; import BlockControls from '../block-controls'; -import BlockFormatControls from '../block-format-controls'; import BlockSettingsMenu from '../block-settings-menu'; import { useShowMoversGestures } from './utils'; import { store as blockEditorStore } from '../../store'; @@ -125,11 +124,16 @@ export default function BlockToolbar( { hideDragHandle } ) { { shouldShowVisualToolbar && ( <> - + + diff --git a/packages/block-editor/src/components/block-toolbar/index.native.js b/packages/block-editor/src/components/block-toolbar/index.native.js index 5e8848e3ff4ffa..f90d2f5a38d03d 100644 --- a/packages/block-editor/src/components/block-toolbar/index.native.js +++ b/packages/block-editor/src/components/block-toolbar/index.native.js @@ -7,7 +7,6 @@ import { useSelect } from '@wordpress/data'; * Internal dependencies */ import BlockControls from '../block-controls'; -import BlockFormatControls from '../block-format-controls'; import UngroupButton from '../ungroup-button'; import { store as blockEditorStore } from '../../store'; @@ -42,8 +41,10 @@ export default function BlockToolbar() { { mode === 'visual' && isValid && ( <> + - + + ) } diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index f34a4e69947ecc..f5c93ab2566918 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -93,7 +93,7 @@ } .block-editor-block-toolbar, -.block-editor-format-toolbar { +.block-editor-rich-text__inline-format-toolbar-group { // Override Toolbar buttons size. .components-toolbar-group, .components-toolbar { @@ -204,7 +204,7 @@ flex-shrink: 1; } - .block-editor-format-toolbar { + .block-editor-rich-text__inline-format-toolbar-group { .components-button + .components-button { margin-left: 6px; } diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js index ca54df0d24b78f..28c538ac52c7d1 100644 --- a/packages/block-editor/src/components/block-types-list/index.js +++ b/packages/block-editor/src/components/block-types-list/index.js @@ -2,15 +2,20 @@ * WordPress dependencies */ import { getBlockMenuDefaultClassName } from '@wordpress/blocks'; -import { - __unstableComposite as Composite, - __unstableUseCompositeState as useCompositeState, -} from '@wordpress/components'; /** * Internal dependencies */ import InserterListItem from '../inserter-list-item'; +import { InserterListboxGroup, InserterListboxRow } from '../inserter-listbox'; + +function chunk( array, size ) { + const chunks = []; + for ( let i = 0, j = array.length; i < j; i += size ) { + chunks.push( array.slice( i, i + size ) ); + } + return chunks; +} function BlockTypesList( { items = [], @@ -20,35 +25,30 @@ function BlockTypesList( { label, isDraggable = true, } ) { - const composite = useCompositeState(); return ( - /* - * Disable reason: The `list` ARIA role is redundant but - * Safari+VoiceOver won't announce the list otherwise. - */ - /* eslint-disable jsx-a11y/no-redundant-roles */ - - { items.map( ( item ) => { - return ( - - ); - } ) } + { chunk( items, 3 ).map( ( row, i ) => ( + + { row.map( ( item, j ) => ( + + ) ) } + + ) ) } { children } - - /* eslint-enable jsx-a11y/no-redundant-roles */ + ); } diff --git a/packages/block-editor/src/components/block-types-list/style.scss b/packages/block-editor/src/components/block-types-list/style.scss index 1f197db18126bd..f1f22fe504fbe4 100644 --- a/packages/block-editor/src/components/block-types-list/style.scss +++ b/packages/block-editor/src/components/block-types-list/style.scss @@ -1,5 +1,4 @@ -.block-editor-block-types-list { - list-style: none; +.block-editor-block-types-list > [role="presentation"] { padding: 4px; margin-left: -4px; margin-right: -4px; diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md b/packages/block-editor/src/components/block-vertical-alignment-control/README.md similarity index 77% rename from packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md rename to packages/block-editor/src/components/block-vertical-alignment-control/README.md index 6b2f3c389c604d..284907f09eda90 100644 --- a/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md +++ b/packages/block-editor/src/components/block-vertical-alignment-control/README.md @@ -1,20 +1,20 @@ -BlockVerticalAlignmentToolbar +BlockVerticalAlignmentControl ============================= -`BlockVerticalAlignmentToolbar` is a simple `Toolbar` component designed to provide _vertical_ alignment UI controls for use within the editor `BlockControls` toolbar. +`BlockVerticalAlignmentControl` is a simple component designed to provide _vertical_ alignment UI controls for use within the editor `BlockControls` toolbar. -This builds upon similar patterns to the [`BlockAlignmentToolbar`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/editor/src/components/block-alignment-toolbar) but is focused on vertical alignment only. +This builds upon similar patterns to the [`BlockAlignmentControl`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/editor/src/components/block-alignment-control) but is focused on vertical alignment only. ## Usage -In a block's `edit` implementation, render a `` component. Then inside of this add the `` where required. +In a block's `edit` implementation, render a `` component. Then inside of this add the `` where required. ```jsx import { registerBlockType } from '@wordpress/blocks'; import { BlockControls, - BlockVerticalAlignmentToolbar, + BlockVerticalAlignmentControl, useBlockProps, } from '@wordpress/block-editor'; @@ -40,8 +40,8 @@ registerBlockType( 'my-plugin/my-block', { return ( <> - - + @@ -68,12 +68,6 @@ _Note:_ the user can repeatedly click on the toolbar buttons to toggle the align The current value of the alignment setting. You may only choose from the `Options` listed above. -### `isCollapsed` -* **Type:** `Boolean` -* **Default:** `true` - -Whether to display toolbar options in the dropdown menu. This toolbar is collapsed by default. - ### `onChange` * **Type:** `Function` @@ -87,4 +81,4 @@ const onChange = ( alignment ) => setAttributes( { verticalAlignment: alignment ## Examples -The [Core Columns](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/columns) Block utilises the `BlockVerticalAlignmentToolbar`. +The [Core Columns](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/columns) Block utilises the `BlockVerticalAlignmentControl`. diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js b/packages/block-editor/src/components/block-vertical-alignment-control/icons.js similarity index 100% rename from packages/block-editor/src/components/block-vertical-alignment-toolbar/icons.js rename to packages/block-editor/src/components/block-vertical-alignment-control/icons.js diff --git a/packages/block-editor/src/components/block-vertical-alignment-control/index.js b/packages/block-editor/src/components/block-vertical-alignment-control/index.js new file mode 100644 index 00000000000000..8dcdfd6030aa0a --- /dev/null +++ b/packages/block-editor/src/components/block-vertical-alignment-control/index.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import BlockVerticalAlignmentUI from './ui'; + +export function BlockVerticalAlignmentControl( props ) { + return ; +} + +export function BlockVerticalAlignmentToolbar( props ) { + return ; +} diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap b/packages/block-editor/src/components/block-vertical-alignment-control/test/__snapshots__/index.js.snap similarity index 95% rename from packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap rename to packages/block-editor/src/components/block-vertical-alignment-control/test/__snapshots__/index.js.snap index cf5ad6cf554d71..94563ab02b7d9f 100644 --- a/packages/block-editor/src/components/block-vertical-alignment-toolbar/test/__snapshots__/index.js.snap +++ b/packages/block-editor/src/components/block-vertical-alignment-control/test/__snapshots__/index.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`BlockVerticalAlignmentToolbar should match snapshot 1`] = ` +exports[`BlockVerticalAlignmentUI should match snapshot 1`] = ` { +describe( 'BlockVerticalAlignmentUI', () => { const alignment = 'top'; const onChange = jest.fn(); const wrapper = shallow( - diff --git a/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js b/packages/block-editor/src/components/block-vertical-alignment-control/ui.js similarity index 82% rename from packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js rename to packages/block-editor/src/components/block-vertical-alignment-control/ui.js index babf6dc676ac92..c2e69596deef07 100644 --- a/packages/block-editor/src/components/block-vertical-alignment-toolbar/index.js +++ b/packages/block-editor/src/components/block-vertical-alignment-control/ui.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { _x } from '@wordpress/i18n'; -import { ToolbarGroup } from '@wordpress/components'; +import { ToolbarGroup, DropdownMenu } from '@wordpress/components'; /** * Internal dependencies @@ -31,11 +31,13 @@ const POPOVER_PROPS = { isAlternate: true, }; -export function BlockVerticalAlignmentToolbar( { +function BlockVerticalAlignmentUI( { value, onChange, controls = DEFAULT_CONTROLS, isCollapsed = true, + isToolbar, + isToolbarButton = true, } ) { function applyOrUnset( align ) { return () => onChange( value === align ? undefined : align ); @@ -45,10 +47,12 @@ export function BlockVerticalAlignmentToolbar( { const defaultAlignmentControl = BLOCK_ALIGNMENTS_CONTROLS[ DEFAULT_CONTROL ]; + const UIComponent = isToolbar ? ToolbarGroup : DropdownMenu; + const extraProps = isToolbar ? { isCollapsed } : { isToolbarButton }; + return ( - ); } @@ -73,4 +78,4 @@ export function BlockVerticalAlignmentToolbar( { /** * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-vertical-alignment-toolbar/README.md */ -export default BlockVerticalAlignmentToolbar; +export default BlockVerticalAlignmentUI; diff --git a/packages/block-editor/src/components/editable-text/README.md b/packages/block-editor/src/components/editable-text/README.md index 6696a43c7e05c0..fb4e780f36d681 100644 --- a/packages/block-editor/src/components/editable-text/README.md +++ b/packages/block-editor/src/components/editable-text/README.md @@ -14,7 +14,7 @@ Renders an editable text input in which text formatting is not allowed. ### `tagName: String` -*Default: `div`.* The [tag name](https://www.w3.org/TR/html51/syntax.html#tag-name) of the editable element. Elements that display inline are not supported. +*Default: `div`.* The [tag name](https://www.w3.org/TR/html51/syntax.html#tag-name) of the editable element. Elements that display inline are not supported. Set to `inline-block` to use tag names that have `inline` as the default. ### `disableLineBreaks: Boolean` diff --git a/packages/block-editor/src/components/index.js b/packages/block-editor/src/components/index.js index 16977e34af4976..ac7471317acbe4 100644 --- a/packages/block-editor/src/components/index.js +++ b/packages/block-editor/src/components/index.js @@ -5,20 +5,22 @@ export * from './colors'; export * from './gradients'; export * from './font-sizes'; -export { default as AlignmentToolbar } from './alignment-toolbar'; +export { AlignmentControl, AlignmentToolbar } from './alignment-control'; export { default as Autocomplete } from './autocomplete'; export { BlockAlignmentControl, BlockAlignmentToolbar, } from './block-alignment-control'; -export { default as __experimentalBlockFullHeightAligmentToolbar } from './block-full-height-alignment-toolbar'; -export { default as __experimentalBlockAlignmentMatrixToolbar } from './block-alignment-matrix-toolbar'; +export { default as __experimentalBlockFullHeightAligmentControl } from './block-full-height-alignment-control'; +export { default as __experimentalBlockAlignmentMatrixControl } from './block-alignment-matrix-control'; export { default as BlockBreadcrumb } from './block-breadcrumb'; export { BlockContextProvider } from './block-context'; -export { default as BlockControls } from './block-controls'; +export { + default as BlockControls, + BlockFormatControls, +} from './block-controls'; export { default as BlockColorsStyleSelector } from './color-style-selector'; export { default as BlockEdit, useBlockEditContext } from './block-edit'; -export { default as BlockFormatControls } from './block-format-controls'; export { default as BlockIcon } from './block-icon'; export { default as BlockNavigationDropdown } from './block-navigation/dropdown'; export { BlockNavigationBlockFill as __experimentalBlockNavigationBlockFill } from './block-navigation/block-slot'; @@ -26,7 +28,10 @@ export { default as __experimentalBlockNavigationEditor } from './block-navigati export { default as __experimentalBlockNavigationTree } from './block-navigation/tree'; export { default as __experimentalBlockVariationPicker } from './block-variation-picker'; export { default as __experimentalBlockVariationTransforms } from './block-variation-transforms'; -export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; +export { + BlockVerticalAlignmentToolbar, + BlockVerticalAlignmentControl, +} from './block-vertical-alignment-control'; export { default as ButtonBlockerAppender } from './button-block-appender'; export { default as ColorPalette } from './color-palette'; export { default as ColorPaletteControl } from './color-palette/control'; @@ -45,7 +50,10 @@ export { } from './inner-blocks'; export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; export { default as InspectorControls } from './inspector-controls'; -export { default as JustifyToolbar } from './justify-toolbar'; +export { + JustifyToolbar, + JustifyContentControl, +} from './justify-content-control'; export { default as __experimentalLinkControl } from './link-control'; export { default as __experimentalLinkControlSearchInput } from './link-control/search-input'; export { default as __experimentalLinkControlSearchResults } from './link-control/search-results'; @@ -84,6 +92,7 @@ export { default as BlockInspector } from './block-inspector'; export { default as BlockList } from './block-list'; export { useBlockProps } from './block-list/use-block-props'; export { Block as __experimentalBlock } from './block-list/block-wrapper'; +export { LayoutStyle as __experimentalLayoutStyle } from './block-list/layout'; export { default as BlockMover } from './block-mover'; export { default as BlockPreview } from './block-preview'; export { diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 3fbd2e0fc752fe..29449bb0a0843d 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -1,19 +1,30 @@ // Block Creation Components -export { BlockAlignmentToolbar } from './block-alignment-control'; +export { + BlockAlignmentControl, + BlockAlignmentToolbar, +} from './block-alignment-control'; export { BlockContextProvider } from './block-context'; -export { default as BlockControls } from './block-controls'; +export { + default as BlockControls, + BlockFormatControls, +} from './block-controls'; export { default as BlockEdit, useBlockEditContext } from './block-edit'; -export { default as BlockFormatControls } from './block-format-controls'; export { default as BlockIcon } from './block-icon'; -export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; +export { + BlockVerticalAlignmentToolbar, + BlockVerticalAlignmentControl, +} from './block-vertical-alignment-control'; export * from './colors'; export * from './gradients'; export * from './font-sizes'; -export { default as AlignmentToolbar } from './alignment-toolbar'; +export { AlignmentControl, AlignmentToolbar } from './alignment-control'; export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorAdvancedControls } from './inspector-advanced-controls'; export { default as InspectorControls } from './inspector-controls'; -export { default as JustifyToolbar } from './justify-toolbar'; +export { + JustifyToolbar, + JustifyContentControl, +} from './justify-content-control'; export { default as LineHeightControl } from './line-height-control'; export { default as PlainText } from './plain-text'; export { diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index 8cae0693f60fb3..f841a1fd99c4cb 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -128,32 +128,33 @@ If locking is not set in an `InnerBlocks` area: the locking of the parent `Inner If the block is a top level block: the locking of the Custom Post Type is used. ### `renderAppender` -* **Type:** `Function|false` -* **Default:** - `undefined`. When `renderAppender` is not specific the `` component is as a default. It automatically inserts whichever block is configured as the default block via `wp.blocks.setDefaultBlockName` (typically `paragraph`). If a `false` value is provider, no appender is rendered. +* **Type:** `Component|false` +* **Default:** - `undefined`. When `renderAppender` is not specified, the default appender is shown. If a `false` value is provided, no appender is rendered. -A 'render prop' function that can be used to customize the block's appender. +A component to show as the trailing appender for the inner blocks list. #### Notes -* For convenience two predefined appender components are exposed on `InnerBlocks` which can be consumed within the render function: - - `` - display a `+` (plus) icon button that, when clicked, displays the block picker menu. No default Block is inserted. - - `` - display the default block appender as set by `wp.blocks.setDefaultBlockName`. Typically this is the `paragraph` block. -* Consumers are also free to pass any valid render function. This provides the full flexibility to define a bespoke block appender. +* For convenience two predefined appender components are exposed on `InnerBlocks` which can be used for the prop: + - `InnerBlocks.ButtonBlockAppender` - display a `+` (plus) icon button as the appender. + - `InnerBlocks.DefaultBlockAppender` - display the default block appender, typically the paragraph style appender when the paragraph block is allowed. +* Consumers are also free to pass any valid component. This provides the full flexibility to define a bespoke block appender. #### Example usage ```jsx // Utilise a predefined component ( - - ) } + renderAppender={ InnerBlocks.ButtonBlockAppender } +/> + +// Don't display an appender + // Fully custom ( - - ) } + renderAppender={ MyAmazingAppender } /> ``` @@ -164,7 +165,7 @@ A 'render prop' function that can be used to customize the block's appender. Determines whether the toolbars of _all_ child Blocks (applied deeply, recursive) should have their toolbars "captured" and shown on the Block which is consuming `InnerBlocks`. -For example, a button block, deeply nested in several levels of block `X` that utilises this property will see the button block's toolbar displayed on block `X`'s toolbar area. +For example, a button block, deeply nested in several levels of block `X` that utilizes this property will see the button block's toolbar displayed on block `X`'s toolbar area. ### `placeholder` diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 7d8db1f019e866..e826f3f9a1c52b 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -23,7 +23,6 @@ import { BlockListItems } from '../block-list'; import { BlockContextProvider } from '../block-context'; import { useBlockEditContext } from '../block-edit/context'; import useBlockSync from '../provider/use-block-sync'; -import { defaultLayout, LayoutProvider } from './layout'; import { store as blockEditorStore } from '../../store'; /** @@ -47,7 +46,7 @@ function UncontrolledInnerBlocks( props ) { renderAppender, orientation, placeholder, - __experimentalLayout: layout = defaultLayout, + __experimentalLayout, } = props; useNestedSettingsUpdate( @@ -82,19 +81,16 @@ function UncontrolledInnerBlocks( props ) { // This component needs to always be synchronous as it's the one changing // the async mode depending on the block selection. return ( - - - - - + + + ); } diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js index 4681b5e6f9ff35..0b9401425e8640 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -20,7 +20,7 @@ import BlockList from '../block-list'; import { useBlockEditContext } from '../block-edit/context'; import useBlockSync from '../provider/use-block-sync'; import { BlockContextProvider } from '../block-context'; -import { defaultLayout, LayoutProvider } from './layout'; +import { defaultLayout, LayoutProvider } from '../block-list/layout'; import { store as blockEditorStore } from '../../store'; /** diff --git a/packages/block-editor/src/components/inner-blocks/layout.js b/packages/block-editor/src/components/inner-blocks/layout.js deleted file mode 100644 index 745b59945ea61d..00000000000000 --- a/packages/block-editor/src/components/inner-blocks/layout.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { createContext, useContext } from '@wordpress/element'; - -export const defaultLayout = { type: 'default' }; - -const Layout = createContext( defaultLayout ); - -/** - * Allows to define the layout. - */ -export const LayoutProvider = Layout.Provider; - -/** - * React hook used to retrieve the layout config. - */ -export function useLayout() { - return useContext( Layout ); -} diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index e2fb004fedbe26..5a13c27c8616f9 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -7,10 +7,6 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useMemo, useRef, memo } from '@wordpress/element'; -import { - Button, - __unstableCompositeItem as CompositeItem, -} from '@wordpress/components'; import { createBlock, createBlocksFromInnerBlocksTemplate, @@ -21,6 +17,7 @@ import { ENTER } from '@wordpress/keycodes'; * Internal dependencies */ import BlockIcon from '../block-icon'; +import { InserterListboxItem } from '../inserter-listbox'; import InserterDraggableBlocks from '../inserter-draggable-blocks'; /** @@ -41,7 +38,7 @@ function isAppleOS( _window = window ) { function InserterListItem( { className, - composite, + isFirst, item, onSelect, onHover, @@ -89,10 +86,8 @@ function InserterListItem( { } } } > - onHover( null ) } onBlur={ () => onHover( null ) } - // Use the CompositeItem `focusable` prop over Button's - // isFocusable. The latter was shown to cause an issue - // with tab order in the inserter list. - focusable { ...props } > { item.title } - +
) } diff --git a/packages/block-editor/src/components/inserter-listbox/context.js b/packages/block-editor/src/components/inserter-listbox/context.js new file mode 100644 index 00000000000000..aaaac34f05f61f --- /dev/null +++ b/packages/block-editor/src/components/inserter-listbox/context.js @@ -0,0 +1,8 @@ +/** + * WordPress dependencies + */ +import { createContext } from '@wordpress/element'; + +const InserterListboxContext = createContext(); + +export default InserterListboxContext; diff --git a/packages/block-editor/src/components/inserter-listbox/group.js b/packages/block-editor/src/components/inserter-listbox/group.js new file mode 100644 index 00000000000000..11e98cf01b202f --- /dev/null +++ b/packages/block-editor/src/components/inserter-listbox/group.js @@ -0,0 +1,40 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useEffect, useState } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import { speak } from '@wordpress/a11y'; + +function InserterListboxGroup( props, ref ) { + const [ shouldSpeak, setShouldSpeak ] = useState( false ); + + useEffect( () => { + if ( shouldSpeak ) { + speak( + __( 'Use left and right arrow keys to move through blocks' ) + ); + } + }, [ shouldSpeak ] ); + + return ( +
{ + setShouldSpeak( true ); + } } + onBlur={ ( event ) => { + const focusingOutsideGroup = ! event.currentTarget.contains( + event.relatedTarget + ); + if ( focusingOutsideGroup ) { + setShouldSpeak( false ); + } + } } + { ...props } + /> + ); +} + +export default forwardRef( InserterListboxGroup ); diff --git a/packages/block-editor/src/components/inserter-listbox/index.js b/packages/block-editor/src/components/inserter-listbox/index.js new file mode 100644 index 00000000000000..6345cb38c494ac --- /dev/null +++ b/packages/block-editor/src/components/inserter-listbox/index.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { __unstableUseCompositeState as useCompositeState } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import InserterListboxContext from './context'; + +export { default as InserterListboxGroup } from './group'; +export { default as InserterListboxRow } from './row'; +export { default as InserterListboxItem } from './item'; + +function InserterListbox( { children } ) { + const compositeState = useCompositeState( { + shift: true, + wrap: 'horizontal', + } ); + return ( + + { children } + + ); +} + +export default InserterListbox; diff --git a/packages/block-editor/src/components/inserter-listbox/item.js b/packages/block-editor/src/components/inserter-listbox/item.js new file mode 100644 index 00000000000000..50adb4a7880387 --- /dev/null +++ b/packages/block-editor/src/components/inserter-listbox/item.js @@ -0,0 +1,52 @@ +/** + * WordPress dependencies + */ +import { + Button, + __unstableCompositeItem as CompositeItem, +} from '@wordpress/components'; +import { forwardRef, useContext } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import InserterListboxContext from './context'; + +function InserterListboxItem( + { isFirst, as: Component, children, ...props }, + ref +) { + const state = useContext( InserterListboxContext ); + return ( + + { ( htmlProps ) => { + const propsWithTabIndex = { + ...htmlProps, + tabIndex: isFirst ? 0 : htmlProps.tabIndex, + }; + if ( Component ) { + return ( + + { children } + + ); + } + if ( typeof children === 'function' ) { + return children( propsWithTabIndex ); + } + return ; + } } + + ); +} + +export default forwardRef( InserterListboxItem ); diff --git a/packages/block-editor/src/components/inserter-listbox/row.js b/packages/block-editor/src/components/inserter-listbox/row.js new file mode 100644 index 00000000000000..710267660199d7 --- /dev/null +++ b/packages/block-editor/src/components/inserter-listbox/row.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { forwardRef, useContext } from '@wordpress/element'; +import { __unstableCompositeGroup as CompositeGroup } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import InserterListboxContext from './context'; + +function InserterListboxRow( props, ref ) { + const state = useContext( InserterListboxContext ); + return ( + + ); +} + +export default forwardRef( InserterListboxRow ); diff --git a/packages/block-editor/src/components/inserter/block-types-tab.js b/packages/block-editor/src/components/inserter/block-types-tab.js index 00b51a4ed2165d..4f681b239a8940 100644 --- a/packages/block-editor/src/components/inserter/block-types-tab.js +++ b/packages/block-editor/src/components/inserter/block-types-tab.js @@ -15,6 +15,7 @@ import { useMemo, useEffect } from '@wordpress/element'; import BlockTypesList from '../block-types-list'; import InserterPanel from './panel'; import useBlockTypesState from './hooks/use-block-types-state'; +import InserterListbox from '../inserter-listbox'; const getBlockNamespace = ( item ) => item.name.split( '/' )[ 0 ]; @@ -71,75 +72,77 @@ export function BlockTypesTab( { useEffect( () => () => onHover( null ), [] ); return ( -
- { showMostUsedBlocks && !! suggestedItems.length && ( - - - - ) } - - { map( categories, ( category ) => { - const categoryItems = itemsPerCategory[ category.slug ]; - if ( ! categoryItems || ! categoryItems.length ) { - return null; - } - return ( - + +
+ { showMostUsedBlocks && !! suggestedItems.length && ( + - ); - } ) } - - { ! uncategorizedItems.length && ( - - - - ) } - - { map( collections, ( collection, namespace ) => { - const collectionItems = itemsPerCollection[ namespace ]; - if ( ! collectionItems || ! collectionItems.length ) { - return null; - } - - return ( + ) } + + { map( categories, ( category ) => { + const categoryItems = itemsPerCategory[ category.slug ]; + if ( ! categoryItems || ! categoryItems.length ) { + return null; + } + return ( + + + + ); + } ) } + + { uncategorizedItems.length > 0 && ( - ); - } ) } -
+ ) } + + { map( collections, ( collection, namespace ) => { + const collectionItems = itemsPerCollection[ namespace ]; + if ( ! collectionItems || ! collectionItems.length ) { + return null; + } + + return ( + + + + ); + } ) } +
+ ); } diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index c71e28a6b69c42..73bb0e074281b7 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -1,21 +1,33 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; /** * WordPress dependencies */ -import { useEffect, useCallback } from '@wordpress/element'; +import { useEffect, useState, useCallback } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { createBlock, rawHandler, store as blocksStore, } from '@wordpress/blocks'; -import { getClipboard } from '@wordpress/components'; +import { + BottomSheet, + BottomSheetConsumer, + getClipboard, +} from '@wordpress/components'; /** * Internal dependencies */ import InserterSearchResults from './search-results'; +import InserterSearchForm from './search-form'; import { store as blockEditorStore } from '../../store'; +import { searchItems } from './search-items'; + +const MIN_ITEMS_FOR_SEARCH = 2; function InserterMenu( { onSelect, @@ -26,6 +38,11 @@ function InserterMenu( { shouldReplaceBlock, insertionIndex, } ) { + const [ filterValue, setFilterValue ] = useState( '' ); + const [ searchFormHeight, setSearchFormHeight ] = useState( 0 ); + // eslint-disable-next-line no-undef + const [ showSearchForm, setShowSearchForm ] = useState( __DEV__ ); + const { showInsertionPoint, hideInsertionPoint, @@ -70,6 +87,7 @@ function InserterMenu( { const { getBlockType } = useSelect( ( select ) => select( blocksStore ) ); useEffect( () => { + // Show/Hide insertion point on Mount/Dismount if ( shouldReplaceBlock ) { const count = getBlockCount(); // Check if there is a rootClientId because that means it is a nested replaceable block @@ -86,14 +104,19 @@ function InserterMenu( { removeBlock( blockToReplace, false ); } } - // Show/Hide insertion point on Mount/Dismount showInsertionPoint( destinationRootClientId, insertionIndex ); + + // Show search form if there are enough items to filter. + if ( getItems()?.length < MIN_ITEMS_FOR_SEARCH ) { + setShowSearchForm( false ); + } + return hideInsertionPoint; }, [] ); const onClose = useCallback( () => { - // If should replace but didn't insert any block - // re-insert default block. + // if should replace but didn't insert any block + // re-insert default block if ( shouldReplaceBlock ) { insertDefaultBlock( {}, destinationRootClientId, insertionIndex ); } @@ -122,10 +145,12 @@ function InserterMenu( { */ function getItems() { // Filter out reusable blocks (they will be added in another tab) - const itemsToDisplay = items.filter( + let itemsToDisplay = items.filter( ( { name } ) => name !== 'core/block' ); + itemsToDisplay = searchItems( itemsToDisplay, filterValue ); + const clipboard = getClipboard(); let clipboardBlock = rawHandler( { HTML: clipboard } )[ 0 ]; @@ -153,14 +178,45 @@ function InserterMenu( { } return ( - { - onInsert( item ); - onSelect( item ); - } } - /> + hideHeader + hasNavigation + setMinHeightToMaxHeight={ showSearchForm } + > + + { ( { listProps, safeAreaBottomInset } ) => ( + + { showSearchForm && ( + { + setFilterValue( value ); + } } + value={ filterValue } + onLayout={ ( event ) => { + const { height } = event.nativeEvent.layout; + setSearchFormHeight( height ); + } } + /> + ) } + + { + onInsert( item ); + onSelect( item ); + } } + { ...{ + listProps, + safeAreaBottomInset, + searchFormHeight, + } } + /> + + ) } + + ); } diff --git a/packages/block-editor/src/components/inserter/search-form.native.js b/packages/block-editor/src/components/inserter/search-form.native.js new file mode 100644 index 00000000000000..8178aef4affc65 --- /dev/null +++ b/packages/block-editor/src/components/inserter/search-form.native.js @@ -0,0 +1,92 @@ +/** + * External dependencies + */ +import { TextInput, View, TouchableHighlight } from 'react-native'; + +/** + * WordPress dependencies + */ +import { useState, useRef } from '@wordpress/element'; +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { __ } from '@wordpress/i18n'; +import { ToolbarButton } from '@wordpress/components'; +import { + Icon, + cancelCircleFilled, + arrowLeft, + search as searchIcon, +} from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +function InserterSearchForm( { value, onChange, onLayout = () => {} } ) { + const [ isActive, setIsActive ] = useState( false ); + + const inputRef = useRef(); + + const searchFormStyle = usePreferredColorSchemeStyle( + styles.searchForm, + styles.searchFormDark + ); + + const searchFormInputStyle = usePreferredColorSchemeStyle( + styles.searchFormInput, + styles.searchFormInputDark + ); + + const placeholderStyle = usePreferredColorSchemeStyle( + styles.searchFormPlaceholder, + styles.searchFormPlaceholderDark + ); + + return ( + + + { isActive ? ( + { + inputRef.current.blur(); + onChange( '' ); + setIsActive( false ); + } } + /> + ) : ( + { + inputRef.current.focus(); + setIsActive( true ); + } } + /> + ) } + setIsActive( true ) } + value={ value } + placeholder={ __( 'Search blocks' ) } + /> + + { !! value && ( + } + onClick={ () => { + onChange( '' ); + } } + /> + ) } + + + ); +} + +export default InserterSearchForm; diff --git a/packages/block-editor/src/components/inserter/search-results.js b/packages/block-editor/src/components/inserter/search-results.js index 0dae29a2305561..03bf7b7c2da998 100644 --- a/packages/block-editor/src/components/inserter/search-results.js +++ b/packages/block-editor/src/components/inserter/search-results.js @@ -24,6 +24,7 @@ import useInsertionPoint from './hooks/use-insertion-point'; import usePatternsState from './hooks/use-patterns-state'; import useBlockTypesState from './hooks/use-block-types-state'; import { searchBlockItems, searchItems } from './search-items'; +import InserterListbox from '../inserter-listbox'; function InserterSearchResults( { filterValue, @@ -104,7 +105,7 @@ function InserterSearchResults( { ! isEmpty( filteredBlockTypes ) || ! isEmpty( filteredBlockPatterns ); return ( - <> + { ! showBlockDirectory && ! hasItems && } { !! filteredBlockTypes.length && ( @@ -168,7 +169,7 @@ function InserterSearchResults( { } } ) } - + ); } diff --git a/packages/block-editor/src/components/inserter/search-results.native.js b/packages/block-editor/src/components/inserter/search-results.native.js index ac1cb6cfd61645..6499645f794c41 100644 --- a/packages/block-editor/src/components/inserter/search-results.native.js +++ b/packages/block-editor/src/components/inserter/search-results.native.js @@ -13,11 +13,7 @@ import { * WordPress dependencies */ import { useState, useEffect } from '@wordpress/element'; -import { - BottomSheet, - BottomSheetConsumer, - InserterButton, -} from '@wordpress/components'; +import { BottomSheet, InserterButton } from '@wordpress/components'; /** * Internal dependencies @@ -26,7 +22,13 @@ import styles from './style.scss'; const MIN_COL_NUM = 3; -function InserterSearchResults( { items, onSelect, onClose } ) { +function InserterSearchResults( { + items, + onSelect, + listProps, + safeAreaBottomInset, + searchFormHeight = 0, +} ) { const [ numberOfColumns, setNumberOfColumns ] = useState( MIN_COL_NUM ); const [ itemWidth, setItemWidth ] = useState(); const [ maxWidth, setMaxWidth ] = useState(); @@ -73,51 +75,41 @@ function InserterSearchResults( { items, onSelect, onClose } ) { } return ( - - - - { ( { listProps, safeAreaBottomInset } ) => ( - ( - - - - ) } - keyExtractor={ ( item ) => item.name } - renderItem={ ( { item } ) => ( - - ) } - { ...listProps } - contentContainerStyle={ [ - ...listProps.contentContainerStyle, - { - paddingBottom: - safeAreaBottomInset || - styles.list.paddingBottom, - }, - ] } - /> - ) } - - - + + ( + + + + ) } + keyExtractor={ ( item ) => item.name } + renderItem={ ( { item } ) => ( + + ) } + { ...listProps } + contentContainerStyle={ [ + ...listProps.contentContainerStyle, + { + paddingBottom: + ( safeAreaBottomInset || + styles.list.paddingBottom ) + searchFormHeight, + }, + ] } + /> + ); } diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index 77456c9587d1dd..55063f811cc784 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -19,3 +19,34 @@ .list { padding-bottom: 20; } + +.searchForm { + height: 46px; + border-radius: 8px; + color: $gray-dark; + margin: $grid-unit-30; + background-color: $gray-light; + flex-direction: row; + justify-content: space-between; +} + +.searchFormDark { + background-color: rgba($white, 0.07); +} + +.searchFormInput { + color: $gray-dark; + flex: 2; +} + +.searchFormInputDark { + color: $white; +} + +.searchFormPlaceholder { + color: $gray; +} + +.searchFormPlaceholderDark { + color: rgba($white, 0.8); +} diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index b6ab3828110178..45b0091508ba60 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -325,10 +325,6 @@ $block-inserter-tabs-height: 44px; float: left; } -.block-editor-inserter__quick-inserter .block-editor-inserter__panel-content { - padding: $grid-unit-10; -} - .block-editor-inserter__quick-inserter.has-search .block-editor-inserter__panel-content, .block-editor-inserter__quick-inserter.has-expand .block-editor-inserter__panel-content { padding: $grid-unit-20; diff --git a/packages/block-editor/src/components/inserter/test/fixtures/index.js b/packages/block-editor/src/components/inserter/test/fixtures/index.js index c46620ec156dcf..b900af0b732a06 100644 --- a/packages/block-editor/src/components/inserter/test/fixtures/index.js +++ b/packages/block-editor/src/components/inserter/test/fixtures/index.js @@ -3,6 +3,7 @@ export const categories = [ { slug: 'media', title: 'Media' }, { slug: 'design', title: 'Design' }, { slug: 'widgets', title: 'Widgets' }, + { slug: 'theme', title: 'Theme' }, { slug: 'embed', title: 'Embeds' }, { slug: 'reusable', title: 'Reusable blocks' }, ]; diff --git a/packages/block-editor/src/components/justify-toolbar/README.md b/packages/block-editor/src/components/justify-content-control/README.md similarity index 82% rename from packages/block-editor/src/components/justify-toolbar/README.md rename to packages/block-editor/src/components/justify-content-control/README.md index 74ad57163c5dec..b98ba0d38693cb 100644 --- a/packages/block-editor/src/components/justify-toolbar/README.md +++ b/packages/block-editor/src/components/justify-content-control/README.md @@ -1,6 +1,6 @@ ## Justify Toolbar -The `JustifyToolbar` component renders a toolbar that displays justify options for the selected block. +The `JustifyContentControl` component renders a toolbar that displays justify options for the selected block. This component is used to set the flex justification for the elements in the block, allowing to justify `left`, `center`, `right`, and `space-between`. In comparison, the alignment options are for the block itself. @@ -10,14 +10,14 @@ See the Navigation block for an example usage. ### Usage -Renders an justification toolbar with options. +Renders an justification control with options. ```jsx -import { JustifyToolbar } from '@wordpress/block-editor'; +import { JustifyContentControl } from '@wordpress/block-editor'; const MyJustifyToolbar = ( { attributes, setAttributes } ) => ( - - + { setAttributes( { justfication: next } ); @@ -35,7 +35,7 @@ const MyJustifyToolbar = ( { attributes, setAttributes } ) => ( items-justified-space-between -_Note:_ In this example that we render `JustifyToolbar` as a child of the `BlockControls` component. +_Note:_ In this example that we render `JustifyContentControl` as a child of the `BlockControls` component. ### Props @@ -46,14 +46,6 @@ _Note:_ In this example that we render `JustifyToolbar` as a child of the `Block An array of strings for what controls to show, by default it shows all. - -#### `isCollapsed` -* **Type:** `boolean` -* **Default:** `true` - -Whether to display toolbar options in the dropdown menu. - - #### `onChange` * **Type:** `Function` * **Required:** Yes diff --git a/packages/block-editor/src/components/justify-content-control/index.js b/packages/block-editor/src/components/justify-content-control/index.js new file mode 100644 index 00000000000000..59ade832cca6dd --- /dev/null +++ b/packages/block-editor/src/components/justify-content-control/index.js @@ -0,0 +1,12 @@ +/** + * Internal dependencies + */ +import JustifyContentUI from './ui'; + +export function JustifyContentControl( props ) { + return ; +} + +export function JustifyToolbar( props ) { + return ; +} diff --git a/packages/block-editor/src/components/justify-toolbar/style.scss b/packages/block-editor/src/components/justify-content-control/style.scss similarity index 100% rename from packages/block-editor/src/components/justify-toolbar/style.scss rename to packages/block-editor/src/components/justify-content-control/style.scss diff --git a/packages/block-editor/src/components/justify-toolbar/index.js b/packages/block-editor/src/components/justify-content-control/ui.js similarity index 83% rename from packages/block-editor/src/components/justify-toolbar/index.js rename to packages/block-editor/src/components/justify-content-control/ui.js index 6243bac81b9b81..5cc173bab0c1db 100644 --- a/packages/block-editor/src/components/justify-toolbar/index.js +++ b/packages/block-editor/src/components/justify-content-control/ui.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { ToolbarGroup } from '@wordpress/components'; +import { DropdownMenu, ToolbarGroup } from '@wordpress/components'; import { justifyLeft, justifyCenter, @@ -17,12 +17,14 @@ const icons = { 'space-between': justifySpaceBetween, }; -export function JustifyToolbar( { +function JustifyContentUI( { allowedControls = [ 'left', 'center', 'right', 'space-between' ], isCollapsed = true, onChange, value, popoverProps, + isToolbar, + isToolbarButton = true, } ) { // If the control is already selected we want a click // again on the control to deselect the item, so we @@ -67,17 +69,20 @@ export function JustifyToolbar( { }, ]; + const UIComponent = isToolbar ? ToolbarGroup : DropdownMenu; + const extraProps = isToolbar ? { isCollapsed } : { isToolbarButton }; + return ( - allowedControls.includes( elem.name ) ) } + { ...extraProps } /> ); } -export default JustifyToolbar; +export default JustifyContentUI; diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index c602c4ca8fea72..b460e20755e57e 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -391,6 +391,7 @@ export function MediaPlaceholder( { if ( dropZoneUIOnly || disableMediaButtons ) { if ( dropZoneUIOnly ) { deprecated( 'wp.blockEditor.MediaPlaceholder dropZoneUIOnly prop', { + since: '5.4', alternative: 'disableMediaButtons', } ); } diff --git a/packages/block-editor/src/components/media-replace-flow/index.js b/packages/block-editor/src/components/media-replace-flow/index.js index 06254841297e9a..b96ea2f4bae3db 100644 --- a/packages/block-editor/src/components/media-replace-flow/index.js +++ b/packages/block-editor/src/components/media-replace-flow/index.js @@ -13,7 +13,6 @@ import { FormFileUpload, NavigableMenu, MenuItem, - ToolbarGroup, ToolbarButton, Dropdown, withFilters, @@ -119,17 +118,15 @@ const MediaReplaceFlow = ( { popoverProps={ POPOVER_PROPS } contentClassName="block-editor-media-replace-flow__options" renderToggle={ ( { isOpen, onToggle } ) => ( - - - { name } - - + + { name } + ) } renderContent={ ( { onClose } ) => ( <> diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index aa516640ae7365..b01220d6461aaf 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -61,6 +61,7 @@ function useIsAccessibleToolbar( ref ) { const onlyToolbarItem = hasOnlyToolbarItem( tabbables ); if ( ! onlyToolbarItem ) { deprecated( 'Using custom components as toolbar controls', { + since: '5.6', alternative: 'ToolbarItem or ToolbarButton components', link: 'https://developer.wordpress.org/block-editor/components/toolbar-button/#inside-blockcontrols', diff --git a/packages/block-editor/src/components/plain-text/index.native.js b/packages/block-editor/src/components/plain-text/index.native.js index 468549b448576d..a53fd8aa7f2d06 100644 --- a/packages/block-editor/src/components/plain-text/index.native.js +++ b/packages/block-editor/src/components/plain-text/index.native.js @@ -53,6 +53,10 @@ export default class PlainText extends Component { this._input.focus(); } + blur() { + this._input.blur(); + } + render() { return ( { @@ -21,15 +21,19 @@ const FormatToolbarContainer = ( { inline, anchorRef } ) => { className="block-editor-rich-text__inline-format-toolbar" __unstableSlotName="block-toolbar" > - +
+ + + +
); } // Render regular toolbar return ( - + - +
); }; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js b/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js index d37fe5bfee0f68..b9b0316a46ba02 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar-container.native.js @@ -1,15 +1,15 @@ /** * Internal dependencies */ -import BlockFormatControls from '../block-format-controls'; +import BlockControls from '../block-controls'; import FormatToolbar from './format-toolbar'; const FormatToolbarContainer = () => { // Render regular toolbar return ( - + - + ); }; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.js index b2cf95204da841..a3150e0ba86be8 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar/index.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.js @@ -9,12 +9,7 @@ import { orderBy } from 'lodash'; */ import { __ } from '@wordpress/i18n'; -import { - ToolbarItem, - ToolbarGroup, - DropdownMenu, - Slot, -} from '@wordpress/components'; +import { ToolbarItem, DropdownMenu, Slot } from '@wordpress/components'; import { chevronDown } from '@wordpress/icons'; const POPOVER_PROPS = { @@ -24,41 +19,35 @@ const POPOVER_PROPS = { const FormatToolbar = () => { return ( -
- - { [ 'bold', 'italic', 'link', 'text-color' ].map( - ( format ) => ( - + <> + { [ 'bold', 'italic', 'link', 'text-color' ].map( ( format ) => ( + + ) ) } + + { ( fills ) => + fills.length !== 0 && ( + + { ( toggleProps ) => ( + props ), + 'title' + ) } + popoverProps={ POPOVER_PROPS } + /> + ) } + ) - ) } - - { ( fills ) => - fills.length !== 0 && ( - - { ( toggleProps ) => ( - props - ), - 'title' - ) } - popoverProps={ POPOVER_PROPS } - /> - ) } - - ) - } - - -
+ } + + ); }; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js b/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js index e7be93d4b43100..4e0eec85e0bdb0 100644 --- a/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js +++ b/packages/block-editor/src/components/rich-text/format-toolbar/index.native.js @@ -2,11 +2,11 @@ * WordPress dependencies */ -import { Toolbar, Slot } from '@wordpress/components'; +import { Slot } from '@wordpress/components'; const FormatToolbar = () => { return ( - + <> { [ 'bold', 'italic', 'link' ].map( ( format ) => ( { /> ) ) } - + ); }; diff --git a/packages/block-editor/src/components/rich-text/format-toolbar/style.scss b/packages/block-editor/src/components/rich-text/format-toolbar/style.scss deleted file mode 100644 index 3a6e433ebb44ad..00000000000000 --- a/packages/block-editor/src/components/rich-text/format-toolbar/style.scss +++ /dev/null @@ -1,22 +0,0 @@ -.block-editor-format-toolbar { - .components-dropdown-menu__toggle { - justify-content: center; - } -} - -.show-icon-labels { - .block-editor-format-toolbar { - .components-button.has-icon { - width: auto; - - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - } - } - } -} diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 11da84a07c7fb8..4a4e53b2877b49 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -101,6 +101,7 @@ function getAllowedFormats( { } deprecated( 'wp.blockEditor.RichText formattingControls prop', { + since: '5.4', alternative: 'allowedFormats', } ); @@ -716,6 +717,7 @@ function RichTextWrapper( } deprecated( 'wp.blockEditor.RichText wrapperClassName prop', { + since: '5.4', alternative: 'className prop or create your own wrapper div', } ); diff --git a/packages/block-editor/src/components/rich-text/style.scss b/packages/block-editor/src/components/rich-text/style.scss index 5d55341ce37f79..bbbbe1e9fb3a88 100644 --- a/packages/block-editor/src/components/rich-text/style.scss +++ b/packages/block-editor/src/components/rich-text/style.scss @@ -65,3 +65,26 @@ figcaption.block-editor-rich-text__editable [data-rich-text-placeholder]::before padding-right: $grid-unit-15; } } + +.block-editor-rich-text__inline-format-toolbar-group { + .components-dropdown-menu__toggle { + justify-content: center; + } +} + +.show-icon-labels { + .block-editor-rich-text__inline-format-toolbar-group { + .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); + } + } + } +} diff --git a/packages/block-editor/src/components/use-editor-feature/index.js b/packages/block-editor/src/components/use-editor-feature/index.js index 68f6e9e0cd8f25..81ab094ec7f6c5 100644 --- a/packages/block-editor/src/components/use-editor-feature/index.js +++ b/packages/block-editor/src/components/use-editor-feature/index.js @@ -63,7 +63,7 @@ function blockAttributesMatch( blockAttributes, attributes ) { * Hook that retrieves the setting for the given editor feature. * It works with nested objects using by finding the value at path. * - * @param {string} featurePath The path to the feature. + * @param {string} featurePath The path to the feature. * * @return {any} Returns the value defined for the setting. * @@ -88,7 +88,7 @@ export default function useEditorFeature( featurePath ) { 'supports', '__experimentalSelector', ] ); - if ( isObject( selectors ) ) { + if ( clientId && isObject( selectors ) ) { const blockAttributes = getBlockAttributes( clientId ) || {}; for ( const contextSelector in selectors ) { const { attributes } = selectors[ contextSelector ]; diff --git a/packages/block-editor/src/components/writing-flow/focus-capture.js b/packages/block-editor/src/components/writing-flow/focus-capture.js index 0ad22717b45f99..ff2d5b4bf964bc 100644 --- a/packages/block-editor/src/components/writing-flow/focus-capture.js +++ b/packages/block-editor/src/components/writing-flow/focus-capture.js @@ -13,7 +13,6 @@ import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { getBlockDOMNode } from '../../utils/dom'; import { store as blockEditorStore } from '../../store'; /** @@ -38,6 +37,7 @@ const FocusCapture = forwardRef( isReverse, containerRef, noCapture, + lastFocus, hasMultiSelection, multiSelectionContainer, }, @@ -79,20 +79,7 @@ const FocusCapture = forwardRef( return; } - // If there is a selected block, move focus to the first or last - // tabbable element depending on the direction. - const wrapper = getBlockDOMNode( - selectedClientId, - ref.current.ownerDocument - ); - - if ( isReverse ) { - const tabbables = focus.tabbable.find( wrapper ); - const lastTabbable = last( tabbables ) || wrapper; - lastTabbable.focus(); - } else { - wrapper.focus(); - } + lastFocus.current.focus(); } return ( diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index 03cb5858020789..f09a9507d883d4 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -2,7 +2,6 @@ * External dependencies */ import { find, reverse, first, last } from 'lodash'; -import classnames from 'classnames'; /** * WordPress dependencies @@ -12,7 +11,6 @@ import { computeCaretRect, focus, isHorizontalEdge, - isTextField, isVerticalEdge, placeCaretAtHorizontalEdge, placeCaretAtVerticalEdge, @@ -33,14 +31,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { - isBlockFocusStop, - isInSameBlock, - hasInnerBlocksContext, - isInsideRootBlock, - getBlockDOMNode, - getBlockClientId, -} from '../../utils/dom'; +import { isInSameBlock, getBlockClientId } from '../../utils/dom'; import FocusCapture from './focus-capture'; import useMultiSelection from './use-multi-selection'; import { store as blockEditorStore } from '../../store'; @@ -51,6 +42,16 @@ function getComputedStyle( node ) { return node.ownerDocument.defaultView.getComputedStyle( node ); } +function isFormElement( element ) { + const { tagName } = element; + return ( + tagName === 'INPUT' || + tagName === 'BUTTON' || + tagName === 'SELECT' || + tagName === 'TEXTAREA' + ); +} + /** * Returns true if the element should consider edge navigation upon a keyboard * event of the given directional key code, or false otherwise. @@ -115,12 +116,17 @@ export function getClosestTabbable( targetRect = target.getBoundingClientRect(); } - function isTabCandidate( node, i, array ) { + function isTabCandidate( node ) { // Not a candidate if the node is not tabbable. if ( ! focus.tabbable.isTabbableIndex( node ) ) { return false; } + // Skip focusable elements such as links within content editable nodes. + if ( node.isContentEditable && node.contentEditable !== 'true' ) { + return false; + } + if ( onlyVertical ) { const nodeRect = node.getBoundingClientRect(); @@ -132,48 +138,6 @@ export function getClosestTabbable( } } - // Prefer text fields... - if ( isTextField( node ) ) { - return true; - } - - // ...but settle for block focus stop. - if ( ! isBlockFocusStop( node ) ) { - return false; - } - - // If element contains inner blocks, stop immediately at its focus - // wrapper. - if ( hasInnerBlocksContext( node ) ) { - return true; - } - - // If navigating out of a block (in reverse), don't consider its - // block focus stop. - if ( node.contains( target ) ) { - return false; - } - - // In case of block focus stop, check to see if there's a better - // text field candidate within. - for ( - let offset = 1, nextNode; - ( nextNode = array[ i + offset ] ); - offset++ - ) { - // Abort if no longer testing descendents of focus stop. - if ( ! node.contains( nextNode ) ) { - break; - } - - // Apply same tests by recursion. This is important to consider - // nestable blocks where we don't want to settle for the inner - // block focus stop. - if ( isTabCandidate( nextNode, i + offset, array ) ) { - return false; - } - } - return true; } @@ -191,7 +155,6 @@ function selector( select ) { getLastMultiSelectedBlockClientId, hasMultiSelection, getBlockOrder, - isNavigationMode, isSelectionEnabled, getBlockSelectionStart, isMultiSelecting, @@ -217,7 +180,6 @@ function selector( select ) { hasMultiSelection: hasMultiSelection(), firstBlock: first( blocks ), lastBlock: last( blocks ), - isNavigationMode: isNavigationMode(), isSelectionEnabled: isSelectionEnabled(), blockSelectionStart: getBlockSelectionStart(), isMultiSelecting: isMultiSelecting(), @@ -261,7 +223,6 @@ export default function WritingFlow( { children } ) { hasMultiSelection, firstBlock, lastBlock, - isNavigationMode, isSelectionEnabled, blockSelectionStart, isMultiSelecting, @@ -274,20 +235,6 @@ export default function WritingFlow( { children } ) { function onMouseDown( event ) { verticalRect.current = null; - const { ownerDocument } = event.target; - - // Clicking inside a selected block should exit navigation mode and block moving mode. - if ( - isNavigationMode && - selectedBlockClientId && - isInsideRootBlock( - getBlockDOMNode( selectedBlockClientId, ownerDocument ), - event.target - ) - ) { - setNavigationMode( false ); - } - // Multi-select blocks when Shift+clicking. if ( isSelectionEnabled && @@ -398,31 +345,29 @@ export default function WritingFlow( { children } ) { // Navigation mode (press Esc), to navigate through blocks. if ( selectedBlockClientId ) { if ( isTab ) { - const wrapper = getBlockDOMNode( - selectedBlockClientId, - ownerDocument - ); - - if ( isShift ) { - if ( target === wrapper ) { - // Disable focus capturing on the focus capture element, so - // it doesn't refocus this block and so it allows default - // behaviour (moving focus to the next tabbable element). - noCapture.current = true; - focusCaptureBeforeRef.current.focus(); - return; - } - } else { - const tabbables = focus.tabbable.find( wrapper ); - const lastTabbable = last( tabbables ) || wrapper; - - if ( target === lastTabbable ) { - // See comment above. - noCapture.current = true; - focusCaptureAfterRef.current.focus(); - return; - } + const direction = isShift ? 'findPrevious' : 'findNext'; + // Allow tabbing between form elements rendered in a block, + // such as inside a placeholder. Form elements are generally + // meant to be UI rather than part of the content. Ideally + // these are not rendered in the content and perhaps in the + // future they can be rendered in an iframe or shadow DOM. + if ( + isFormElement( target ) && + isFormElement( focus.tabbable[ direction ]( target ) ) + ) { + return; } + + const next = isShift + ? focusCaptureBeforeRef + : focusCaptureAfterRef; + + // Disable focus capturing on the focus capture element, so it + // doesn't refocus this block and so it allows default behaviour + // (moving focus to the next tabbable element). + noCapture.current = true; + next.current.focus(); + return; } else if ( isEscape ) { setNavigationMode( true ); } @@ -573,9 +518,18 @@ export default function WritingFlow( { children } ) { } }, [ hasMultiSelection, isMultiSelecting ] ); - const className = classnames( 'block-editor-writing-flow', { - 'is-navigate-mode': isNavigationMode, - } ); + const lastFocus = useRef(); + + useEffect( () => { + function onFocusOut( event ) { + lastFocus.current = event.target; + } + + container.current.addEventListener( 'focusout', onFocusOut ); + return () => { + container.current.removeEventListener( 'focusout', onFocusOut ); + }; + }, [] ); // Disable reason: Wrapper itself is non-interactive, but must capture // bubbling events from children to determine focus transition intents. @@ -587,6 +541,7 @@ export default function WritingFlow( { children } ) { selectedClientId={ selectedBlockClientId } containerRef={ container } noCapture={ noCapture } + lastFocus={ lastFocus } hasMultiSelection={ hasMultiSelection } multiSelectionContainer={ multiSelectionContainer } /> @@ -605,7 +560,7 @@ export default function WritingFlow( { children } ) { />
@@ -616,6 +571,7 @@ export default function WritingFlow( { children } ) { selectedClientId={ selectedBlockClientId } containerRef={ container } noCapture={ noCapture } + lastFocus={ lastFocus } hasMultiSelection={ hasMultiSelection } multiSelectionContainer={ multiSelectionContainer } isReverse diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 41987c43a6fe75..34e1d3ffe94611 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -14,13 +14,12 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { BlockControls, BlockAlignmentToolbar } from '../components'; -import { store as blockEditorStore } from '../store'; +import { BlockControls, BlockAlignmentControl } from '../components'; +import useAvailableAlignments from '../components/block-alignment-control/use-available-alignments'; /** * An array which includes all possible valid alignments, @@ -116,14 +115,17 @@ export function addAttribute( settings ) { export const withToolbarControls = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name: blockName } = props; - // Compute valid alignments without taking into account, + // Compute the block allowed alignments without taking into account, // if the theme supports wide alignments or not // and without checking the layout for availble alignments. // BlockAlignmentToolbar takes both of these into account. - const validAlignments = getValidAlignments( + const blockAllowedAlignments = getValidAlignments( getBlockSupport( blockName, 'align' ), hasBlockSupport( blockName, 'alignWide', true ) ); + const validAlignments = useAvailableAlignments( + blockAllowedAlignments + ); const updateAlignment = ( nextAlign ) => { if ( ! nextAlign ) { @@ -138,8 +140,8 @@ export const withToolbarControls = createHigherOrderComponent( return [ validAlignments.length > 0 && props.isSelected && ( - - + ( props ) => { const { name, attributes } = props; const { align } = attributes; - const hasWideEnabled = useSelect( - ( select ) => !! select( blockEditorStore ).getSettings().alignWide, - [] + const blockAllowedAlignments = getValidAlignments( + getBlockSupport( name, 'align' ), + hasBlockSupport( name, 'alignWide', true ) + ); + const validAlignments = useAvailableAlignments( + blockAllowedAlignments ); // If an alignment is not assigned, there's no need to go through the @@ -173,12 +178,6 @@ export const withDataAlign = createHigherOrderComponent( return ; } - const validAlignments = getValidAlignments( - getBlockSupport( name, 'align' ), - hasBlockSupport( name, 'alignWide', true ), - hasWideEnabled - ); - let wrapperProps = props.wrapperProps; if ( validAlignments.includes( align ) ) { wrapperProps = { ...wrapperProps, 'data-align': align }; diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 8a106d7cdbf24e..d2b96e89891181 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -89,9 +89,7 @@ export const withInspectorControl = createHigherOrderComponent( } value={ props.attributes.anchor || '' } - valuePlaceholder={ - ! isWeb ? __( 'Add an anchor' ) : null - } + placeholder={ ! isWeb ? __( 'Add an anchor' ) : null } onChange={ ( nextValue ) => { nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); props.setAttributes( { diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 6aa2b76fc1d1f5..c087e994ba9eb9 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -8,3 +8,4 @@ import './generated-class-name'; import './style'; import './color'; import './font-size'; +import './layout'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js new file mode 100644 index 00000000000000..4a4a6ee7ffd240 --- /dev/null +++ b/packages/block-editor/src/hooks/layout.js @@ -0,0 +1,234 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +import { has } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Platform } from '@wordpress/element'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; +import { hasBlockSupport } from '@wordpress/blocks'; +import { useSelect } from '@wordpress/data'; +import { + ToggleControl, + PanelBody, + __experimentalUnitControl as UnitControl, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { Icon, positionCenter, stretchWide } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { InspectorControls } from '../components'; +import useEditorFeature from '../components/use-editor-feature'; +import { LayoutStyle } from '../components/block-list/layout'; + +const isWeb = Platform.OS === 'web'; +const CSS_UNITS = [ + { + value: '%', + label: isWeb ? '%' : __( 'Percentage (%)' ), + default: '', + }, + { + value: 'px', + label: isWeb ? 'px' : __( 'Pixels (px)' ), + default: '', + }, + { + value: 'em', + label: isWeb ? 'em' : __( 'Relative to parent font size (em)' ), + default: '', + }, + { + value: 'rem', + label: isWeb ? 'rem' : __( 'Relative to root font size (rem)' ), + default: '', + }, + { + value: 'vw', + label: isWeb ? 'vw' : __( 'Viewport width (vw)' ), + default: '', + }, +]; + +function LayoutPanel( { setAttributes, attributes } ) { + const { layout = {} } = attributes; + const { wideSize, contentSize, inherit = false } = layout; + const defaultLayout = useEditorFeature( 'layout' ); + const themeSupportsLayout = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return getSettings().supportsLayout; + }, [] ); + + if ( ! themeSupportsLayout ) { + return null; + } + return ( + + + { !! defaultLayout && ( + + setAttributes( { layout: { inherit: ! inherit } } ) + } + /> + ) } + { ! inherit && ( +
+
+ { + nextWidth = + 0 > parseFloat( nextWidth ) + ? '0' + : nextWidth; + setAttributes( { + layout: { + ...layout, + contentSize: nextWidth, + }, + } ); + } } + units={ CSS_UNITS } + /> + +
+
+ { + nextWidth = + 0 > parseFloat( nextWidth ) + ? '0' + : nextWidth; + setAttributes( { + layout: { + ...layout, + wideSize: nextWidth, + }, + } ); + } } + units={ CSS_UNITS } + /> + +
+
+ ) } +

+ { __( + 'Customize the width for all elements that are assigned to the center or wide columns.' + ) } +

+
+
+ ); +} + +/** + * Filters registered block settings, extending attributes to include `layout`. + * + * @param {Object} settings Original block settings + * @return {Object} Filtered block settings + */ +export function addAttribute( settings ) { + if ( has( settings.attributes, [ 'layout', 'type' ] ) ) { + return settings; + } + if ( hasBlockSupport( settings, '__experimentalLayout' ) ) { + settings.attributes = { + ...settings.attributes, + layout: { + type: 'object', + }, + }; + } + + return settings; +} + +/** + * Override the default edit UI to include layout controls + * + * @param {Function} BlockEdit Original component + * @return {Function} Wrapped component + */ +export const withInspectorControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { + const { name: blockName } = props; + const supportLayout = hasBlockSupport( + blockName, + '__experimentalLayout' + ); + + return [ + supportLayout && , + , + ]; + }, + 'withInspectorControls' +); + +/** + * Override the default block element to add the layout styles. + * + * @param {Function} BlockListBlock Original component + * @return {Function} Wrapped component + */ +export const withLayoutStyles = createHigherOrderComponent( + ( BlockListBlock ) => ( props ) => { + const { name, attributes } = props; + const supportLayout = hasBlockSupport( name, '__experimentalLayout' ); + const id = useInstanceId( BlockListBlock ); + const defaultLayout = useEditorFeature( 'layout' ) || {}; + if ( ! supportLayout ) { + return ; + } + const { layout = {} } = attributes; + const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const className = classnames( + props?.className, + `wp-container-${ id }` + ); + + return ( + <> + + + + ); + } +); + +addFilter( + 'blocks.registerBlockType', + 'core/layout/addAttribute', + addAttribute +); +addFilter( + 'editor.BlockListBlock', + 'core/editor/layout/with-layout-styles', + withLayoutStyles +); +addFilter( + 'editor.BlockEdit', + 'core/editor/layout/with-inspector-controls', + withInspectorControls +); diff --git a/packages/block-editor/src/hooks/layout.scss b/packages/block-editor/src/hooks/layout.scss new file mode 100644 index 00000000000000..1c4773a8889ecf --- /dev/null +++ b/packages/block-editor/src/hooks/layout.scss @@ -0,0 +1,17 @@ +.block-editor-hooks__layout-controls { + display: flex; + margin-bottom: $grid-unit-30; + + .block-editor-hooks__layout-controls-unit { + display: flex; + margin-right: $grid-unit-30; + + svg { + margin: auto 0 $grid-unit-05 $grid-unit-10; + } + } +} + +.block-editor-hooks__layout-controls-helptext { + font-size: $helptext-font-size; +} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 702da2ae7983f0..b1c7239b2d791d 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -128,6 +128,7 @@ export function addSaveProps( props, blockType, attributes ) { const { style } = attributes; const filteredStyle = omitKeysNotToSerialize( style, { + border: getBlockSupport( blockType, BORDER_SUPPORT_KEY ), [ COLOR_SUPPORT_KEY ]: getBlockSupport( blockType, COLOR_SUPPORT_KEY ), } ); props.style = { diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index 7fd0555881e346..aad215e14ae50c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -574,6 +574,8 @@ export function* insertBlocks( meta = initialPosition; initialPosition = 0; deprecated( "meta argument in wp.data.dispatch('core/block-editor')", { + since: '10.1', + plugin: 'Gutenberg', hint: 'The meta argument is now the 6th argument of the function', } ); } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index ba608d34a35f87..84b1a2a9a49c7c 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -12,6 +12,7 @@ export const PREFERENCES_DEFAULTS = { * * @typedef {Object} SETTINGS_DEFAULT * @property {boolean} alignWide Enable/Disable Wide/Full Alignments + * @property {boolean} supportsLayout Enable/disable layouts support in container blocks. * @property {Array} availableLegacyWidgets Array of objects representing the legacy widgets available. * @property {boolean} imageEditing Image Editing settings set to false to disable. * @property {Array} imageSizes Available image sizes @@ -31,6 +32,8 @@ export const PREFERENCES_DEFAULTS = { */ export const SETTINGS_DEFAULTS = { alignWide: false, + supportsLayout: true, + // colors setting is not used anymore now defaults are passed from theme.json on the server and core has its own defaults. // The setting is only kept for backward compatibility purposes. colors: [ @@ -121,6 +124,9 @@ export const SETTINGS_DEFAULTS = { }, ], + // Image default size slug. + imageDefaultSize: 'large', + imageSizes: [ { slug: 'thumbnail', name: __( 'Thumbnail' ) }, { slug: 'medium', name: __( 'Medium' ) }, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 1e6e1a2073ae3f..d3dcb13a583149 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -15,7 +15,6 @@ import { filter, mapKeys, orderBy, - every, } from 'lodash'; import createSelector from 'rememo'; @@ -265,6 +264,41 @@ export const __unstableGetBlockTree = createSelector( ] ); +/** + * Returns a stripped down block object containing only its client ID, + * and its inner blocks' client IDs. + * + * @param {Object} state Editor state. + * @param {string} clientId Client ID of the block to get. + * + * @return {Object} Client IDs of the post blocks. + */ +export const __unstableGetClientIdWithClientIdsTree = createSelector( + ( state, clientId ) => ( { + clientId, + innerBlocks: __unstableGetClientIdsTree( state, clientId ), + } ), + ( state ) => [ state.blocks.order ] +); + +/** + * Returns the block tree represented in the block-editor store from the + * given root, consisting of stripped down block objects containing only + * their client IDs, and their inner blocks' client IDs. + * + * @param {Object} state Editor state. + * @param {?string} rootClientId Optional root client ID of block list. + * + * @return {Object[]} Client IDs of the post blocks. + */ +export const __unstableGetClientIdsTree = createSelector( + ( state, rootClientId = '' ) => + map( getBlockOrder( state, rootClientId ), ( clientId ) => + __unstableGetClientIdWithClientIdsTree( state, clientId ) + ), + ( state ) => [ state.blocks.order ] +); + /** * Returns an array containing the clientIds of all descendants * of the blocks given. @@ -1757,13 +1791,18 @@ export const __experimentalGetAllowedBlocks = createSelector( ] ); -const __experimentalGetParsedPatterns = createSelector( - ( state ) => { +export const __experimentalGetParsedPattern = createSelector( + ( state, patternName ) => { const patterns = state.settings.__experimentalBlockPatterns; - return map( patterns, ( pattern ) => ( { + const pattern = patterns.find( ( { name } ) => name === patternName ); + if ( ! pattern ) { + return null; + } + + return { ...pattern, - contentBlocks: parse( pattern.content ), - } ) ); + blocks: parse( pattern.content ), + }; }, ( state ) => [ state.settings.__experimentalBlockPatterns ] ); @@ -1778,17 +1817,19 @@ const __experimentalGetParsedPatterns = createSelector( */ export const __experimentalGetAllowedPatterns = createSelector( ( state, rootClientId = null ) => { - const patterns = __experimentalGetParsedPatterns( state ); - + const patterns = state.settings.__experimentalBlockPatterns; if ( ! rootClientId ) { return patterns; } - const patternsAllowed = filter( patterns, ( { contentBlocks } ) => { - return every( contentBlocks, ( { name } ) => + const parsedPatterns = patterns.map( ( { name } ) => + __experimentalGetParsedPattern( state, name ) + ); + const patternsAllowed = filter( parsedPatterns, ( { blocks } ) => + blocks.every( ( { name } ) => canInsertBlockType( state, name, rootClientId ) - ); - } ); + ) + ); return patternsAllowed; }, diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 3d2e058dd844f6..167c830f510366 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -73,6 +73,8 @@ const { __experimentalGetParsedReusableBlock, __experimentalGetAllowedPatterns, __experimentalGetScopedBlockPatterns, + __unstableGetClientIdWithClientIdsTree, + __unstableGetClientIdsTree, } = selectors; describe( 'selectors', () => { @@ -3375,10 +3377,12 @@ describe( 'selectors', () => { settings: { __experimentalBlockPatterns: [ { + name: 'pattern-a', title: 'pattern with a', content: ``, }, { + name: 'pattern-b', title: 'pattern with b', content: '', @@ -3409,10 +3413,12 @@ describe( 'selectors', () => { settings: { __experimentalBlockPatterns: [ { + name: 'pattern-a', title: 'pattern a', scope: { block: [ 'test/block-a' ] }, }, { + name: 'pattern-b', title: 'pattern b', scope: { block: [ 'test/block-b' ] }, }, @@ -3535,3 +3541,76 @@ describe( 'getInserterItems with core blocks prioritization', () => { expect( items.map( ( { name } ) => name ) ).toEqual( expectedResult ); } ); } ); + +describe( '__unstableGetClientIdWithClientIdsTree', () => { + it( "should return a stripped down block object containing only its client ID and its inner blocks' client IDs", () => { + const state = { + blocks: { + order: { + '': [ 'foo' ], + foo: [ 'bar', 'baz' ], + bar: [ 'qux' ], + }, + }, + }; + + expect( + __unstableGetClientIdWithClientIdsTree( state, 'foo' ) + ).toEqual( { + clientId: 'foo', + innerBlocks: [ + { + clientId: 'bar', + innerBlocks: [ { clientId: 'qux', innerBlocks: [] } ], + }, + { clientId: 'baz', innerBlocks: [] }, + ], + } ); + } ); +} ); +describe( '__unstableGetClientIdsTree', () => { + it( "should return the full content tree starting from the given root, consisting of stripped down block object containing only its client ID and its inner blocks' client IDs", () => { + const state = { + blocks: { + order: { + '': [ 'foo' ], + foo: [ 'bar', 'baz' ], + bar: [ 'qux' ], + }, + }, + }; + + expect( __unstableGetClientIdsTree( state, 'foo' ) ).toEqual( [ + { + clientId: 'bar', + innerBlocks: [ { clientId: 'qux', innerBlocks: [] } ], + }, + { clientId: 'baz', innerBlocks: [] }, + ] ); + } ); + + it( "should return the full content tree starting from the root, consisting of stripped down block object containing only its client ID and its inner blocks' client IDs", () => { + const state = { + blocks: { + order: { + '': [ 'foo' ], + foo: [ 'bar', 'baz' ], + bar: [ 'qux' ], + }, + }, + }; + + expect( __unstableGetClientIdsTree( state ) ).toEqual( [ + { + clientId: 'foo', + innerBlocks: [ + { + clientId: 'bar', + innerBlocks: [ { clientId: 'qux', innerBlocks: [] } ], + }, + { clientId: 'baz', innerBlocks: [] }, + ], + }, + ] ); + } ); +} ); diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 9469ed8cdf9096..f80eeefd3be42d 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -6,7 +6,7 @@ // Only add styles for components that are used inside the editing canvas here: @import "./autocompleters/style.scss"; -@import "./components/block-alignment-matrix-toolbar/style.scss"; +@import "./components/block-alignment-matrix-control/style.scss"; @import "./components/block-icon/style.scss"; @import "./components/block-inspector/style.scss"; @import "./components/block-list/style.scss"; @@ -32,7 +32,7 @@ @import "./components/contrast-checker/style.scss"; @import "./components/default-block-appender/style.scss"; @import "./components/font-appearance-control/style.scss"; -@import "./components/justify-toolbar/style.scss"; +@import "./components/justify-content-control/style.scss"; @import "./components/link-control/style.scss"; @import "./components/line-height-control/style.scss"; @import "./components/image-size-control/style.scss"; @@ -43,7 +43,6 @@ @import "./components/multi-selection-inspector/style.scss"; @import "./components/plain-text/style.scss"; @import "./components/responsive-block-control/style.scss"; -@import "./components/rich-text/format-toolbar/style.scss"; @import "./components/rich-text/style.scss"; @import "./components/skip-to-selected-block/style.scss"; @import "./components/text-decoration-and-transform/style.scss"; @@ -54,6 +53,7 @@ @import "./components/url-popover/style.scss"; @import "./components/warning/style.scss"; @import "./hooks/anchor.scss"; +@import "./hooks/layout.scss"; // This tag marks the end of the styles that apply to editing canvas contents and need to be manipulated when we resize the editor. #end-resizable-editor-section { diff --git a/packages/block-editor/src/utils/dom.js b/packages/block-editor/src/utils/dom.js index 4f9c971ccd0771..248587311d1d2b 100644 --- a/packages/block-editor/src/utils/dom.js +++ b/packages/block-editor/src/utils/dom.js @@ -31,18 +31,6 @@ export function getBlockPreviewContainerDOMNode( clientId, doc ) { return domNode.firstChild || domNode; } -/** - * Returns true if the given element is a block focus stop. Blocks without their - * own text fields rely on the focus stop to be keyboard navigable. - * - * @param {Element} element Element to test. - * - * @return {boolean} Whether element is a block focus stop. - */ -export function isBlockFocusStop( element ) { - return element.classList.contains( 'block-editor-block-list__block' ); -} - /** * Returns true if two elements are contained within the same block. * @@ -73,21 +61,6 @@ export function isInsideRootBlock( blockElement, element ) { return parentBlock === blockElement; } -/** - * Returns true if the given element contains inner blocks (an InnerBlocks - * element). - * - * @param {Element} element Element to test. - * - * @return {boolean} Whether element contains inner blocks. - */ -export function hasInnerBlocksContext( element ) { - return ( - element.classList.contains( 'block-editor-block-list__layout' ) || - !! element.querySelector( '.block-editor-block-list__layout' ) - ); -} - /** * Finds the block client ID given any DOM node inside the block. * diff --git a/packages/block-editor/src/utils/pre-parse-patterns.js b/packages/block-editor/src/utils/pre-parse-patterns.js new file mode 100644 index 00000000000000..c5e1d5ad41ac59 --- /dev/null +++ b/packages/block-editor/src/utils/pre-parse-patterns.js @@ -0,0 +1,64 @@ +/** + * WordPress dependencies + */ +import { useSelect, select } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; + +const requestIdleCallback = ( () => { + if ( typeof window === 'undefined' ) { + return ( callback ) => { + setTimeout( () => callback( Date.now() ), 0 ); + }; + } + + return window.requestIdleCallback || window.requestAnimationFrame; +} )(); + +const cancelIdleCallback = ( () => { + if ( typeof window === 'undefined' ) { + return clearTimeout; + } + + return window.cancelIdleCallback || window.cancelAnimationFrame; +} )(); + +export function usePreParsePatterns() { + const patterns = useSelect( + ( _select ) => + _select( blockEditorStore ).getSettings() + .__experimentalBlockPatterns, + [] + ); + + useEffect( () => { + if ( ! patterns?.length ) { + return; + } + + let handle; + let index = -1; + + const callback = () => { + index++; + if ( index >= patterns.length ) { + return; + } + + select( blockEditorStore ).__experimentalGetParsedPattern( + patterns[ index ].name + ); + + handle = requestIdleCallback( callback ); + }; + + handle = requestIdleCallback( callback ); + return () => cancelIdleCallback( handle ); + }, [ patterns ] ); + + return null; +} diff --git a/packages/block-editor/src/utils/test/dom.js b/packages/block-editor/src/utils/test/dom.js deleted file mode 100644 index b755f74bcb899f..00000000000000 --- a/packages/block-editor/src/utils/test/dom.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Internal dependencies - */ -import { hasInnerBlocksContext } from '../dom'; - -describe( 'hasInnerBlocksContext()', () => { - it( 'should return false for a block node which has no inner blocks', () => { - const wrapper = document.createElement( 'div' ); - wrapper.innerHTML = - '
' + - '
' + - '

This is a test.

' + - '
' + - '
'; - - const blockNode = wrapper.firstChild; - expect( hasInnerBlocksContext( blockNode ) ).toBe( false ); - } ); - - it( 'should return true for a block node which contains inner blocks', () => { - const wrapper = document.createElement( 'div' ); - wrapper.innerHTML = - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
' + - '
'; - - const blockNode = wrapper.firstChild; - expect( hasInnerBlocksContext( blockNode ) ).toBe( true ); - } ); -} ); diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index fda47146463f28..28a165ac9c8d7b 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -30,6 +30,11 @@ - Fix a regression where the Cover would not show opacity controls for the default overlay color ([#26625](https://github.com/WordPress/gutenberg/pull/26625)). - Fix a regression ([#26545](https://github.com/WordPress/gutenberg/pull/26545)) where the Cover block lost its default background overlay color ([#26569](https://github.com/WordPress/gutenberg/pull/26569)). +- Fix Image Block, reset image dimensions when replace URL. bug mentioned in ([#26333](https://github.com/WordPress/gutenberg/issues/26333)). + +### Enhancement + +- File Block: Copy url button is moved to Block toolbar. ## 2.23.0 (2020-09-03) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index bacdc4c35b7e60..f238c2edafce7c 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -26,7 +26,7 @@ "src/**/*.scss" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/autop": "file:../autop", diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index 491b467ebb381f..c691b72fcabbde 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -134,7 +134,7 @@ function AudioEdit( { return ( <> - + diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index f9a03d89f00937..9b33ee27d6c418 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -62,7 +62,8 @@ "align": true, "alignWide": false, "color": { - "__experimentalSkipSerialization": true + "__experimentalSkipSerialization": true, + "gradients": true }, "reusable": false, "__experimentalSelector": ".wp-block-button__link" diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 5925a07b089208..b0fcc297d6b9ed 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -16,7 +16,6 @@ import { RangeControl, TextControl, ToolbarButton, - ToolbarGroup, Popover, } from '@wordpress/components'; import { @@ -147,28 +146,26 @@ function URLPicker( { ); return ( <> - - - { ! urlIsSet && ( - - ) } - { urlIsSetandSelected && ( - - ) } - + + { ! urlIsSet && ( + + ) } + { urlIsSetandSelected && ( + + ) } { isSelected && ( @@ -426,6 +427,12 @@ class ButtonEdit extends Component { onChange={ this.onChangeText } style={ { ...richTextStyle.richText, + paddingLeft: isFixedWidth + ? 0 + : richTextStyle.richText.paddingLeft, + paddingRight: isFixedWidth + ? 0 + : richTextStyle.richText.paddingRight, color: textColor, } } textAlign={ align } @@ -435,7 +442,7 @@ class ButtonEdit extends Component { identifier="text" tagName="p" minWidth={ minWidth } // The minimum Button size. - maxWidth={ maxWidth } // The width of the screen. + maxWidth={ isFixedWidth ? minWidth : maxWidth } // The width of the screen. id={ clientId } isSelected={ isButtonFocused } withoutInteractiveFormatting diff --git a/packages/block-library/src/button/rich-text.ios.scss b/packages/block-library/src/button/rich-text.ios.scss index 27ec62345fa266..6ac8c973d67f53 100644 --- a/packages/block-library/src/button/rich-text.ios.scss +++ b/packages/block-library/src/button/rich-text.ios.scss @@ -1,4 +1,6 @@ .richText { margin: 10px $grid-unit-20; + padding-left: 0; + padding-right: 0; background-color: transparent; } diff --git a/packages/block-library/src/button/save.js b/packages/block-library/src/button/save.js index a5899d6e67d6c0..8a224cbc841121 100644 --- a/packages/block-library/src/button/save.js +++ b/packages/block-library/src/button/save.js @@ -23,6 +23,11 @@ export default function save( { attributes, className } ) { url, width, } = attributes; + + if ( ! text ) { + return null; + } + const colorProps = getColorAndStyleProps( attributes ); const buttonClasses = classnames( 'wp-block-button__link', @@ -45,19 +50,17 @@ export default function save( { attributes, className } ) { } ); return ( - text && ( -
- -
- ) +
+ +
); } diff --git a/packages/block-library/src/buttons/edit.js b/packages/block-library/src/buttons/edit.js index 886f89d40d14ec..7b1243ccb47da6 100644 --- a/packages/block-library/src/buttons/edit.js +++ b/packages/block-library/src/buttons/edit.js @@ -10,7 +10,7 @@ import { BlockControls, useBlockProps, __experimentalUseInnerBlocksProps as useInnerBlocksProps, - JustifyToolbar, + JustifyContentControl, } from '@wordpress/block-editor'; /** @@ -49,8 +49,8 @@ function ButtonsEdit( { return ( <> - - + diff --git a/packages/block-library/src/buttons/edit.native.js b/packages/block-library/src/buttons/edit.native.js index 484ea35ae150da..8b9c1745ce1b3f 100644 --- a/packages/block-library/src/buttons/edit.native.js +++ b/packages/block-library/src/buttons/edit.native.js @@ -10,7 +10,7 @@ import { View } from 'react-native'; import { BlockControls, InnerBlocks, - JustifyToolbar, + JustifyContentControl, store as blockEditorStore, } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; @@ -121,8 +121,8 @@ export default function ButtonsEdit( { return ( <> { isSelected && ( - - + diff --git a/packages/block-library/src/columns/edit.native.js b/packages/block-library/src/columns/edit.native.js index 59560192bf87f3..241e5fe247e358 100644 --- a/packages/block-library/src/columns/edit.native.js +++ b/packages/block-library/src/columns/edit.native.js @@ -189,6 +189,7 @@ function ColumnsEditContainer( { return ( - } + labels={ { + title: __( 'Cover' ), + instructions: __( + 'Upload an image or video file, or pick one from your media library.' + ), + } } + onSelect={ onSelectMedia } + accept="image/*,video/*" + allowedTypes={ ALLOWED_MEDIA_TYPES } + notices={ noticeUI } + disableMediaButtons={ hasBackground } + onError={ ( message ) => { + removeAllNotices(); + createErrorNotice( message ); + } } + > + { children } + + ); +} + function CoverEdit( { attributes, isSelected, @@ -329,7 +361,6 @@ function CoverEdit( { const isVideoBackground = VIDEO_BACKGROUND_TYPE === backgroundType; const [ temporaryMinHeight, setTemporaryMinHeight ] = useState( null ); - const { removeAllNotices, createErrorNotice } = noticeOperations; const minHeightWithUnit = minHeightUnit ? `${ minHeight }${ minHeightUnit }` @@ -368,8 +399,8 @@ function CoverEdit( { const controls = ( <> - - + @@ -379,11 +410,13 @@ function CoverEdit( { } isDisabled={ ! hasBackground } /> - + + ; - const label = __( 'Cover' ); - return ( <> { controls } @@ -520,22 +550,10 @@ function CoverEdit( { blockProps.className ) } > - { - removeAllNotices(); - createErrorNotice( message ); - } } +
-
+
); @@ -627,6 +645,12 @@ function CoverEdit( { /> ) } { isBlogUrl && } +
diff --git a/packages/block-library/src/cover/edit.native.js b/packages/block-library/src/cover/edit.native.js index f003ea7b6d800b..7a7ef4b0c4117c 100644 --- a/packages/block-library/src/cover/edit.native.js +++ b/packages/block-library/src/cover/edit.native.js @@ -26,7 +26,6 @@ import { ImageEditingButton, IMAGE_DEFAULT_FOCAL_POINT, ToolbarButton, - ToolbarGroup, Gradient, ColorPalette, ColorPicker, @@ -265,14 +264,12 @@ const Cover = ( { const placeholderIcon = ; const toolbarControls = ( open ) => ( - - - - + + ); diff --git a/packages/block-library/src/cover/editor.scss b/packages/block-library/src/cover/editor.scss index b6d6aae7e92ec9..899cf8f84f7f0c 100644 --- a/packages/block-library/src/cover/editor.scss +++ b/packages/block-library/src/cover/editor.scss @@ -65,3 +65,43 @@ // Important is used to have higher specificity than the inline style set by re-resizable library. height: auto !important; } + +// Only target direct dropzone. +.wp-block-cover > .components-drop-zone { + &.is-active { + transition: 0.2s opacity, 0.2s border; + @include reduce-motion("transition"); + } + + &.is-dragging-over-element { + background-color: transparent; + border: $grid-unit-60 solid var(--wp-admin-theme-color); + + .components-drop-zone__content { + transform: none; + } + } + + .components-drop-zone__content { + display: flex; + align-items: center; + top: -($grid-unit-15 * 3); + left: -($grid-unit-15 * 3); + transform: none; + } + + .components-drop-zone__content-icon, + .components-drop-zone__content-text { + display: inline; + } + + .components-drop-zone__content-icon { + // Reset margin. + margin: 0; + margin-right: $grid-unit; + } + + .components-drop-zone__content-text { + font-size: $default-font-size; + } +} diff --git a/packages/block-library/src/cover/focal-point-settings.native.js b/packages/block-library/src/cover/focal-point-settings-button.native.js similarity index 93% rename from packages/block-library/src/cover/focal-point-settings.native.js rename to packages/block-library/src/cover/focal-point-settings-button.native.js index 9d268e18f237f1..d9451831a6a2f5 100644 --- a/packages/block-library/src/cover/focal-point-settings.native.js +++ b/packages/block-library/src/cover/focal-point-settings-button.native.js @@ -17,7 +17,7 @@ import { chevronRight } from '@wordpress/icons'; */ import styles from './style.scss'; -function FocalPointSettings( { +function FocalPointSettingsButton( { disabled, focalPoint, onFocalPointChange, @@ -50,4 +50,4 @@ function FocalPointSettings( { ); } -export default FocalPointSettings; +export default FocalPointSettingsButton; diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index 4ff3ee319d800d..41234b75cca7ec 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -21,7 +21,6 @@ @import "./image/editor.scss"; @import "./latest-comments/editor.scss"; @import "./latest-posts/editor.scss"; -@import "./list/editor.scss"; @import "./media-text/editor.scss"; @import "./more/editor.scss"; @import "./navigation/editor.scss"; @@ -48,11 +47,13 @@ @import "./text-columns/editor.scss"; @import "./verse/editor.scss"; @import "./video/editor.scss"; +@import "./query-title/editor.scss"; @import "./query-loop/editor.scss"; @import "./query/editor.scss"; @import "./query-pagination/editor.scss"; @import "./query-pagination-numbers/editor.scss"; @import "./post-featured-image/editor.scss"; +@import "./term-description/editor.scss"; :root .editor-styles-wrapper { // Background colors. diff --git a/packages/block-library/src/embed/test/index.js b/packages/block-library/src/embed/test/index.js index 02cbfe9a583b29..9ef7082f913570 100644 --- a/packages/block-library/src/embed/test/index.js +++ b/packages/block-library/src/embed/test/index.js @@ -56,19 +56,19 @@ describe( 'utils', () => { } ); } ); describe( 'getClassNames', () => { - test( 'getClassNames returns aspect ratio class names for iframes with width and height', () => { + it( 'should return aspect ratio class names for iframes with width and height', () => { const html = ''; const expected = 'wp-embed-aspect-16-9 wp-has-aspect-ratio'; expect( getClassNames( html ) ).toEqual( expected ); } ); - test( 'getClassNames does not return aspect ratio class names if we do not allow responsive', () => { + it( 'should not return aspect ratio class names if we do not allow responsive', () => { const html = ''; const expected = ''; expect( getClassNames( html, '', false ) ).toEqual( expected ); } ); - test( 'getClassNames preserves exsiting class names when removing responsive classes', () => { + it( 'should preserve exsiting class names when removing responsive classes', () => { const html = ''; const expected = 'lovely'; expect( @@ -79,6 +79,15 @@ describe( 'utils', () => { ) ).toEqual( expected ); } ); + + it( 'should return the same falsy value as passed for existing classes when no new classes are added', () => { + const html = ''; + const expected = undefined; + expect( getClassNames( html, undefined, false ) ).toEqual( + expected + ); + } ); + it( 'should preserve existing classes and replace aspect ratio related classes with the current embed preview', () => { const html = ''; const expected = @@ -93,6 +102,12 @@ describe( 'utils', () => { } ); } ); describe( 'removeAspectRatioClasses', () => { + it( 'should return the same falsy value as received', () => { + const existingClassNames = undefined; + expect( removeAspectRatioClasses( existingClassNames ) ).toEqual( + existingClassNames + ); + } ); it( 'should preserve existing classes, if no aspect ratio classes exist', () => { const existingClassNames = 'wp-block-embed is-type-video'; expect( removeAspectRatioClasses( existingClassNames ) ).toEqual( diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index 7c5c123fd21c48..bc49207a829eb3 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -156,6 +156,12 @@ export const createUpgradedEmbedBlock = ( * @return {string} The class names without any aspect ratio related class. */ export const removeAspectRatioClasses = ( existingClassNames ) => { + if ( ! existingClassNames ) { + // Avoids extraneous work and also, by returning the same value as + // received, ensures the post is not dirtied by a change of the block + // attribute from `undefined` to an emtpy string. + return existingClassNames; + } const aspectRatioClassNames = ASPECT_RATIOS.reduce( ( accumulator, { className } ) => { accumulator[ className ] = false; @@ -176,7 +182,7 @@ export const removeAspectRatioClasses = ( existingClassNames ) => { */ export function getClassNames( html, - existingClassNames = '', + existingClassNames, allowResponsive = true ) { if ( ! allowResponsive ) { @@ -198,6 +204,14 @@ export function getClassNames( ) { const potentialRatio = ASPECT_RATIOS[ ratioIndex ]; if ( aspectRatio >= potentialRatio.ratio ) { + // Evaluate the difference between actual aspect ratio and closest match. + // If the difference is too big, do not scale the embed according to aspect ratio. + const ratioDiff = aspectRatio - potentialRatio.ratio; + if ( ratioDiff > 0.1 ) { + // No close aspect ratio match found. + return removeAspectRatioClasses( existingClassNames ); + } + // Close aspect ratio match found. return classnames( removeAspectRatioClasses( existingClassNames ), potentialRatio.className, diff --git a/packages/block-library/src/embed/wp-embed-preview.js b/packages/block-library/src/embed/wp-embed-preview.js index 2bdc086e3bc19f..f83341853c250f 100644 --- a/packages/block-library/src/embed/wp-embed-preview.js +++ b/packages/block-library/src/embed/wp-embed-preview.js @@ -11,7 +11,6 @@ export default function WpEmbedPreview( { html } ) { useEffect( () => { const { ownerDocument } = ref.current; const { defaultView } = ownerDocument; - const { FocusEvent } = defaultView; /** * Checks for WordPress embed events signaling the height change when iframe @@ -58,8 +57,7 @@ export default function WpEmbedPreview( { html } ) { return; } - const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); - activeElement.dispatchEvent( focusEvent ); + activeElement.focus(); } defaultView.addEventListener( 'message', resizeWPembeds ); diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index 4ece3165939f01..766de08b989909 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -10,10 +10,9 @@ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; import { __unstableGetAnimateClassName as getAnimateClassName, withNotices, - ToolbarGroup, ToolbarButton, } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { BlockControls, BlockIcon, @@ -23,11 +22,12 @@ import { useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useEffect, useState, useRef } from '@wordpress/element'; -import { useCopyOnClick } from '@wordpress/compose'; +import { useEffect, useState } from '@wordpress/element'; +import { useCopyToClipboard } from '@wordpress/compose'; import { __, _x } from '@wordpress/i18n'; import { file as icon } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -35,8 +35,13 @@ import { store as coreStore } from '@wordpress/core-data'; import FileBlockInspector from './inspector'; function ClipboardToolbarButton( { text, disabled } ) { - const ref = useRef(); - const hasCopied = useCopyOnClick( ref, text ); + const { createNotice } = useDispatch( noticesStore ); + const ref = useCopyToClipboard( text, () => { + createNotice( 'info', __( 'Copied URL to clipboard.' ), { + isDismissible: true, + type: 'snackbar', + } ); + } ); return ( - { hasCopied ? __( 'Copied!' ) : __( 'Copy URL' ) } + { __( 'Copy URL' ) } ); } @@ -176,34 +181,32 @@ function FileEdit( { attributes, setAttributes, noticeUI, noticeOperations } ) { changeShowDownloadButton, } } /> - - - - - + + +
-
- - setAttributes( { fileName: text } ) - } - /> -
+ + setAttributes( { fileName: text } ) + } + href={ textLinkHref } + /> { showDownloadButton && (
{ shouldShowSizeOptions && ( ) } diff --git a/packages/block-library/src/gallery/editor.scss b/packages/block-library/src/gallery/editor.scss index 817f65a62327b0..9a07be2a23d735 100644 --- a/packages/block-library/src/gallery/editor.scss +++ b/packages/block-library/src/gallery/editor.scss @@ -50,6 +50,7 @@ figure.wp-block-gallery { bottom: 0; left: 0; z-index: 1; + pointer-events: none; } figcaption { z-index: 2; diff --git a/packages/block-library/src/group/block.json b/packages/block-library/src/group/block.json index 913194728559b7..ad4cc62fc68a73 100644 --- a/packages/block-library/src/group/block.json +++ b/packages/block-library/src/group/block.json @@ -12,10 +12,7 @@ } }, "supports": { - "align": [ - "wide", - "full" - ], + "align": [ "wide", "full" ], "anchor": true, "html": false, "color": { @@ -30,7 +27,8 @@ "radius": true, "style": true, "width": true - } + }, + "__experimentalLayout": true }, "editorStyle": "wp-block-group-editor", "style": "wp-block-group" diff --git a/packages/block-library/src/group/deprecated.js b/packages/block-library/src/group/deprecated.js index 3e6d394197ffd6..337e498b0bdedd 100644 --- a/packages/block-library/src/group/deprecated.js +++ b/packages/block-library/src/group/deprecated.js @@ -7,7 +7,11 @@ import { omit } from 'lodash'; /** * WordPress dependencies */ -import { InnerBlocks, getColorClassName } from '@wordpress/block-editor'; +import { + InnerBlocks, + getColorClassName, + useBlockProps, +} from '@wordpress/block-editor'; const migrateAttributes = ( attributes ) => { if ( ! attributes.tagName ) { @@ -34,6 +38,43 @@ const migrateAttributes = ( attributes ) => { }; const deprecated = [ + // Version of the block with the double div. + { + attributes: { + tagName: { + type: 'string', + default: 'div', + }, + templateLock: { + type: 'string', + }, + }, + supports: { + align: [ 'wide', 'full' ], + anchor: true, + color: { + gradients: true, + link: true, + }, + spacing: { + padding: true, + }, + __experimentalBorder: { + radius: true, + }, + }, + save( { attributes } ) { + const { tagName: Tag } = attributes; + + return ( + +
+ +
+
+ ); + }, + }, // Version of the block without global styles support { attributes: { diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 55de289edbd42f..bb88f00772a7e0 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -7,35 +7,47 @@ import { useBlockProps, InspectorAdvancedControls, __experimentalUseInnerBlocksProps as useInnerBlocksProps, + __experimentalUseEditorFeature as useEditorFeature, store as blockEditorStore, } from '@wordpress/block-editor'; -import { - SelectControl, - __experimentalBoxControl as BoxControl, -} from '@wordpress/components'; +import { SelectControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -const { __Visualizer: BoxControlVisualizer } = BoxControl; function GroupEdit( { attributes, setAttributes, clientId } ) { - const hasInnerBlocks = useSelect( + const { hasInnerBlocks, themeSupportsLayout } = useSelect( ( select ) => { - const { getBlock } = select( blockEditorStore ); + const { getBlock, getSettings } = select( blockEditorStore ); const block = getBlock( clientId ); - return !! ( block && block.innerBlocks.length ); + return { + hasInnerBlocks: !! ( block && block.innerBlocks.length ), + themeSupportsLayout: getSettings()?.supportsLayout, + }; }, [ clientId ] ); + const defaultLayout = useEditorFeature( 'layout' ) || {}; + const { tagName: TagName = 'div', templateLock, layout = {} } = attributes; + const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const { contentSize, wideSize } = usedLayout; + const alignments = + contentSize || wideSize + ? [ 'wide', 'full' ] + : [ 'left', 'center', 'right' ]; const blockProps = useBlockProps(); - const { tagName: TagName = 'div', templateLock } = attributes; const innerBlocksProps = useInnerBlocksProps( - { - className: 'wp-block-group__inner-container', - }, + themeSupportsLayout + ? blockProps + : { className: 'wp-block-group__inner-container' }, { templateLock, renderAppender: hasInnerBlocks ? undefined : InnerBlocks.ButtonBlockAppender, + __experimentalLayout: { + type: 'default', + // Find a way to inject this in the support flag code (hooks). + alignments: themeSupportsLayout ? alignments : undefined, + }, } ); @@ -59,13 +71,14 @@ function GroupEdit( { attributes, setAttributes, clientId } ) { } /> - - -
- + { themeSupportsLayout && } + { /* Ideally this is not needed but it's there for backward compatibility reason + to keep this div for themes that might rely on its presence */ } + { ! themeSupportsLayout && ( + +
+ + ) } ); } diff --git a/packages/block-library/src/group/editor.scss b/packages/block-library/src/group/editor.scss index 080bc64f433c44..92103665aa8c8e 100644 --- a/packages/block-library/src/group/editor.scss +++ b/packages/block-library/src/group/editor.scss @@ -1,75 +1,17 @@ /** * Group: All Alignment Settings */ - .wp-block-group { - // Ensure not rendering outside the element // as -1px causes overflow-x scrollbars .block-editor-block-list__insertion-point { left: 0; right: 0; } - - // Full Width Blocks - // specificity required to only target immediate child Blocks of a Group - > .wp-block-group__inner-container > [data-align="full"] { - margin-left: auto; - margin-right: auto; - } - - // Full Width Blocks with a background (ie: has padding) - &.has-background > .wp-block-group__inner-container > [data-align="full"] { - // note: using position `left` causes hoz scrollbars so - // we opt to use margin instead - // the 30px matches the hoz padding applied in `theme.scss` - // added when the Block has a background set - margin-left: -30px; - - // 60px here is x2 the hoz padding from `theme.scss` added when - // the Block has a background set - // note: also duplicated below for full width Groups - width: calc(100% + 60px); - } -} - -/** - * Group: Full Width Alignment - */ -[data-align="full"] .wp-block-group { - - // Non-full Width Blocks - // specificity required to only target immediate child Blocks of Group - > .wp-block-group__inner-container > .wp-block { - padding-left: $block-padding; - padding-right: $block-padding; - - @include break-small() { - padding-left: 0; - padding-right: 0; - } - } - - // Full Width Blocks - // specificity required to only target immediate child Blocks of Group - > .wp-block-group__inner-container > [data-align="full"] { - padding-right: 0; - padding-left: 0; - left: 0; - width: 100%; - max-width: none; - } - - // Full Width Blocks with a background (ie: has padding) - // note: also duplicated above for all Group widths - &.has-background > .wp-block-group__inner-container > [data-align="full"] { - width: calc(100% + 60px); - } } // Place block list appender in the same place content will appear. [data-type="core/group"].is-selected { - .block-list-appender { margin-left: 0; margin-right: 0; diff --git a/packages/block-library/src/group/save.js b/packages/block-library/src/group/save.js index 0a5f4b87ec7085..60e4dbd30a8f5b 100644 --- a/packages/block-library/src/group/save.js +++ b/packages/block-library/src/group/save.js @@ -5,12 +5,9 @@ import { InnerBlocks, useBlockProps } from '@wordpress/block-editor'; export default function save( { attributes } ) { const { tagName: Tag } = attributes; - return ( -
- -
+
); } diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index dca740e6962265..e10ff49aa787cd 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -9,12 +9,11 @@ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { createBlock } from '@wordpress/blocks'; import { - AlignmentToolbar, + AlignmentControl, BlockControls, RichText, useBlockProps, } from '@wordpress/block-editor'; -import { ToolbarGroup } from '@wordpress/components'; /** * Internal dependencies @@ -40,16 +39,14 @@ function HeadingEdit( { return ( <> - - - - setAttributes( { level: newLevel } ) - } - /> - - + + setAttributes( { level: newLevel } ) + } + /> + { setAttributes( { textAlign: nextAlign } ); diff --git a/packages/block-library/src/image/constants.js b/packages/block-library/src/image/constants.js index ec8c41b8b09c31..01029c1166bcc4 100644 --- a/packages/block-library/src/image/constants.js +++ b/packages/block-library/src/image/constants.js @@ -5,4 +5,3 @@ export const LINK_DESTINATION_ATTACHMENT = 'attachment'; export const LINK_DESTINATION_CUSTOM = 'custom'; export const NEW_TAB_REL = [ 'noreferrer', 'noopener' ]; export const ALLOWED_MEDIA_TYPES = [ 'image' ]; -export const DEFAULT_SIZE_SLUG = 'large'; diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 15477e3e1eaca9..f86ef56a24dc8c 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -11,14 +11,14 @@ import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; import { withNotices } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { - BlockAlignmentToolbar, + BlockAlignmentControl, BlockControls, BlockIcon, MediaPlaceholder, useBlockProps, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { image as icon } from '@wordpress/icons'; @@ -38,14 +38,13 @@ import { LINK_DESTINATION_MEDIA, LINK_DESTINATION_NONE, ALLOWED_MEDIA_TYPES, - DEFAULT_SIZE_SLUG, } from './constants'; -export const pickRelevantMediaFiles = ( image ) => { +export const pickRelevantMediaFiles = ( image, size ) => { const imageProps = pick( image, [ 'alt', 'id', 'link', 'caption' ] ); imageProps.url = - get( image, [ 'sizes', 'large', 'url' ] ) || - get( image, [ 'media_details', 'sizes', 'large', 'source_url' ] ) || + get( image, [ 'sizes', size, 'url' ] ) || + get( image, [ 'media_details', 'sizes', size, 'source_url' ] ) || image.url; return imageProps; }; @@ -92,6 +91,7 @@ export function ImageEdit( { height, sizeSlug, } = attributes; + const [ temporaryURL, setTemporaryURL ] = useState(); const altRef = useRef(); useEffect( () => { @@ -104,10 +104,10 @@ export function ImageEdit( { }, [ caption ] ); const ref = useRef(); - const mediaUpload = useSelect( ( select ) => { + const { imageDefaultSize, mediaUpload } = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); - return getSettings().mediaUpload; - } ); + return pick( getSettings(), [ 'imageDefaultSize', 'mediaUpload' ] ); + }, [] ); function onUploadError( message ) { noticeOperations.removeAllNotices(); @@ -126,16 +126,15 @@ export function ImageEdit( { return; } - let mediaAttributes = pickRelevantMediaFiles( media ); - - // If the current image is temporary but an alt text was meanwhile - // written by the user, make sure the text is not overwritten. - if ( isTemporaryImage( id, url ) ) { - if ( altRef.current ) { - mediaAttributes = omit( mediaAttributes, [ 'alt' ] ); - } + if ( isBlobURL( media.url ) ) { + setTemporaryURL( media.url ); + return; } + setTemporaryURL(); + + let mediaAttributes = pickRelevantMediaFiles( media, imageDefaultSize ); + // If a caption text was meanwhile written by the user, // make sure the text is not overwritten by empty captions. if ( captionRef.current && ! get( mediaAttributes, [ 'caption' ] ) ) { @@ -148,7 +147,7 @@ export function ImageEdit( { additionalAttributes = { width: undefined, height: undefined, - sizeSlug: DEFAULT_SIZE_SLUG, + sizeSlug: imageDefaultSize, }; } else { // Keep the same url when selecting the same file, so "Image Size" @@ -207,7 +206,9 @@ export function ImageEdit( { setAttributes( { url: newURL, id: undefined, - sizeSlug: DEFAULT_SIZE_SLUG, + width: undefined, + height: undefined, + sizeSlug: imageDefaultSize, } ); } } @@ -254,14 +255,14 @@ export function ImageEdit( { // If an image is temporary, revoke the Blob url when it is uploaded (and is // no longer temporary). useEffect( () => { - if ( ! isTemp ) { + if ( ! temporaryURL ) { return; } return () => { - revokeBlobURL( url ); + revokeBlobURL( temporaryURL ); }; - }, [ isTemp ] ); + }, [ temporaryURL ] ); const isExternal = isExternalImage( id, url ); const src = isExternal ? url : undefined; @@ -275,9 +276,8 @@ export function ImageEdit( { ); const classes = classnames( className, { - 'is-transient': isBlobURL( url ), + 'is-transient': temporaryURL, 'is-resized': !! width || !! height, - 'is-focused': isSelected, [ `size-${ sizeSlug }` ]: sizeSlug, } ); @@ -288,8 +288,9 @@ export function ImageEdit( { return (
- { url && ( + { ( temporaryURL || url ) && ( ) } { ! url && ( - - + diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 3f9baab48611f2..09727d0cceb14d 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -57,7 +57,7 @@ import { store as coreStore } from '@wordpress/core-data'; import styles from './styles.scss'; import { getUpdatedLinkTargetSettings } from './utils'; -import { LINK_DESTINATION_CUSTOM, DEFAULT_SIZE_SLUG } from './constants'; +import { LINK_DESTINATION_CUSTOM } from './constants'; const getUrlForSlug = ( image, { sizeSlug } ) => { return get( image, [ 'media_details', 'sizes', sizeSlug, 'source_url' ] ); @@ -302,7 +302,10 @@ export class ImageEdit extends Component { } onSelectMediaUploadOption( media ) { - const { id, url } = this.props.attributes; + const { + attributes: { id, url }, + imageDefaultSize, + } = this.props; const mediaAttributes = { id: media.id, @@ -316,7 +319,7 @@ export class ImageEdit extends Component { additionalAttributes = { width: undefined, height: undefined, - sizeSlug: DEFAULT_SIZE_SLUG, + sizeSlug: imageDefaultSize, }; } else { // Keep the same url when selecting the same file, so "Image Size" option is not changed. @@ -399,12 +402,18 @@ export class ImageEdit extends Component { render() { const { isCaptionSelected } = this.state; - const { attributes, isSelected, image, clientId } = this.props; + const { + attributes, + isSelected, + image, + clientId, + imageDefaultSize, + } = this.props; const { align, url, alt, id, sizeSlug, className } = attributes; const sizeOptionsValid = find( this.sizeOptions, [ 'value', - DEFAULT_SIZE_SLUG, + imageDefaultSize, ] ); const getToolbarEditButton = ( open ) => ( @@ -434,7 +443,7 @@ export class ImageEdit extends Component { @@ -563,7 +572,7 @@ export default compose( [ attributes: { id, url }, isSelected, } = props; - const { imageSizes } = getSettings(); + const { imageSizes, imageDefaultSize } = getSettings(); const isNotFileUrl = id && getProtocol( url ) !== 'file:'; const shouldGetMedia = @@ -577,6 +586,7 @@ export default compose( [ return { image: shouldGetMedia ? getMedia( id ) : null, imageSizes, + imageDefaultSize, }; } ), withPreferredColorScheme, diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index b9bb60c8d805ef..995993f33a060c 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -14,7 +14,6 @@ import { Spinner, TextareaControl, TextControl, - ToolbarGroup, ToolbarButton, } from '@wordpress/components'; import { useViewportMatch, usePrevious } from '@wordpress/compose'; @@ -63,6 +62,7 @@ function getFilename( url ) { } export default function Image( { + temporaryURL, attributes: { url = '', alt, @@ -281,52 +281,52 @@ export default function Image( { const controls = ( <> - - - - { ! multiImageSelection && ! isEditingImage && ( - - ) } - { allowCrop && ( - setIsEditingImage( true ) } - icon={ crop } - label={ __( 'Crop' ) } - /> - ) } - { externalBlob && ( - - ) } - { ! multiImageSelection && coverBlockExists && ( - - replaceBlocks( - currentId, - switchToBlockType( block, 'core/cover' ) - ) - } - /> - ) } - + + { ! multiImageSelection && ! isEditingImage && ( + + ) } + { allowCrop && ( + setIsEditingImage( true ) } + icon={ crop } + label={ __( 'Crop' ) } + /> + ) } + { externalBlob && ( + + ) } + { ! multiImageSelection && coverBlockExists && ( + + replaceBlocks( + currentId, + switchToBlockType( block, 'core/cover' ) + ) + } + /> + ) } + + { ! multiImageSelection && ! isEditingImage && ( + - ) } - + + ) } { ! multiImageSelection && ( @@ -415,7 +415,7 @@ export default function Image( { /* eslint-disable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ <> { onImageError() } @@ -428,7 +428,7 @@ export default function Image( { ); } } /> - { isBlobURL( url ) && } + { temporaryURL && } /* eslint-enable jsx-a11y/no-noninteractive-element-interactions, jsx-a11y/click-events-have-key-events */ ); diff --git a/packages/block-library/src/index.js b/packages/block-library/src/index.js index c4830add9cc1a7..243f83646f2006 100644 --- a/packages/block-library/src/index.js +++ b/packages/block-library/src/index.js @@ -38,6 +38,7 @@ import * as navigation from './navigation'; import * as navigationLink from './navigation-link'; import * as latestComments from './latest-comments'; import * as latestPosts from './latest-posts'; +import * as logInOut from './loginout'; import * as list from './list'; import * as missing from './missing'; import * as more from './more'; @@ -69,6 +70,7 @@ import * as siteTitle from './site-title'; import * as templatePart from './template-part'; import * as query from './query'; import * as queryLoop from './query-loop'; +import * as queryTitle from './query-title'; import * as queryPagination from './query-pagination'; import * as queryPaginationNext from './query-pagination-next'; import * as queryPaginationNumbers from './query-pagination-numbers'; @@ -89,6 +91,7 @@ import * as postExcerpt from './post-excerpt'; import * as postFeaturedImage from './post-featured-image'; import * as postHierarchicalTerms from './post-hierarchical-terms'; import * as postTags from './post-tags'; +import * as termDescription from './term-description'; /** * Function to register an individual block. @@ -220,10 +223,12 @@ export const __experimentalRegisterExperimentalCoreBlocks = templatePart, query, queryLoop, + queryTitle, queryPagination, queryPaginationNext, queryPaginationNumbers, queryPaginationPrevious, + logInOut, postTitle, postContent, postAuthor, @@ -240,6 +245,7 @@ export const __experimentalRegisterExperimentalCoreBlocks = postHierarchicalTerms, postTags, postNavigationLink, + termDescription, ] : [] ), ].forEach( registerBlock ); diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index a43646edd72ce2..8d28ae234029d4 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -11,6 +11,7 @@ * * @var int */ +global $block_core_latest_posts_excerpt_length; $block_core_latest_posts_excerpt_length = 0; /** diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index afb5d562e33eba..3e84918f5ec957 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -9,7 +9,7 @@ import { RichTextShortcut, useBlockProps, } from '@wordpress/block-editor'; -import { ToolbarGroup } from '@wordpress/components'; +import { ToolbarButton } from '@wordpress/components'; import { __unstableCanIndentListItems as canIndentListItems, __unstableCanOutdentListItems as canOutdentListItems, @@ -84,70 +84,58 @@ export default function ListEdit( { /> ) } - - + { + onChange( changeListType( value, { type: 'ul' } ) ); + onFocus(); - if ( isListRootSelected( value ) ) { - setAttributes( { ordered: false } ); - } - }, - }, - { - icon: isRTL() - ? formatListNumberedRTL - : formatListNumbered, - title: __( 'Ordered' ), - describedBy: __( 'Convert to ordered list' ), - isActive: isActiveListType( value, 'ol', tagName ), - onClick() { - onChange( - changeListType( value, { type: 'ol' } ) - ); - onFocus(); + if ( isListRootSelected( value ) ) { + setAttributes( { ordered: false } ); + } + } } + /> + { + onChange( changeListType( value, { type: 'ol' } ) ); + onFocus(); - if ( isListRootSelected( value ) ) { - setAttributes( { ordered: true } ); - } - }, - }, - { - icon: isRTL() ? formatOutdentRTL : formatOutdent, - title: __( 'Outdent' ), - describedBy: __( 'Outdent list item' ), - shortcut: _x( 'Backspace', 'keyboard key' ), - isDisabled: ! canOutdentListItems( value ), - onClick() { - onChange( outdentListItems( value ) ); - onFocus(); - }, - }, - { - icon: isRTL() ? formatIndentRTL : formatIndent, - title: __( 'Indent' ), - describedBy: __( 'Indent list item' ), - shortcut: _x( 'Space', 'keyboard key' ), - isDisabled: ! canIndentListItems( value ), - onClick() { - onChange( - indentListItems( value, { type: tagName } ) - ); - onFocus(); - }, - }, - ] } + if ( isListRootSelected( value ) ) { + setAttributes( { ordered: true } ); + } + } } + /> + { + onChange( outdentListItems( value ) ); + onFocus(); + } } + /> + { + onChange( indentListItems( value, { type: tagName } ) ); + onFocus(); + } } /> diff --git a/packages/block-library/src/list/editor.scss b/packages/block-library/src/list/editor.scss deleted file mode 100644 index ba2b8357074162..00000000000000 --- a/packages/block-library/src/list/editor.scss +++ /dev/null @@ -1,7 +0,0 @@ -// Extra specificity required to override default editor styles, which are -// loaded after core block styles. If the load order changes, this rule can be -// removed. See https://github.com/WordPress/gutenberg/issues/24011. -ol.has-background.has-background, -ul.has-background.has-background { - padding: $block-bg-padding--v $block-bg-padding--h; -} diff --git a/packages/block-library/src/loginout/block.json b/packages/block-library/src/loginout/block.json new file mode 100644 index 00000000000000..5c8240c0d7ca8d --- /dev/null +++ b/packages/block-library/src/loginout/block.json @@ -0,0 +1,19 @@ +{ + "apiVersion": 2, + "name": "core/loginout", + "category": "design", + "attributes": { + "displayLoginAsForm": { + "type": "boolean", + "default": false + }, + "redirectToCurrent": { + "type": "boolean", + "default": true + } + }, + "supports": { + "className": true, + "fontSize": false + } +} diff --git a/packages/block-library/src/loginout/edit.js b/packages/block-library/src/loginout/edit.js new file mode 100644 index 00000000000000..70e90bf84e903a --- /dev/null +++ b/packages/block-library/src/loginout/edit.js @@ -0,0 +1,44 @@ +/** + * WordPress dependencies + */ +import { PanelBody, ToggleControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; + +export default function LoginOutEdit( { attributes, setAttributes } ) { + const { displayLoginAsForm, redirectToCurrent } = attributes; + + return ( + <> + + + + setAttributes( { + displayLoginAsForm: ! displayLoginAsForm, + } ) + } + /> + + setAttributes( { + redirectToCurrent: ! redirectToCurrent, + } ) + } + /> + + + + + ); +} diff --git a/packages/block-library/src/loginout/index.js b/packages/block-library/src/loginout/index.js new file mode 100644 index 00000000000000..3f5c6625e0dc76 --- /dev/null +++ b/packages/block-library/src/loginout/index.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { login as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import edit from './edit'; +import metadata from './block.json'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: _x( 'Login/out', 'block title' ), + description: __( 'Show login & logout links.' ), + icon, + keywords: [ __( 'login' ), __( 'logout' ), __( 'form' ) ], + edit, +}; diff --git a/packages/block-library/src/loginout/index.php b/packages/block-library/src/loginout/index.php new file mode 100644 index 00000000000000..c9c23cf90b7b96 --- /dev/null +++ b/packages/block-library/src/loginout/index.php @@ -0,0 +1,51 @@ + false ) ); + } + + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $classes ) ); + + return '
' . $contents . '
'; +} + +/** + * Registers the `core/latest-posts` block on server. + */ +function register_block_core_loginout() { + register_block_type_from_metadata( + __DIR__ . '/loginout', + array( + 'render_callback' => 'render_block_core_loginout', + ) + ); +} +add_action( 'init', 'register_block_core_loginout' ); diff --git a/packages/block-library/src/media-text/edit.js b/packages/block-library/src/media-text/edit.js index 55b4551a2557aa..41134d2693c4af 100644 --- a/packages/block-library/src/media-text/edit.js +++ b/packages/block-library/src/media-text/edit.js @@ -12,7 +12,7 @@ import { useSelect } from '@wordpress/data'; import { useState, useRef } from '@wordpress/element'; import { BlockControls, - BlockVerticalAlignmentToolbar, + BlockVerticalAlignmentControl, __experimentalUseInnerBlocksProps as useInnerBlocksProps, InspectorControls, useBlockProps, @@ -24,7 +24,7 @@ import { PanelBody, TextareaControl, ToggleControl, - ToolbarGroup, + ToolbarButton, ExternalLink, FocalPointPicker, } from '@wordpress/components'; @@ -187,20 +187,6 @@ function MediaTextEdit( { attributes, isSelected, setAttributes } ) { gridTemplateColumns, msGridColumns: gridTemplateColumns, }; - const toolbarControls = [ - { - icon: pullLeft, - title: __( 'Show media on left' ), - isActive: mediaPosition === 'left', - onClick: () => setAttributes( { mediaPosition: 'left' } ), - }, - { - icon: pullRight, - title: __( 'Show media on right' ), - isActive: mediaPosition === 'right', - onClick: () => setAttributes( { mediaPosition: 'right' } ), - }, - ]; const onMediaAltChange = ( newMediaAlt ) => { setAttributes( { mediaAlt: newMediaAlt } ); }; @@ -306,26 +292,37 @@ function MediaTextEdit( { attributes, isSelected, setAttributes } ) { return ( <> { mediaTextGeneralSettings } - - - + + setAttributes( { mediaPosition: 'left' } ) } + /> + + setAttributes( { mediaPosition: 'right' } ) + } + /> { mediaType === 'image' && ( - - - + ) }
diff --git a/packages/block-library/src/media-text/media-container.js b/packages/block-library/src/media-text/media-container.js index 26a87128096407..fb9325c410f3e2 100644 --- a/packages/block-library/src/media-text/media-container.js +++ b/packages/block-library/src/media-text/media-container.js @@ -58,7 +58,7 @@ const ResizableBoxContainer = forwardRef( function ToolbarEditButton( { mediaId, mediaUrl, onSelectMedia } ) { return ( - +
  • -
    + { /* eslint-disable jsx-a11y/anchor-is-valid */ } + + { /* eslint-enable */ } { ! url ? (
    setIsLinkOpen( false ) } + anchorRef={ listItemRef.current } > ) } -
    +
    { hasDescendants && showSubmenuIcon && ( diff --git a/packages/block-library/src/navigation-link/editor.scss b/packages/block-library/src/navigation-link/editor.scss index 9c5a5082a82ad8..bd81b95f3bd9f4 100644 --- a/packages/block-library/src/navigation-link/editor.scss +++ b/packages/block-library/src/navigation-link/editor.scss @@ -6,11 +6,31 @@ min-height: $button-size; } + +/** + * Submenus. + */ + // Show submenus above the sibling inserter. -.has-child .wp-block-navigation-link__container { - z-index: z-index(".has-child .wp-block-navigation-link__container"); +.has-child { + cursor: pointer; + + .wp-block-navigation-link__container { + z-index: z-index(".has-child .wp-block-navigation-link__container"); + } + + // Show on editor selected, even if on frontend it only stays open on focus-within. + &.is-selected, + &.has-child-selected { + > .wp-block-navigation-link__container { + // We use important here because if the parent block is selected and submenus are present, they should always be visible. + visibility: visible !important; + opacity: 1 !important; + } + } } + /** * Navigation Items. */ @@ -20,6 +40,10 @@ display: block; } + .wp-block-navigation-link__content { + cursor: text; + } + &.is-editing, &.is-selected { min-width: 20px; diff --git a/packages/block-library/src/navigation-link/hooks.js b/packages/block-library/src/navigation-link/hooks.js index 410bf8541369cc..7fb89cafe414dd 100644 --- a/packages/block-library/src/navigation-link/hooks.js +++ b/packages/block-library/src/navigation-link/hooks.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { addFilter } from '@wordpress/hooks'; import { category, page, @@ -30,7 +29,7 @@ function getIcon( variationName ) { } } -function enhanceNavigationLinkVariations( settings, name ) { +export function enhanceNavigationLinkVariations( settings, name ) { if ( name !== 'core/navigation-link' ) { return settings; } @@ -67,9 +66,3 @@ function enhanceNavigationLinkVariations( settings, name ) { } return settings; } - -addFilter( - 'blocks.registerBlockType', - 'core/navigation-link', - enhanceNavigationLinkVariations -); diff --git a/packages/block-library/src/navigation-link/index.js b/packages/block-library/src/navigation-link/index.js index 95de1e963db52f..b183e069360f85 100644 --- a/packages/block-library/src/navigation-link/index.js +++ b/packages/block-library/src/navigation-link/index.js @@ -4,6 +4,7 @@ import { __, _x } from '@wordpress/i18n'; import { customLink as linkIcon } from '@wordpress/icons'; import { InnerBlocks } from '@wordpress/block-editor'; +import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -11,7 +12,7 @@ import { InnerBlocks } from '@wordpress/block-editor'; import metadata from './block.json'; import edit from './edit'; import save from './save'; -import './hooks'; +import { enhanceNavigationLinkVariations } from './hooks'; const { name } = metadata; @@ -88,3 +89,10 @@ export const settings = { }, ], }; + +// ensure that we import and use this from hooks.js, so code is not shaken out in final build. +addFilter( + 'blocks.registerBlockType', + 'core/navigation-link', + enhanceNavigationLinkVariations +); diff --git a/packages/block-library/src/navigation-link/style.scss b/packages/block-library/src/navigation-link/style.scss index 8d94551e9dff30..1696307a70d7f5 100644 --- a/packages/block-library/src/navigation-link/style.scss +++ b/packages/block-library/src/navigation-link/style.scss @@ -1,161 +1,196 @@ -.wp-block-navigation-link { - display: flex; - align-items: center; - position: relative; - margin: 0; - - .wp-block-navigation-link__container:empty { - display: none; - } -} +// Navigation item styles. +// This also styles navigation links inside the Page List block, +// as that block is meant to behave as menu items when leveraged. +.wp-block-navigation { + // Menu item container. + .wp-block-pages-list__item, + .wp-block-navigation-link { + display: flex; + align-items: center; + position: relative; + margin: 0 0.5em 0 0; -// Styles for submenu flyout. -.has-child { - > .wp-block-navigation-link__content { - padding-right: 0.5em; + .wp-block-navigation-link__container:empty { + display: none; + } } - .wp-block-navigation-link__container { - border: $border-width solid rgba(0, 0, 0, 0.15); - background-color: inherit; + // Menu item link. + .wp-block-pages-list__item__link, + .wp-block-navigation-link__content { + // Inherit colors set by the block color definition. color: inherit; - position: absolute; - left: 0; - top: 100%; - width: fit-content; - z-index: 2; - opacity: 0; - transition: opacity 0.1s linear; - visibility: hidden; - - > .wp-block-navigation-link { - > .wp-block-navigation-link__content { - flex-grow: 1; - } - > .wp-block-navigation-link__submenu-icon { - padding-right: 0.5em; - } + display: block; + padding: 0.5em 1em; + } + + // Force links to inherit text decoration applied to navigation block. + // The extra selector adds specificity to ensure it works when user-set. + &[style*="text-decoration"] { + .wp-block-pages-list__item, + .wp-block-navigation-link__container, + .wp-block-navigation-link { + text-decoration: inherit; } - @include break-medium { - left: 1.5em; - - // Nested submenus sit to the left on large breakpoints - .wp-block-navigation-link__container { - left: 100%; - top: -1px; - - // Prevent the menu from disappearing when the mouse is over the gap - &::before { - content: ""; - position: absolute; - right: 100%; - height: 100%; - display: block; - width: 0.5em; - background: transparent; - } + .wp-block-pages-list__item__link, + .wp-block-navigation-link__content { + text-decoration: inherit; + + &:focus, + &:active { + text-decoration: inherit; } + } + } - .wp-block-navigation-link__submenu-icon svg { - transform: rotate(0); + &:not([style*="text-decoration"]) { + .wp-block-pages-list__item__link, + .wp-block-navigation-link__content { + text-decoration: none; + + &:focus, + &:active { + text-decoration: none; } } } - // Show submenus on hover. - // Separating out hover and focus-within so hover works again on IE: https://davidwalsh.name/css-focus-within#comment-513401 - // We will need to replace focus-within with a JS solution for IE keyboard support. - &:hover { - cursor: pointer; + // This wraps just the innermost text for custom menu items. + .wp-block-navigation-link__label { + word-break: normal; + overflow-wrap: break-word; + } - > .wp-block-navigation-link__container { - visibility: visible; - opacity: 1; - display: flex; - flex-direction: column; + // Submenu indicator. + .wp-block-page-list__submenu-icon, + .wp-block-navigation-link__submenu-icon { + height: inherit; + padding: 0.375em 1em 0.375em 0; + + svg { + fill: currentColor; + + @include break-medium { + transform: rotate(90deg); + } } } - // Keep submenus open when focus is within. - &:focus-within { - cursor: pointer; + // Styles for submenu flyout. + .has-child { + > .wp-block-pages-list__item__link, + > .wp-block-navigation-link__content { + padding-right: 0.5em; + } - > .wp-block-navigation-link__container { - visibility: visible; - opacity: 1; + .submenu-container, + .wp-block-navigation-link__container { + border: 1px solid rgba(0, 0, 0, 0.15); + background-color: inherit; + color: inherit; + position: absolute; + left: 0; + top: 100%; + z-index: 2; display: flex; flex-direction: column; - } - } -} + align-items: normal; + min-width: 200px; + + // Hide until hover or focus within. + opacity: 0; + transition: opacity 0.1s linear; + visibility: hidden; + + > .wp-block-pages-list__item, + > .wp-block-navigation-link { + > .wp-block-pages-list__item__link, + > .wp-block-navigation-link__content { + flex-grow: 1; + } -// Default background and font color -.wp-block-navigation:not(.has-background) .wp-block-navigation__container { - .wp-block-navigation-link__container { - // Set a background color for submenus so that they're not transparent. - // NOTE TO DEVS - if refactoring this code, please double-check that - // submenus have a default background color, this feature has regressed - // several times, so care needs to be taken. - background-color: #fff; - color: #000; - } -} + > .wp-block-page-list__submenu-icon, + > .wp-block-navigation-link__submenu-icon { + padding-right: 0.5em; + } + } -// Force links to inherit text decoration applied to navigation block. -.wp-block-navigation[style*="text-decoration"] { - .wp-block-navigation-link__container, - .wp-block-navigation-link { - text-decoration: inherit; - } - .wp-block-navigation-link__content { - text-decoration: inherit; + @include break-medium { + left: 1.5em; + + // Nested submenus sit to the left on large breakpoints. + .submenu-container, + .wp-block-navigation-link__container { + left: 100%; + top: -$border-width; + + // Prevent the menu from disappearing when the mouse is over the gap + &::before { + content: ""; + position: absolute; + right: 100%; + height: 100%; + display: block; + width: 0.5em; + background: transparent; + } + } - &:focus, - &:active { - text-decoration: inherit; + // Reset the submenu indicator for horizontal flyouts. + .wp-block-page-list__submenu-icon svg, + .wp-block-navigation-link__submenu-icon svg { + transform: rotate(0); + } + } } - } -} -.wp-block-navigation:not([style*="text-decoration"]) { - .wp-block-navigation-link__content { - text-decoration: none; + // Separating out hover and focus-within so hover works again on IE: https://davidwalsh.name/css-focus-within#comment-513401 + // We will need to replace focus-within with a JS solution for IE keyboard support. - &:focus, - &:active { - text-decoration: none; + // Custom menu items. + // Show submenus on hover. + &:hover > .wp-block-navigation-link__container { + visibility: visible; + opacity: 1; } - } -} -// All links. -.wp-block-navigation-link__content { - color: inherit; - padding: 0.5em 1em; - - + .wp-block-navigation-link__content { - padding-top: 0; - } - .has-text-color & { - color: inherit; - } -} + // Keep submenus open when focus is within. + &:focus-within > .wp-block-navigation-link__container { + visibility: visible; + opacity: 1; + } -.wp-block-navigation-link__label { - word-break: normal; - overflow-wrap: break-word; -} + // Page list menu items. + &:hover { + cursor: pointer; -.wp-block-navigation-link__submenu-icon { - height: inherit; - padding: 0.375em 1em 0.375em 0; + > .submenu-container { + visibility: visible; + opacity: 1; + } + } - svg { - fill: currentColor; + &:focus-within { + cursor: pointer; - @include break-medium { - transform: rotate(90deg); + > .submenu-container { + visibility: visible; + opacity: 1; + } } } } + +// Default background and font color. +.wp-block-navigation:not(.has-background) { + .submenu-container, // This target items created by the Page List block. + .wp-block-navigation__container .wp-block-navigation-link__container { + // Set a background color for submenus so that they're not transparent. + // NOTE TO DEVS - if refactoring this code, please double-check that + // submenus have a default background color, this feature has regressed + // several times, so care needs to be taken. + background-color: #fff; + color: #000; + } +} diff --git a/packages/block-library/src/navigation-link/test/__snapshots__/hooks.js.snap b/packages/block-library/src/navigation-link/test/__snapshots__/hooks.js.snap new file mode 100644 index 00000000000000..26422f280fc0fb --- /dev/null +++ b/packages/block-library/src/navigation-link/test/__snapshots__/hooks.js.snap @@ -0,0 +1,184 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`hooks enhanceNavigationLinkVariations adds fallback variations when variations are missing 1`] = ` +Object { + "name": "core/navigation-link", + "one": "one", + "three": "three", + "two": "two", + "variations": Array [ + Object { + "attributes": Object {}, + "description": "A link to a URL.", + "isActive": [Function], + "isDefault": true, + "name": "link", + "title": "Link", + }, + Object { + "attributes": Object { + "type": "post", + }, + "description": "A link to a post.", + "icon": + + , + "isActive": [Function], + "name": "post", + "title": "Post Link", + }, + Object { + "attributes": Object { + "type": "page", + }, + "description": "A link to a page.", + "icon": + + , + "isActive": [Function], + "name": "page", + "title": "Page Link", + }, + Object { + "attributes": Object { + "type": "category", + }, + "description": "A link to a category.", + "icon": + + , + "isActive": [Function], + "name": "category", + "title": "Category Link", + }, + Object { + "attributes": Object { + "type": "tag", + }, + "description": "A link to a tag.", + "icon": + + , + "isActive": [Function], + "name": "tag", + "title": "Tag Link", + }, + ], +} +`; + +exports[`hooks enhanceNavigationLinkVariations enhances variations with icon and isActive functions 1`] = ` +Object { + "extraProp": "extraProp", + "name": "core/navigation-link", + "variations": Array [ + Object { + "attributes": Object {}, + "description": "A link to a URL.", + "icon": + + , + "isActive": [Function], + "name": "link", + "title": "Link", + }, + Object { + "attributes": Object { + "type": "post", + }, + "description": "A link to a post.", + "icon": + + , + "isActive": [Function], + "name": "post", + "title": "Post Link", + }, + Object { + "attributes": Object { + "type": "page", + }, + "description": "A link to a page.", + "icon": + + , + "isActive": [Function], + "name": "page", + "title": "Page Link", + }, + Object { + "attributes": Object { + "type": "category", + }, + "description": "A link to a category.", + "icon": + + , + "isActive": [Function], + "name": "category", + "title": "Category Link", + }, + Object { + "attributes": Object { + "type": "tag", + }, + "description": "A link to a tag.", + "icon": + + , + "isActive": [Function], + "name": "tag", + "title": "Tag Link", + }, + ], +} +`; diff --git a/packages/block-library/src/navigation-link/test/hooks.js b/packages/block-library/src/navigation-link/test/hooks.js new file mode 100644 index 00000000000000..16c4aad1fba5b5 --- /dev/null +++ b/packages/block-library/src/navigation-link/test/hooks.js @@ -0,0 +1,84 @@ +/** + * Internal dependencies + */ +import { enhanceNavigationLinkVariations } from '../hooks'; +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +describe( 'hooks', () => { + describe( 'enhanceNavigationLinkVariations', () => { + it( 'does not modify settings when settings do not belong to a navigation link', () => { + const updatedSettings = enhanceNavigationLinkVariations( + { + name: 'core/test', + one: 'one', + two: 'two', + three: 'three', + }, + 'core/test' + ); + expect( updatedSettings ).toEqual( { + name: 'core/test', + one: 'one', + two: 'two', + three: 'three', + } ); + } ); + it( 'adds fallback variations when variations are missing', () => { + const updatedSettings = enhanceNavigationLinkVariations( + { + name: 'core/navigation-link', + one: 'one', + two: 'two', + three: 'three', + }, + 'core/navigation-link' + ); + expect( updatedSettings ).toMatchSnapshot(); + } ); + it( 'enhances variations with icon and isActive functions', () => { + const updatedSettings = enhanceNavigationLinkVariations( + { + name: 'core/navigation-link', + extraProp: 'extraProp', + variations: [ + { + name: 'link', + title: __( 'Link' ), + description: __( 'A link to a URL.' ), + attributes: {}, + }, + { + name: 'post', + title: __( 'Post Link' ), + description: __( 'A link to a post.' ), + attributes: { type: 'post' }, + }, + { + name: 'page', + title: __( 'Page Link' ), + description: __( 'A link to a page.' ), + attributes: { type: 'page' }, + }, + { + name: 'category', + title: __( 'Category Link' ), + description: __( 'A link to a category.' ), + attributes: { type: 'category' }, + }, + { + name: 'tag', + title: __( 'Tag Link' ), + description: __( 'A link to a tag.' ), + attributes: { type: 'tag' }, + }, + ], + }, + 'core/navigation-link' + ); + expect( updatedSettings ).toMatchSnapshot(); + } ); + } ); +} ); diff --git a/packages/block-library/src/navigation/block-navigation-list.js b/packages/block-library/src/navigation/block-navigation-list.js index 638609067ea9d0..057c4e6396b049 100644 --- a/packages/block-library/src/navigation/block-navigation-list.js +++ b/packages/block-library/src/navigation/block-navigation-list.js @@ -11,14 +11,15 @@ export default function BlockNavigationList( { clientId, __experimentalFeatures, } ) { - const { block, selectedBlockClientId } = useSelect( + const { blocks, selectedBlockClientId } = useSelect( ( select ) => { - const { getSelectedBlockClientId, getBlock } = select( - blockEditorStore - ); + const { + getSelectedBlockClientId, + __unstableGetClientIdsTree, + } = select( blockEditorStore ); return { - block: getBlock( clientId ), + blocks: __unstableGetClientIdsTree( clientId ), selectedBlockClientId: getSelectedBlockClientId(), }; }, @@ -29,8 +30,8 @@ export default function BlockNavigationList( { return ( <__experimentalBlockNavigationTree - blocks={ block.innerBlocks } - selectedBlockClientId={ selectedBlockClientId } + blocks={ blocks } + selectedBlockClientIds={ [ selectedBlockClientId ] } selectBlock={ selectBlock } __experimentalFeatures={ __experimentalFeatures } showNestedBlocks diff --git a/packages/block-library/src/navigation/block.json b/packages/block-library/src/navigation/block.json index 211d20a97c18cc..dd6c660d87a8cd 100644 --- a/packages/block-library/src/navigation/block.json +++ b/packages/block-library/src/navigation/block.json @@ -52,6 +52,7 @@ "html": false, "inserter": true, "fontSize": true, + "lineHeight": true, "__experimentalFontStyle": true, "__experimentalFontWeight": true, "__experimentalTextTransform": true, diff --git a/packages/block-library/src/navigation/edit.js b/packages/block-library/src/navigation/edit.js index f93399e38931ab..0f21df8e75ad3d 100644 --- a/packages/block-library/src/navigation/edit.js +++ b/packages/block-library/src/navigation/edit.js @@ -41,7 +41,6 @@ function Navigation( { className, hasSubmenuIndicatorSetting = true, hasItemJustificationControls = attributes.orientation === 'horizontal', - hasListViewModal = true, } ) { const [ isPlaceholderShown, setIsPlaceholderShown ] = useState( ! hasExistingNavItems @@ -124,11 +123,9 @@ function Navigation( { } } /> ) } - { hasListViewModal && ( - { navigatorToolbarButton } - ) } + { navigatorToolbarButton } - { hasListViewModal && navigatorModal } + { navigatorModal } { hasSubmenuIndicatorSetting && ( diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index af12015affd2d2..972678544bb370 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -6,14 +6,23 @@ // These need extra specificity. .editor-styles-wrapper .wp-block-navigation { ul { + margin-top: 0; margin-bottom: 0; margin-left: 0; padding-left: 0; } // Unset horizontal and vertical margin rules from editor normalization stylesheet. + // Both margin-left: auto; and margin-right: auto; from .wp-block, and also + // margin: revert; from .editor-styles-wrapper ul li. .block-editor-block-list__block { margin: 0; + + // This CSS provides a little spacing between blocks. + // It matches a rule in style.scss exactly, but needs higher specificity due to the unsetting of inherited styles above. + &.wp-block-navigation-link { + margin: 0 0.5em 0 0; + } } } @@ -34,18 +43,16 @@ background-color: inherit; } -// Styles for submenu flyout -.has-child { - // Only show the flyout when the parent menu item is selected. - // Do not show it on hover. - &, - &:hover { - > .wp-block-navigation__container { - opacity: 0; - visibility: hidden; - } +// Only show the flyout on hover if the parent menu item is selected. +.wp-block-navigation:not(.is-selected):not(.has-child-selected) .has-child:hover { + > .wp-block-navigation-link__container { + opacity: 0; + visibility: hidden; } +} +// Styles for submenu flyout. +.has-child { &.is-selected, &.has-child-selected, // Show the submenu list when is dragging and drag enter the list item. @@ -57,7 +64,7 @@ } } -// IE fix for submenu visibility on parent focus +// IE fix for submenu visibility on parent focus. .is-editing > .wp-block-navigation__container { visibility: visible; opacity: 1; @@ -231,6 +238,12 @@ $color-control-label-height: 20px; position: relative; z-index: 1; + // If an ancestor has a text-decoration property applied, it is inherited regardless of + // the specificity of a child element. Only pulling the child out of the flow fixes it. + // See also https://www.w3.org/TR/CSS21/text.html#propdef-text-decoration. + float: left; + width: 100%; + // Show when selected. .is-selected & { display: flex; diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 79aa4bea2197d0..9b7ef55cbaa143 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -5,21 +5,13 @@ list-style: none; // Overrides generic ".entry-content li" styles on the front end. - margin: 0; padding: 0; } - - // Submenus. - .wp-block-navigation__container { - .wp-block-navigation__container { - align-items: normal; - min-width: 200px; - } - } } +// Navigation block inner container. .wp-block-navigation__container { - // Vertically align child blocks, like Social Links or Search. + // Vertically center child blocks, like Social Links or Search. align-items: center; // Reset the default list styles diff --git a/packages/block-library/src/navigation/use-block-navigator.js b/packages/block-library/src/navigation/use-block-navigator.js index 688fb304af3783..479a54c24e216e 100644 --- a/packages/block-library/src/navigation/use-block-navigator.js +++ b/packages/block-library/src/navigation/use-block-navigator.js @@ -17,7 +17,7 @@ export default function useBlockNavigator( clientId, __experimentalFeatures ) { const navigatorToolbarButton = ( setIsNavigationListOpen( true ) } icon={ listView } /> @@ -25,7 +25,7 @@ export default function useBlockNavigator( clientId, __experimentalFeatures ) { const navigatorModal = isNavigationListOpen && ( { setIsNavigationListOpen( false ); diff --git a/packages/block-library/src/page-list/editor.scss b/packages/block-library/src/page-list/editor.scss index fd6b7f5eee483c..778691f11ebf6b 100644 --- a/packages/block-library/src/page-list/editor.scss +++ b/packages/block-library/src/page-list/editor.scss @@ -4,16 +4,9 @@ .wp-block-page-list { background-color: inherit; } - // Make the dropdown background white if there's no background color set. - &:not(.has-background) { - .submenu-container { - color: $gray-900; - background-color: $white; - } - } } -// Make links unclickable in the editor +// Make links unclickable in the editor. .wp-block-pages-list__item__link { pointer-events: none; } diff --git a/packages/block-library/src/page-list/style.scss b/packages/block-library/src/page-list/style.scss index 9c65b730255b2d..264ebe23387021 100644 --- a/packages/block-library/src/page-list/style.scss +++ b/packages/block-library/src/page-list/style.scss @@ -1,98 +1,27 @@ -.wp-block-page-list__submenu-icon { - display: none; -} - -.show-submenu-icons { - .wp-block-page-list__submenu-icon { - display: block; - padding: 0.375em 1em 0.375em 0; - - svg { - fill: currentColor; - } - } -} - -// The Pages block should inherit navigation styles when nested within it +// The Page List block should inherit navigation styles when nested within it .wp-block-navigation { .wp-block-page-list { display: flex; flex-wrap: wrap; - } - .wp-block-pages-list__item__link { - display: block; - color: inherit; - padding: 0.5em 1em; + background-color: inherit; } - .wp-block-pages-list__item.has-child { - display: flex; - position: relative; + // Menu items generated by the page list do not get `has-[x]-background-color`, + // and must therefore inherit from the parent. + .wp-block-pages-list__item { background-color: inherit; + } - > a { - padding-right: 0.5em; - } - > .submenu-container { - border: $border-width solid rgba(0, 0, 0, 0.15); - background-color: inherit; - color: inherit; - position: absolute; - left: 0; - top: 100%; - width: fit-content; - z-index: 2; - opacity: 0; - transition: opacity 0.1s linear; - visibility: hidden; - - @include break-medium { - left: 1.5em; - - // Nested submenus sit to the left on large breakpoints - .submenu-container { - left: 100%; - top: -1px; - - // Prevent the menu from disappearing when the mouse is over the gap - &::before { - content: ""; - position: absolute; - right: 100%; - height: 100%; - display: block; - width: 0.5em; - background: transparent; - } - } - - .wp-block-navigation-link__submenu-icon svg { - transform: rotate(0); - } - } - } - - &:hover { - cursor: pointer; - - > .submenu-container { - visibility: visible; - opacity: 1; - } - } - - &:focus-within { - cursor: pointer; - - > .submenu-container { - visibility: visible; - opacity: 1; - } - } + // Submenu icon indicator. + // The specific styles for the submenu indicator are inherited from the navigation block style. + .wp-block-page-list__submenu-icon { + display: none; } - .submenu-container { - padding: 0; + .show-submenu-icons { + .wp-block-page-list__submenu-icon { + display: block; + } } } diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 64167c359e2d5c..86c3af60c36721 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -7,9 +7,9 @@ import classnames from 'classnames'; * WordPress dependencies */ import { __, _x, isRTL } from '@wordpress/i18n'; -import { PanelBody, ToggleControl, ToolbarGroup } from '@wordpress/components'; +import { DropdownMenu, PanelBody, ToggleControl } from '@wordpress/components'; import { - AlignmentToolbar, + AlignmentControl, BlockControls, InspectorControls, RichText, @@ -21,10 +21,11 @@ import { formatLtr } from '@wordpress/icons'; const name = 'core/paragraph'; -function ParagraphRTLToolbar( { direction, setDirection } ) { +function ParagraphRTLControl( { direction, setDirection } ) { return ( isRTL() && ( - - - + setAttributes( { align: newAlign } ) } /> - setAttributes( { direction: newDirection } ) diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js index 2c2a3f7552a8ec..a51a708a222767 100644 --- a/packages/block-library/src/paragraph/edit.native.js +++ b/packages/block-library/src/paragraph/edit.native.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { createBlock } from '@wordpress/blocks'; import { - AlignmentToolbar, + AlignmentControl, BlockControls, RichText, store as blockEditorStore, @@ -39,8 +39,8 @@ function ParagraphBlock( { }, [] ); return ( <> - - + $classes ) ); return sprintf( - '
    %2$s
    ', + '
    %2$s
    ', $wrapper_attributes, get_comments_number( $block->context['postId'] ) ); diff --git a/packages/block-library/src/post-content/block.json b/packages/block-library/src/post-content/block.json index 1869af668fd839..0c714599ff48db 100644 --- a/packages/block-library/src/post-content/block.json +++ b/packages/block-library/src/post-content/block.json @@ -8,7 +8,8 @@ ], "supports": { "align": [ "wide", "full" ], - "html": false + "html": false, + "__experimentalLayout": true }, "editorStyle": "wp-block-post-content-editor" } diff --git a/packages/block-library/src/post-content/edit.js b/packages/block-library/src/post-content/edit.js index d4c86853533d89..fcdb14198d7b10 100644 --- a/packages/block-library/src/post-content/edit.js +++ b/packages/block-library/src/post-content/edit.js @@ -2,28 +2,50 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { useBlockProps } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; +import { + useBlockProps, + __experimentalUseInnerBlocksProps as useInnerBlocksProps, + __experimentalUseEditorFeature as useEditorFeature, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { useEntityBlockEditor } from '@wordpress/core-data'; -/** - * Internal dependencies - */ -import PostContentInnerBlocks from './inner-blocks'; +function Content( { layout, postType, postId } ) { + const themeSupportsLayout = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return getSettings()?.supportsLayout; + }, [] ); + const defaultLayout = useEditorFeature( 'layout' ) || {}; + const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const { contentSize, wideSize } = usedLayout; + const alignments = + contentSize || wideSize + ? [ 'wide', 'full' ] + : [ 'left', 'center', 'right' ]; + const [ blocks, onInput, onChange ] = useEntityBlockEditor( + 'postType', + postType, + { id: postId } + ); + const props = useInnerBlocksProps( + useBlockProps( { className: 'entry-content' } ), + { + value: blocks, + onInput, + onChange, + __experimentalLayout: { + type: 'default', + // Find a way to inject this in the support flag code (hooks). + alignments: themeSupportsLayout ? alignments : undefined, + }, + } + ); + return
    ; +} -export default function PostContentEdit( { - context: { postId: contextPostId, postType: contextPostType }, -} ) { +function Placeholder() { const blockProps = useBlockProps(); - if ( contextPostId && contextPostType ) { - return ( -
    - -
    - ); - } - return (
    @@ -32,3 +54,20 @@ export default function PostContentEdit( {
    ); } + +export default function PostContentEdit( { + context: { postId: contextPostId, postType: contextPostType }, + attributes, +} ) { + const { layout = {} } = attributes; + + return contextPostId && contextPostType ? ( + + ) : ( + + ); +} diff --git a/packages/block-library/src/post-content/index.js b/packages/block-library/src/post-content/index.js index 71b64b4cdd6251..97a16b7ba68b63 100644 --- a/packages/block-library/src/post-content/index.js +++ b/packages/block-library/src/post-content/index.js @@ -1,8 +1,8 @@ /** * WordPress dependencies */ -import { _x } from '@wordpress/i18n'; -import { alignJustify as icon } from '@wordpress/icons'; +import { __, _x } from '@wordpress/i18n'; +import { postContent as icon } from '@wordpress/icons'; /** * Internal dependencies @@ -15,6 +15,7 @@ export { metadata, name }; export const settings = { title: _x( 'Post Content', 'block title' ), + description: __( 'Displays the contents of a post or page.' ), icon, edit, }; diff --git a/packages/block-library/src/post-content/inner-blocks.js b/packages/block-library/src/post-content/inner-blocks.js deleted file mode 100644 index e42d00b35d1c95..00000000000000 --- a/packages/block-library/src/post-content/inner-blocks.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEntityBlockEditor } from '@wordpress/core-data'; -import { InnerBlocks } from '@wordpress/block-editor'; - -export default function PostContentInnerBlocks( { postType, postId } ) { - const [ blocks, onInput, onChange ] = useEntityBlockEditor( - 'postType', - postType, - { id: postId } - ); - return ( - - ); -} diff --git a/packages/block-library/src/post-featured-image/edit.js b/packages/block-library/src/post-featured-image/edit.js index 1cb34f50f3e112..9c3cca282d9c94 100644 --- a/packages/block-library/src/post-featured-image/edit.js +++ b/packages/block-library/src/post-featured-image/edit.js @@ -100,7 +100,7 @@ function PostFeaturedImageDisplay( { /> - + { !! media && ( event.preventDefault() } - { ...blockProps } - > - { titleElement } - + + + editEntityRecord( 'postType', postType, postId, { + title: value, + } ) + } + __experimentalVersion={ 2 } + /> + </TagName> ); } diff --git a/packages/block-library/src/query-loop/edit.js b/packages/block-library/src/query-loop/edit.js index 1ccdd47d07b240..78be55e88bfa8b 100644 --- a/packages/block-library/src/query-loop/edit.js +++ b/packages/block-library/src/query-loop/edit.js @@ -18,11 +18,6 @@ import { } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; -/** - * Internal dependencies - */ -import { useQueryContext } from '../query'; - const TEMPLATE = [ [ 'core/post-title' ], [ 'core/post-date' ], @@ -45,12 +40,12 @@ export default function QueryLoopEdit( { sticky, inherit, } = {}, - queryContext = [ {} ], + queryContext = [ { page: 1 } ], templateSlug, layout: { type: layoutType = 'flex', columns = 1 } = {}, }, } ) { - const [ { page } ] = useQueryContext() || queryContext; + const [ { page } ] = queryContext; const [ activeBlockContext, setActiveBlockContext ] = useState(); const { posts, blocks } = useSelect( diff --git a/packages/block-library/src/query-loop/editor.scss b/packages/block-library/src/query-loop/editor.scss index 39a614277d58d7..a5629acbfc4cd3 100644 --- a/packages/block-library/src/query-loop/editor.scss +++ b/packages/block-library/src/query-loop/editor.scss @@ -1,5 +1,5 @@ .wp-block.wp-block-query-loop { - max-width: 100%; padding-left: 0; + margin-left: 0; list-style: none; } diff --git a/packages/block-library/src/query-pagination/block.json b/packages/block-library/src/query-pagination/block.json index 765e4c1f0bcceb..f47bc6f9461922 100644 --- a/packages/block-library/src/query-pagination/block.json +++ b/packages/block-library/src/query-pagination/block.json @@ -4,8 +4,7 @@ "category": "design", "usesContext": [ "queryId", - "query", - "queryContext" + "query" ], "supports": { "reusable": false, diff --git a/packages/block-library/src/query-title/block.json b/packages/block-library/src/query-title/block.json new file mode 100644 index 00000000000000..6635b27d5e2c37 --- /dev/null +++ b/packages/block-library/src/query-title/block.json @@ -0,0 +1,72 @@ +{ + "apiVersion": 2, + "name": "core/query-title", + "category": "design", + "attributes": { + "type": { + "type": "string" + }, + "textAlign": { + "type": "string" + }, + "level": { + "type": "number", + "default": 1 + } + }, + "supports": { + "align": [ "wide", "full" ], + "html": false, + "color": { + "gradients": true + }, + "fontSize": true, + "lineHeight": true, + "__experimentalFontFamily": true, + "__experimentalSelector": { + "core/query-title/h1": { + "title": "h1", + "selector": "h1.wp-block-query-title", + "attributes": { + "level": 1 + } + }, + "core/query-title/h2": { + "title": "h2", + "selector": "h2.wp-block-query-title", + "attributes": { + "level": 2 + } + }, + "core/query-title/h3": { + "title": "h3", + "selector": "h3.wp-block-query-title", + "attributes": { + "level": 3 + } + }, + "core/query-title/h4": { + "title": "h4", + "selector": "h4.wp-block-query-title", + "attributes": { + "level": 4 + } + }, + "core/query-title/h5": { + "title": "h5", + "selector": "h5.wp-block-query-title", + "attributes": { + "level": 5 + } + }, + "core/query-title/h6": { + "title": "h6", + "selector": "h6.wp-block-query-title", + "attributes": { + "level": 6 + } + } + } + }, + "editorStyle": "wp-block-query-title-editor" +} diff --git a/packages/block-library/src/query-title/edit.js b/packages/block-library/src/query-title/edit.js new file mode 100644 index 00000000000000..c57c843bff8e4a --- /dev/null +++ b/packages/block-library/src/query-title/edit.js @@ -0,0 +1,74 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +// import { useSelect, useDispatch } from '@wordpress/data'; +import { + AlignmentToolbar, + BlockControls, + useBlockProps, + Warning, +} from '@wordpress/block-editor'; +import { ToolbarGroup } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import HeadingLevelDropdown from '../heading/heading-level-dropdown'; + +const SUPPORTED_TYPES = [ 'archive' ]; + +export default function QueryTitleEdit( { + attributes: { type, level, textAlign }, + setAttributes, +} ) { + const TagName = `h${ level }`; + const blockProps = useBlockProps( { + className: classnames( { + [ `has-text-align-${ textAlign }` ]: textAlign, + 'wp-block-query-title__placeholder': type === 'archive', + } ), + } ); + // The plan is to augment this block with more + // block variations like `Search Title`. + if ( ! SUPPORTED_TYPES.includes( type ) ) { + return ( + <div { ...blockProps }> + <Warning>{ __( 'Provided type is not supported.' ) }</Warning> + </div> + ); + } + + let titleElement; + if ( type === 'archive' ) { + titleElement = ( + <TagName { ...blockProps }>{ __( 'Archive title' ) }</TagName> + ); + } + return ( + <> + <BlockControls> + <ToolbarGroup> + <HeadingLevelDropdown + selectedLevel={ level } + onChange={ ( newLevel ) => + setAttributes( { level: newLevel } ) + } + /> + </ToolbarGroup> + <AlignmentToolbar + value={ textAlign } + onChange={ ( nextAlign ) => { + setAttributes( { textAlign: nextAlign } ); + } } + /> + </BlockControls> + { titleElement } + </> + ); +} diff --git a/packages/block-library/src/query-title/editor.scss b/packages/block-library/src/query-title/editor.scss new file mode 100644 index 00000000000000..6e4e6eac443d89 --- /dev/null +++ b/packages/block-library/src/query-title/editor.scss @@ -0,0 +1,4 @@ +.wp-block-query-title__placeholder { + padding: 1em 0; + border: 1px dashed; +} diff --git a/packages/block-library/src/query-title/index.js b/packages/block-library/src/query-title/index.js new file mode 100644 index 00000000000000..faa637638952a4 --- /dev/null +++ b/packages/block-library/src/query-title/index.js @@ -0,0 +1,21 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; +import variations from './variations'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: __( 'Query Title' ), + description: __( 'Display the query title.' ), + edit, + variations, +}; diff --git a/packages/block-library/src/query-title/index.php b/packages/block-library/src/query-title/index.php new file mode 100644 index 00000000000000..065186dd60260f --- /dev/null +++ b/packages/block-library/src/query-title/index.php @@ -0,0 +1,49 @@ +<?php +/** + * Server-side rendering of the `core/query-title` block. + * + * @package WordPress + */ + +/** + * Renders the `core/query-title` block on the server. + * For now it only supports Archive title, + * using queried object information + * + * @param array $attributes Block attributes. + * + * @return string Returns the query title based on the queried object. + */ +function render_block_core_query_title( $attributes ) { + $type = isset( $attributes['type'] ) ? $attributes['type'] : null; + $is_archive = is_archive(); + if ( ! $type || ( 'archive' === $type && ! $is_archive ) ) { + return ''; + } + $title = ''; + if ( $is_archive ) { + $title = get_the_archive_title(); + } + $tag_name = isset( $attributes['level'] ) ? 'h' . (int) $attributes['level'] : 'h1'; + $align_class_name = empty( $attributes['textAlign'] ) ? '' : "has-text-align-{$attributes['textAlign']}"; + $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $align_class_name ) ); + return sprintf( + '<%1$s %2$s>%3$s</%1$s>', + $tag_name, + $wrapper_attributes, + $title + ); +} + +/** + * Registers the `core/query-title` block on the server. + */ +function register_block_core_query_title() { + register_block_type_from_metadata( + __DIR__ . '/query-title', + array( + 'render_callback' => 'render_block_core_query_title', + ) + ); +} +add_action( 'init', 'register_block_core_query_title' ); diff --git a/packages/block-library/src/query-title/variations.js b/packages/block-library/src/query-title/variations.js new file mode 100644 index 00000000000000..c82a419602ef75 --- /dev/null +++ b/packages/block-library/src/query-title/variations.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { archiveTitle } from '@wordpress/icons'; +const variations = [ + { + isDefault: true, + name: 'archive-title', + title: __( 'Archive Title' ), + description: __( + 'Display the archive title based on the queried object.' + ), + icon: archiveTitle, + attributes: { + type: 'archive', + }, + scope: [ 'inserter' ], + }, +]; + +/** + * Add `isActive` function to all `query-title` variations, if not defined. + * `isActive` function is used to find a variation match from a created + * Block by providing its attributes. + */ +variations.forEach( ( variation ) => { + if ( variation.isActive ) return; + variation.isActive = ( blockAttributes, variationAttributes ) => + blockAttributes.type === variationAttributes.type; +} ); + +export default variations; diff --git a/packages/block-library/src/query/edit/index.js b/packages/block-library/src/query/edit/index.js index 8ca57e39e52680..05f60afcf39afb 100644 --- a/packages/block-library/src/query/edit/index.js +++ b/packages/block-library/src/query/edit/index.js @@ -15,7 +15,6 @@ import { * Internal dependencies */ import QueryToolbar from './query-toolbar'; -import QueryProvider from './query-provider'; import QueryInspectorControls from './query-inspector-controls'; import QueryBlockSetup from './query-block-setup'; import { DEFAULTS_POSTS_PER_PAGE } from '../constants'; @@ -71,9 +70,7 @@ export function QueryContent( { attributes, setAttributes } ) { /> </BlockControls> <div { ...blockProps }> - <QueryProvider> - <div { ...innerBlocksProps } /> - </QueryProvider> + <div { ...innerBlocksProps } /> </div> </> ); @@ -91,4 +88,3 @@ const QueryEdit = ( props ) => { }; export default QueryEdit; -export * from './query-provider'; diff --git a/packages/block-library/src/query/edit/query-provider.js b/packages/block-library/src/query/edit/query-provider.js deleted file mode 100644 index 01aa8cecdb98e8..00000000000000 --- a/packages/block-library/src/query/edit/query-provider.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createContext, - useState, - useMemo, - useContext, -} from '@wordpress/element'; - -const QueryContext = createContext(); -export default function QueryProvider( { children } ) { - const [ queryContext, setQueryContext ] = useState( { page: 1 } ); - return ( - <QueryContext.Provider - value={ useMemo( - () => [ - queryContext, - ( newQueryContext ) => - setQueryContext( ( currentQueryContext ) => ( { - ...currentQueryContext, - ...newQueryContext, - } ) ), - ], - [ queryContext ] - ) } - > - { children } - </QueryContext.Provider> - ); -} - -export function useQueryContext() { - return useContext( QueryContext ); -} diff --git a/packages/block-library/src/query/editor.scss b/packages/block-library/src/query/editor.scss index 3803d1b1e9fac3..620f96e3d8be43 100644 --- a/packages/block-library/src/query/editor.scss +++ b/packages/block-library/src/query/editor.scss @@ -1,7 +1,3 @@ -.editor-styles-wrapper .wp-block.wp-block-query { - max-width: 100%; -} - .block-library-query-toolbar__popover .components-popover__content { min-width: 230px; } diff --git a/packages/block-library/src/query/index.js b/packages/block-library/src/query/index.js index 62b8404c8b4f14..317fd1b0445739 100644 --- a/packages/block-library/src/query/index.js +++ b/packages/block-library/src/query/index.js @@ -23,5 +23,3 @@ export const settings = { save, variations, }; - -export { useQueryContext } from './edit'; diff --git a/packages/block-library/src/quote/edit.js b/packages/block-library/src/quote/edit.js index 8064aca0eef303..bc94820b5fc661 100644 --- a/packages/block-library/src/quote/edit.js +++ b/packages/block-library/src/quote/edit.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; */ import { __ } from '@wordpress/i18n'; import { - AlignmentToolbar, + AlignmentControl, BlockControls, RichText, useBlockProps, @@ -36,8 +36,8 @@ export default function QuoteEdit( { return ( <> - <BlockControls> - <AlignmentToolbar + <BlockControls group="block"> + <AlignmentControl value={ align } onChange={ ( nextAlign ) => { setAttributes( { align: nextAlign } ); diff --git a/packages/block-library/src/reset.scss b/packages/block-library/src/reset.scss new file mode 100644 index 00000000000000..934545ad1b8060 --- /dev/null +++ b/packages/block-library/src/reset.scss @@ -0,0 +1,101 @@ +/** + * Editor Normalization Styles + * + * These are only output in the editor, but styles here are prefixed .editor-styles-wrapper and affect the theming + * of the editor by themes. + */ + +.editor-styles-wrapper { + padding: 10px; + + /** + * The following styles revert to the browser defaults overriding the WPAdmin styles. + * This is only needed while the block editor is not being loaded in an iframe. + */ + font-family: serif; // unfortunately initial doesn't work for font-family. + font-size: initial; + line-height: initial; + color: initial; + + // For full-wide blocks, we compensate for these 10px. + .block-editor-block-list__layout.is-root-container > .wp-block[data-align="full"] { + margin-left: -10px; + margin-right: -10px; + } + + .wp-align-wrapper { + max-width: $content-width; + + > .wp-block, + &.wp-align-full { + max-width: none; + } + + &.wp-align-wide { + max-width: $content-width; + } + } + + a { + // This inherits the blue link color set by wp-admin, which is unfortunate. + // However both inherit and unset properties set the color to black. + transition: none; + } + + code, + kbd { + padding: 0; + margin: 0; + background: inherit; + font-size: inherit; + font-family: monospace; + } + + p { + font-size: revert; + line-height: revert; + margin: revert; + } + + ul, + ol { + margin: revert; + padding: revert; + + // Remove bottom margin from nested lists. + ul, + ol { + margin: revert; + } + + li { + margin: revert; + } + } + + ul { + list-style-type: revert; + } + + ol { + list-style-type: revert; + } + + ul ul, + ol ul { + list-style-type: revert; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + font-size: revert; + margin: revert; + color: revert; + line-height: revert; + font-weight: revert; + } +} diff --git a/packages/block-library/src/search/button-position-dropdown.native.js b/packages/block-library/src/search/button-position-dropdown.native.js deleted file mode 100644 index b6e4537e290438..00000000000000 --- a/packages/block-library/src/search/button-position-dropdown.native.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { DropdownMenu } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { buttonOutside, buttonInside, noButton } from './icons'; - -const BUTTON_OPTIONS = [ - 'button-inside', - 'button-outside', - 'no-button', - 'button-only', -]; - -/** - * Creates a custom dropdown menu for the button position options. - * - * @param {Object} Component Component properties. - * @param {string} Component.selectedOption The selected option. - * @param {Function} Component.onChange Function to handle a change - * in the selected option. - * - * @return {DropdownMenu} Dropdown menu for use inside {ToolbarGroup} - */ -export default function ButtonPositionDropdown( { selectedOption, onChange } ) { - const getButtonIcon = ( option ) => { - switch ( option ) { - case 'button-inside': - return buttonInside; - case 'button-outside': - return buttonOutside; - case 'no-button': - return noButton; - } - }; - - const getButtonTitle = ( option ) => { - switch ( option ) { - case 'button-inside': - return __( 'Button Inside' ); - case 'button-outside': - return __( 'Button Outside' ); - case 'no-button': - return __( 'No Button' ); - } - }; - - const createOptionControl = ( - targetOption, - activeOption, - title, - onChangeCallback - ) => { - const isActive = targetOption === activeOption; - - return { - icon: getButtonIcon( targetOption ), - title, - isActive, - onClick: () => onChangeCallback( targetOption ), - }; - }; - - return ( - <DropdownMenu - icon={ getButtonIcon( selectedOption ) } - controls={ BUTTON_OPTIONS.map( ( option ) => - createOptionControl( - option, - selectedOption, - getButtonTitle( option ), - onChange - ) - ) } - label={ __( 'Change button position' ) } - /> - ); -} diff --git a/packages/block-library/src/search/edit.native.js b/packages/block-library/src/search/edit.native.js index 8c4f9035638830..be8b24ebad879a 100644 --- a/packages/block-library/src/search/edit.native.js +++ b/packages/block-library/src/search/edit.native.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { View, TextInput } from 'react-native'; +import { View } from 'react-native'; import classnames from 'classnames'; /** @@ -9,41 +9,51 @@ import classnames from 'classnames'; */ import { RichText, - BlockControls, + PlainText, useBlockProps, InspectorControls, } from '@wordpress/block-editor'; import { - ToolbarGroup, - ToolbarButton, Button, PanelBody, - UnitControl, + SelectControl, + ToggleControl, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { search } from '@wordpress/icons'; +import { useRef, useEffect, useState } from '@wordpress/element'; /** * Internal dependencies */ -import { buttonWithIcon, toggleLabel } from './icons'; -import ButtonPositionDropdown from './button-position-dropdown'; import styles from './style.scss'; import richTextStyles from './rich-text.scss'; -import { - isPercentageUnit, - CSS_UNITS, - MIN_WIDTH, - PC_WIDTH_DEFAULT, - PX_WIDTH_DEFAULT, -} from './utils.js'; /** * Constants */ const MIN_BUTTON_WIDTH = 100; +const BUTTON_OPTIONS = [ + { value: 'button-inside', label: __( 'Button inside' ) }, + { value: 'button-outside', label: __( 'Button outside' ) }, + { value: 'no-button', label: __( 'No button' ) }, +]; + +export default function SearchEdit( { + onFocus, + isSelected, + attributes, + setAttributes, + className, +} ) { + const [ isButtonSelected, setIsButtonSelected ] = useState( false ); + const [ isLabelSelected, setIsLabelSelected ] = useState( false ); + const [ isPlaceholderSelected, setIsPlaceholderSelected ] = useState( + true + ); + + const textInputRef = useRef( null ); -export default function SearchEdit( { attributes, setAttributes, className } ) { const { label, showLabel, @@ -51,29 +61,20 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { buttonUseIcon, placeholder, buttonText, - width = 100, - widthUnit = '%', } = attributes; - const onChange = ( nextWidth ) => { - if ( isPercentageUnit( widthUnit ) || ! widthUnit ) { - return; + /* + * Called when the value of isSelected changes. Blurs the PlainText component + * used by the placeholder when this block loses focus. + */ + useEffect( () => { + if ( hasTextInput() && isPlaceholderSelected && ! isSelected ) { + textInputRef.current.blur(); } - onChangeWidth( nextWidth ); - }; - - const onChangeWidth = ( nextWidth ) => { - setAttributes( { - width: nextWidth, - widthUnit, - } ); - }; + }, [ isSelected ] ); - const onChangeUnit = ( nextUnit ) => { - setAttributes( { - width: '%' === nextUnit ? PC_WIDTH_DEFAULT : PX_WIDTH_DEFAULT, - widthUnit: nextUnit, - } ); + const hasTextInput = () => { + return textInputRef && textInputRef.current; }; const getBlockClassNames = () => { @@ -100,65 +101,57 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { ); }; + const getSelectedButtonPositionLabel = ( option ) => { + switch ( option ) { + case 'button-inside': + return __( 'Inside' ); + case 'button-outside': + return __( 'Outside' ); + case 'no-button': + return __( 'No button' ); + } + }; + const blockProps = useBlockProps( { className: getBlockClassNames(), } ); const controls = ( - <> - <BlockControls> - <ToolbarGroup> - <ToolbarButton - title={ __( 'Toggle search label' ) } - icon={ toggleLabel } - onClick={ () => { - setAttributes( { - showLabel: ! showLabel, - } ); - } } - isActive={ showLabel } - /> - - <ButtonPositionDropdown - selectedOption={ buttonPosition } - onChange={ ( position ) => { + <InspectorControls> + <PanelBody title={ __( 'Search settings' ) }> + <ToggleControl + label={ __( 'Hide search heading' ) } + checked={ ! showLabel } + onChange={ () => { + setAttributes( { + showLabel: ! showLabel, + } ); + } } + /> + <SelectControl + label={ __( 'Button position' ) } + value={ getSelectedButtonPositionLabel( buttonPosition ) } + onChange={ ( position ) => { + setAttributes( { + buttonPosition: position, + } ); + } } + options={ BUTTON_OPTIONS } + hideCancelButton={ true } + /> + { buttonPosition !== 'no-button' && ( + <ToggleControl + label={ __( 'Use icon button' ) } + checked={ buttonUseIcon } + onChange={ () => { setAttributes( { - buttonPosition: position, + buttonUseIcon: ! buttonUseIcon, } ); } } /> - - { 'no-button' !== buttonPosition && ( - <ToolbarButton - title={ __( 'Use button with icon' ) } - icon={ buttonWithIcon } - onClick={ () => { - setAttributes( { - buttonUseIcon: ! buttonUseIcon, - } ); - } } - isActive={ buttonUseIcon } - /> - ) } - </ToolbarGroup> - </BlockControls> - <InspectorControls> - <PanelBody title={ __( 'Search Settings' ) }> - <UnitControl - label={ __( 'Width' ) } - min={ widthUnit === '%' ? 1 : MIN_WIDTH } - max={ isPercentageUnit( widthUnit ) ? 100 : undefined } - decimalNum={ 1 } - units={ CSS_UNITS } - unit={ widthUnit } - onChange={ onChange } - onComplete={ onChangeWidth } - onUnitChange={ onChangeUnit } - value={ parseFloat( width ) } - /> - </PanelBody> - </InspectorControls> - </> + ) } + </PanelBody> + </InspectorControls> ); const mergeWithBorderStyle = ( style ) => { @@ -172,7 +165,9 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { : mergeWithBorderStyle( styles.searchTextInput ); return ( - <TextInput + <PlainText + ref={ textInputRef } + isSelected={ isPlaceholderSelected } className="wp-block-search__input" style={ inputStyle } numberOfLines={ 1 } @@ -182,9 +177,14 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { placeholder={ placeholder ? undefined : __( 'Optional placeholderโ€ฆ' ) } - onChangeText={ ( newVal ) => + onChange={ ( newVal ) => setAttributes( { placeholder: newVal } ) } + onFocus={ () => { + setIsPlaceholderSelected( true ); + onFocus(); + } } + onBlur={ () => setIsPlaceholderSelected( false ) } /> ); }; @@ -196,22 +196,32 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { <Button className="wp-block-search__button" icon={ search } + onFocus={ onFocus } /> ) } { ! buttonUseIcon && ( <RichText className="wp-block-search__button" + identifier="text" + tagName="p" style={ richTextStyles.searchButton } placeholder={ __( 'Add button text' ) } value={ buttonText } - identifier="text" withoutInteractiveFormatting onChange={ ( html ) => setAttributes( { buttonText: html } ) } minWidth={ MIN_BUTTON_WIDTH } textAlign="center" + isSelected={ isButtonSelected } + __unstableMobileNoFocusOnMount={ ! isSelected } + unstableOnFocus={ () => { + setIsButtonSelected( true ); + } } + onBlur={ () => { + setIsButtonSelected( false ); + } } /> ) } </View> @@ -225,11 +235,13 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { return ( <View { ...blockProps } style={ styles.searchBlockContainer }> - { controls } + { isSelected && controls } { showLabel && ( <RichText className="wp-block-search__label" + identifier="text" + tagName="p" style={ { ...styles.searchLabel, ...richTextStyles.searchLabel, @@ -239,6 +251,14 @@ export default function SearchEdit( { attributes, setAttributes, className } ) { withoutInteractiveFormatting value={ label } onChange={ ( html ) => setAttributes( { label: html } ) } + isSelected={ isLabelSelected } + __unstableMobileNoFocusOnMount={ ! isSelected } + unstableOnFocus={ () => { + setIsLabelSelected( true ); + } } + onBlur={ () => { + setIsLabelSelected( false ); + } } /> ) } diff --git a/packages/block-library/src/site-logo/edit.js b/packages/block-library/src/site-logo/edit.js index db600b589da6b3..27023d7489be9f 100644 --- a/packages/block-library/src/site-logo/edit.js +++ b/packages/block-library/src/site-logo/edit.js @@ -16,7 +16,6 @@ import { RangeControl, ResizableBox, Spinner, - ToolbarGroup, } from '@wordpress/components'; import { useViewportMatch } from '@wordpress/compose'; import { @@ -284,19 +283,15 @@ export default function LogoEdit( { setError( message[ 2 ] ? message[ 2 ] : null ); }; - const controls = ( - <BlockControls> - <ToolbarGroup> - { logoUrl && ( - <MediaReplaceFlow - mediaURL={ logoUrl } - allowedTypes={ ALLOWED_MEDIA_TYPES } - accept={ ACCEPT_MEDIA_STRING } - onSelect={ onSelectLogo } - onError={ onUploadError } - /> - ) } - </ToolbarGroup> + const controls = logoUrl && ( + <BlockControls group="other"> + <MediaReplaceFlow + mediaURL={ logoUrl } + allowedTypes={ ALLOWED_MEDIA_TYPES } + accept={ ACCEPT_MEDIA_STRING } + onSelect={ onSelectLogo } + onError={ onUploadError } + /> </BlockControls> ); @@ -347,7 +342,6 @@ export default function LogoEdit( { const classes = classnames( className, { 'is-resized': !! width, - 'is-focused': isSelected, } ); const blockProps = useBlockProps( { diff --git a/packages/block-library/src/site-title/block.json b/packages/block-library/src/site-title/block.json index 91c346cac52fc0..b237bc8cafc84c 100644 --- a/packages/block-library/src/site-title/block.json +++ b/packages/block-library/src/site-title/block.json @@ -15,11 +15,14 @@ "align": [ "wide", "full" ], "html": false, "color": { - "gradients": true + "gradients": true, + "text": false, + "link": true }, "fontSize": true, "lineHeight": true, "__experimentalFontFamily": true, + "__experimentalSelector": ".wp-block-site-title, .wp-block-site-title > a", "__experimentalTextTransform": true } } diff --git a/packages/block-library/src/site-title/edit/index.js b/packages/block-library/src/site-title/edit/index.js index 8feb6d4a4611d9..f7a0640bdf74b9 100644 --- a/packages/block-library/src/site-title/edit/index.js +++ b/packages/block-library/src/site-title/edit/index.js @@ -23,13 +23,12 @@ import LevelToolbar from './level-toolbar'; export default function SiteTitleEdit( { attributes, setAttributes } ) { const { level, textAlign } = attributes; const [ title, setTitle ] = useEntityProp( 'root', 'site', 'title' ); - const tagName = level === 0 ? 'p' : `h${ level }`; + const TagName = level === 0 ? 'p' : `h${ level }`; const blockProps = useBlockProps( { className: classnames( { [ `has-text-align-${ textAlign }` ]: textAlign, } ), } ); - return ( <> <BlockControls> @@ -47,17 +46,17 @@ export default function SiteTitleEdit( { attributes, setAttributes } ) { } /> </BlockControls> - - <RichText - tagName={ tagName } - aria-label={ __( 'Site title text' ) } - placeholder={ __( 'Write site titleโ€ฆ' ) } - value={ title } - onChange={ setTitle } - allowedFormats={ [] } - disableLineBreaks - { ...blockProps } - /> + <TagName { ...blockProps }> + <RichText + tagName="a" + aria-label={ __( 'Site title text' ) } + placeholder={ __( 'Write site titleโ€ฆ' ) } + value={ title } + onChange={ setTitle } + allowedFormats={ [] } + disableLineBreaks + /> + </TagName> </> ); } diff --git a/packages/block-library/src/social-link/index.php b/packages/block-library/src/social-link/index.php index 4f8893511d48f3..c4f78f5dc4e11a 100644 --- a/packages/block-library/src/social-link/index.php +++ b/packages/block-library/src/social-link/index.php @@ -17,9 +17,14 @@ function render_block_core_social_link( $attributes, $content, $block ) { $open_in_new_tab = isset( $block->context['openInNewTab'] ) ? $block->context['openInNewTab'] : false; - $service = ( isset( $attributes['service'] ) ) ? $attributes['service'] : 'Icon'; - $url = ( isset( $attributes['url'] ) ) ? $attributes['url'] : false; - $label = ( isset( $attributes['label'] ) ) ? $attributes['label'] : block_core_social_link_get_name( $service ); + $service = ( isset( $attributes['service'] ) ) ? $attributes['service'] : 'Icon'; + $url = ( isset( $attributes['url'] ) ) ? $attributes['url'] : false; + $label = ( isset( $attributes['label'] ) ) ? $attributes['label'] : sprintf( + /* translators: %1$s: Social-network name. %2$s: URL. */ + __( '%1$s: %2$s', 'gutenberg' ), + block_core_social_link_get_name( $service ), + $url + ); $class_name = isset( $attributes['className'] ) ? ' ' . $attributes['className'] : false; // Don't render a link if there is no URL set. diff --git a/packages/block-library/src/social-links/editor.scss b/packages/block-library/src/social-links/editor.scss index 0f345225d6ecd4..a75e5e78e50f0e 100644 --- a/packages/block-library/src/social-links/editor.scss +++ b/packages/block-library/src/social-links/editor.scss @@ -23,12 +23,8 @@ @include reduce-motion("transition"); .is-selected > & { - opacity: 0.1; - } - - // Show more opaque in dark themes. - .is-dark-theme .is-selected & { - opacity: 0.4; + opacity: 0; + width: 0; // This allows the appender to sit on the left as it should. } // Use the first link to set the height. @@ -57,6 +53,10 @@ } } + & + .block-list-appender { + box-shadow: inset 0 0 0 $border-width $gray-700; + } + .wp-social-link::before { content: ""; display: block; @@ -71,30 +71,45 @@ } // Polish the Appender. -.wp-block-social-links .wp-block-social-links__social-placeholder + .block-list-appender { - position: absolute; -} - .wp-block-social-links .block-list-appender { - padding: 0; + // Match the overall silhouette of social links. + margin: 4px auto 4px 0; + border-radius: 9999px; // This works as both circular and pill shapes. - &::before { - content: ""; - display: block; + // Treat just like a social icon. + .block-editor-inserter { + display: flex; + align-items: center; + justify-content: center; + font-size: inherit; width: 1em; height: 1em; } - .block-editor-inserter { - position: absolute; - top: 0; + // Duplicate the font sizes from style.scss to size the appender. + .has-small-icon-size & { + font-size: 16px; // 16 makes for a quarter-padding that keeps the icon centered. } -} -.wp-block-social-links.is-style-logos-only .block-list-appender { - padding: 4px; -} + // Normal/default. + .has-normal-icon-size & { + font-size: 24px; + } + + // Large. + .has-large-icon-size & { + font-size: 36px; + } + // Huge. + .has-huge-icon-size & { + font-size: 48px; + } + + &::before { + content: none; + } +} // Center flex items. This has an equivalent in style.scss. .wp-block[data-align="center"] > .wp-block-social-links { diff --git a/packages/block-library/src/template-part/block.json b/packages/block-library/src/template-part/block.json index 6e5c9835cbf7da..8ffb9be3fe1ce0 100644 --- a/packages/block-library/src/template-part/block.json +++ b/packages/block-library/src/template-part/block.json @@ -1,7 +1,7 @@ { "apiVersion": 2, "name": "core/template-part", - "category": "design", + "category": "theme", "attributes": { "slug": { "type": "string" @@ -11,6 +11,9 @@ }, "tagName": { "type": "string" + }, + "area": { + "type": "string" } }, "supports": { @@ -19,7 +22,8 @@ "color": { "gradients": true, "link": true - } + }, + "__experimentalLayout": true }, "editorStyle": "wp-block-template-part-editor" } diff --git a/packages/block-library/src/template-part/edit/get-tag-based-on-area.js b/packages/block-library/src/template-part/edit/get-tag-based-on-area.js index 82c901deeed157..aa38fbd2d22159 100644 --- a/packages/block-library/src/template-part/edit/get-tag-based-on-area.js +++ b/packages/block-library/src/template-part/edit/get-tag-based-on-area.js @@ -1,9 +1,9 @@ const AREA_TAGS = { footer: 'footer', header: 'header', - unactegorized: 'div', + uncategorized: 'div', }; export function getTagBasedOnArea( area ) { - return AREA_TAGS[ area ] || AREA_TAGS.unactegorized; + return AREA_TAGS[ area ] || AREA_TAGS.uncategorized; } diff --git a/packages/block-library/src/template-part/edit/index.js b/packages/block-library/src/template-part/edit/index.js index ab9b9a76c6ea79..b3673a17775b8e 100644 --- a/packages/block-library/src/template-part/edit/index.js +++ b/packages/block-library/src/template-part/edit/index.js @@ -15,7 +15,7 @@ import { ToolbarButton, Spinner, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; /** @@ -28,10 +28,11 @@ import { TemplatePartAdvancedControls } from './advanced-controls'; import { getTagBasedOnArea } from './get-tag-based-on-area'; export default function TemplatePartEdit( { - attributes: { slug, theme, tagName }, + attributes, setAttributes, clientId, } ) { + const { slug, theme, tagName, layout = {} } = attributes; const templatePartId = theme && slug ? theme + '//' + slug : null; const [ hasAlreadyRendered, RecursionProvider ] = useNoRecursiveRenders( @@ -75,7 +76,7 @@ export default function TemplatePartEdit( { const blockProps = useBlockProps(); const isPlaceholder = ! slug; - const isEntityAvailable = ! isPlaceholder && ! isMissing; + const isEntityAvailable = ! isPlaceholder && ! isMissing && isResolved; const TagName = tagName || getTagBasedOnArea( area ); // We don't want to render a missing state if we have any inner blocks. @@ -87,8 +88,12 @@ export default function TemplatePartEdit( { return ( <TagName { ...blockProps }> <Warning> - { __( - 'Template part has been deleted or is unavailable.' + { sprintf( + /* translators: %s: Template part slug */ + __( + 'Template part has been deleted or is unavailable: %s' + ), + slug ) } </Warning> </TagName> @@ -113,49 +118,57 @@ export default function TemplatePartEdit( { isEntityAvailable={ isEntityAvailable } templatePartId={ templatePartId } /> - <TagName { ...blockProps }> - { isPlaceholder && ( + { isPlaceholder && ( + <TagName { ...blockProps }> <TemplatePartPlaceholder + area={ attributes.area } setAttributes={ setAttributes } innerBlocks={ innerBlocks } /> - ) } - { isEntityAvailable && ( - <BlockControls> - <ToolbarGroup className="wp-block-template-part__block-control-group"> - <Dropdown - className="wp-block-template-part__preview-dropdown-button" - contentClassName="wp-block-template-part__preview-dropdown-content" - position="bottom right left" - renderToggle={ ( { isOpen, onToggle } ) => ( - <ToolbarButton - aria-expanded={ isOpen } - onClick={ onToggle } - // Disable when open to prevent odd FireFox bug causing reopening. - // As noted in https://github.com/WordPress/gutenberg/pull/24990#issuecomment-689094119 . - disabled={ isOpen } - > - { __( 'Replace' ) } - </ToolbarButton> - ) } - renderContent={ ( { onClose } ) => ( - <TemplatePartSelection - setAttributes={ setAttributes } - onClose={ onClose } - /> - ) } - /> - </ToolbarGroup> - </BlockControls> - ) } - { isEntityAvailable && ( - <TemplatePartInnerBlocks - postId={ templatePartId } - hasInnerBlocks={ innerBlocks.length > 0 } - /> - ) } - { ! isPlaceholder && ! isResolved && <Spinner /> } - </TagName> + </TagName> + ) } + { isEntityAvailable && ( + <BlockControls> + <ToolbarGroup className="wp-block-template-part__block-control-group"> + <Dropdown + className="wp-block-template-part__preview-dropdown-button" + contentClassName="wp-block-template-part__preview-dropdown-content" + position="bottom right left" + renderToggle={ ( { isOpen, onToggle } ) => ( + <ToolbarButton + aria-expanded={ isOpen } + onClick={ onToggle } + // Disable when open to prevent odd FireFox bug causing reopening. + // As noted in https://github.com/WordPress/gutenberg/pull/24990#issuecomment-689094119 . + disabled={ isOpen } + > + { __( 'Replace' ) } + </ToolbarButton> + ) } + renderContent={ ( { onClose } ) => ( + <TemplatePartSelection + setAttributes={ setAttributes } + onClose={ onClose } + /> + ) } + /> + </ToolbarGroup> + </BlockControls> + ) } + { isEntityAvailable && ( + <TemplatePartInnerBlocks + tagName={ TagName } + blockProps={ blockProps } + postId={ templatePartId } + hasInnerBlocks={ innerBlocks.length > 0 } + layout={ layout } + /> + ) } + { ! isPlaceholder && ! isResolved && ( + <TagName { ...blockProps }> + <Spinner /> + </TagName> + ) } </RecursionProvider> ); } diff --git a/packages/block-library/src/template-part/edit/inner-blocks.js b/packages/block-library/src/template-part/edit/inner-blocks.js index 9b5e2af21428b0..6ac67ee109faa3 100644 --- a/packages/block-library/src/template-part/edit/inner-blocks.js +++ b/packages/block-library/src/template-part/edit/inner-blocks.js @@ -5,27 +5,46 @@ import { useEntityBlockEditor } from '@wordpress/core-data'; import { InnerBlocks, __experimentalUseInnerBlocksProps as useInnerBlocksProps, + __experimentalUseEditorFeature as useEditorFeature, + store as blockEditorStore, } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; export default function TemplatePartInnerBlocks( { postId: id, hasInnerBlocks, + layout, + tagName: TagName, + blockProps, } ) { + const themeSupportsLayout = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return getSettings()?.supportsLayout; + }, [] ); + const defaultLayout = useEditorFeature( 'layout' ) || {}; + const usedLayout = !! layout && layout.inherit ? defaultLayout : layout; + const { contentSize, wideSize } = usedLayout; + const alignments = + contentSize || wideSize + ? [ 'wide', 'full' ] + : [ 'left', 'center', 'right' ]; const [ blocks, onInput, onChange ] = useEntityBlockEditor( 'postType', 'wp_template_part', { id } ); - const innerBlocksProps = useInnerBlocksProps( - {}, - { - value: blocks, - onInput, - onChange, - renderAppender: hasInnerBlocks - ? undefined - : InnerBlocks.ButtonBlockAppender, - } - ); - return <div { ...innerBlocksProps } />; + const innerBlocksProps = useInnerBlocksProps( blockProps, { + value: blocks, + onInput, + onChange, + renderAppender: hasInnerBlocks + ? undefined + : InnerBlocks.ButtonBlockAppender, + __experimentalLayout: { + type: 'default', + // Find a way to inject this in the support flag code (hooks). + alignments: themeSupportsLayout ? alignments : undefined, + }, + } ); + return <TagName { ...innerBlocksProps } />; } diff --git a/packages/block-library/src/template-part/edit/placeholder/index.js b/packages/block-library/src/template-part/edit/placeholder/index.js index caaa42e35ea601..4a8462b278b9c7 100644 --- a/packages/block-library/src/template-part/edit/placeholder/index.js +++ b/packages/block-library/src/template-part/edit/placeholder/index.js @@ -15,26 +15,36 @@ import { store as coreStore } from '@wordpress/core-data'; import TemplatePartSelection from '../selection'; export default function TemplatePartPlaceholder( { + area, setAttributes, innerBlocks, } ) { const { saveEntityRecord } = useDispatch( coreStore ); const onCreate = useCallback( async () => { const title = __( 'Untitled Template Part' ); + // If we have `area` set from block attributes, means an exposed + // block variation was inserted. So add this prop to the template + // part entity on creation. Afterwards remove `area` value from + // block attributes. + const record = { + title, + slug: 'template-part', + content: serialize( innerBlocks ), + // `area` is filterable on the server and defaults to `UNCATEGORIZED` + // if provided value is not allowed. + area, + }; const templatePart = await saveEntityRecord( 'postType', 'wp_template_part', - { - title, - slug: 'template-part', - content: serialize( innerBlocks ), - } + record ); setAttributes( { slug: templatePart.slug, theme: templatePart.theme, + area: undefined, } ); - }, [ setAttributes ] ); + }, [ setAttributes, area ] ); return ( <Placeholder diff --git a/packages/block-library/src/template-part/edit/selection/template-part-previews.js b/packages/block-library/src/template-part/edit/selection/template-part-previews.js index 2e1e8b9889e48c..416f19fc203d30 100644 --- a/packages/block-library/src/template-part/edit/selection/template-part-previews.js +++ b/packages/block-library/src/template-part/edit/selection/template-part-previews.js @@ -49,7 +49,7 @@ function TemplatePartItem( { const { createSuccessNotice } = useDispatch( noticesStore ); const onClick = useCallback( () => { - setAttributes( { slug, theme } ); + setAttributes( { slug, theme, area: undefined } ); createSuccessNotice( sprintf( /* translators: %s: template part title. */ diff --git a/packages/block-library/src/template-part/editor.scss b/packages/block-library/src/template-part/editor.scss index 393d62d97d6c55..c61e0651ae7251 100644 --- a/packages/block-library/src/template-part/editor.scss +++ b/packages/block-library/src/template-part/editor.scss @@ -64,15 +64,16 @@ // Ensures a border is present when a child block is selected. .block-editor-block-list__block[data-type="core/template-part"] { - &.is-selected, &.has-child-selected { &::after { - top: $border-width; - bottom: $border-width; - left: $border-width; - right: $border-width; - border-radius: $radius-block-ui - $border-width; // Border is outset, so so subtract the width to achieve correct radius. - box-shadow: 0 0 0 $border-width $gray-900; + border: $border-width dotted $gray-900; + } + + &.is-hovered, + &.is-highlighted { + &::after { + border: none; + } } } } diff --git a/packages/block-library/src/template-part/index.js b/packages/block-library/src/template-part/index.js index d47caa88f63390..46a0e5bc684fce 100644 --- a/packages/block-library/src/template-part/index.js +++ b/packages/block-library/src/template-part/index.js @@ -9,6 +9,7 @@ import { startCase } from 'lodash'; import { store as coreDataStore } from '@wordpress/core-data'; import { select } from '@wordpress/data'; import { __, _x } from '@wordpress/i18n'; +import { layout } from '@wordpress/icons'; /** * Internal dependencies @@ -25,6 +26,7 @@ export const settings = { description: __( 'Edit the different global regions of your site, like the header, footer, sidebar, or create your own.' ), + icon: layout, keywords: [ __( 'template part' ) ], __experimentalLabel: ( { slug, theme } ) => { // Attempt to find entity title if block is a template part. diff --git a/packages/block-library/src/template-part/index.php b/packages/block-library/src/template-part/index.php index df64764e2b2750..c11263a3b16a69 100644 --- a/packages/block-library/src/template-part/index.php +++ b/packages/block-library/src/template-part/index.php @@ -65,8 +65,12 @@ function render_block_core_template_part( $attributes ) { } } - if ( ! $template_part_id || is_null( $content ) ) { - return __( 'Template Part not found.' ); + if ( is_null( $content ) && is_user_logged_in() ) { + return sprintf( + /* translators: %s: Template part slug. */ + __( 'Template part has been deleted or is unavailable: %s' ), + $attributes['slug'] + ); } if ( isset( $seen_ids[ $template_part_id ] ) ) { diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index 86cf023b2f497a..52fddbb53ae22c 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -6,20 +6,6 @@ import { store as coreDataStore } from '@wordpress/core-data'; import { select } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -const createIsActiveBasedOnArea = ( area ) => ( { theme, slug } ) => { - if ( ! slug ) { - return false; - } - - const entity = select( coreDataStore ).getEntityRecord( - 'postType', - 'wp_template_part', - `${ theme }//${ slug }` - ); - - return entity?.area === area; -}; - const variations = [ { name: 'header', @@ -28,8 +14,8 @@ const variations = [ "The header template defines a page area that typically contains a title, logo, and main navigation. Since it's a global element it can be present across all pages and posts." ), icon: header, - isActive: createIsActiveBasedOnArea( 'header' ), - scope: [], + attributes: { area: 'header' }, + scope: [ 'inserter' ], }, { name: 'footer', @@ -38,9 +24,33 @@ const variations = [ "The footer template defines a page area that typically contains site credits, social links, or any other combination of blocks. Since it's a global element it can be present across all pages and posts." ), icon: footer, - isActive: createIsActiveBasedOnArea( 'footer' ), - scope: [], + attributes: { area: 'footer' }, + scope: [ 'inserter' ], }, ]; +/** + * Add `isActive` function to all `Template Part` variations, if not defined. + * `isActive` function is used to find a variation match from a created + * Block by providing its attributes. + */ +variations.forEach( ( variation ) => { + if ( variation.isActive ) return; + variation.isActive = ( blockAttributes, variationAttributes ) => { + const { area, theme, slug } = blockAttributes; + // We first check the `area` block attribute which is set during insertion. + // This property is removed on the creation of a template part. + if ( area ) return area === variationAttributes.area; + // Find a matching variation from the created template part + // by checking the entity's `area` property. + if ( ! slug ) return false; + const entity = select( coreDataStore ).getEntityRecord( + 'postType', + 'wp_template_part', + `${ theme }//${ slug }` + ); + return entity?.area === variationAttributes.area; + }; +} ); + export default variations; diff --git a/packages/block-library/src/term-description/block.json b/packages/block-library/src/term-description/block.json new file mode 100644 index 00000000000000..b358334963994e --- /dev/null +++ b/packages/block-library/src/term-description/block.json @@ -0,0 +1,20 @@ +{ + "apiVersion": 2, + "name": "core/term-description", + "category": "design", + "attributes": { + "textAlign": { + "type": "string" + } + }, + "supports": { + "align": [ "wide", "full" ], + "html": false, + "fontSize": true, + "lineHeight": true, + "color": { + "link": true + } + }, + "editorStyle": "wp-block-term-description-editor" +} diff --git a/packages/block-library/src/term-description/edit.js b/packages/block-library/src/term-description/edit.js new file mode 100644 index 00000000000000..b4cc0a062c6dfb --- /dev/null +++ b/packages/block-library/src/term-description/edit.js @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + useBlockProps, + BlockControls, + AlignmentToolbar, +} from '@wordpress/block-editor'; + +export default function TermDescriptionEdit( { + attributes, + setAttributes, + mergedStyle, +} ) { + const { textAlign } = attributes; + const blockProps = useBlockProps( { + className: classnames( { + [ `has-text-align-${ textAlign }` ]: textAlign, + } ), + style: mergedStyle, + } ); + return ( + <> + <BlockControls> + <AlignmentToolbar + value={ textAlign } + onChange={ ( nextAlign ) => { + setAttributes( { textAlign: nextAlign } ); + } } + /> + </BlockControls> + <div { ...blockProps }> + <div className="wp-block-term-description__placeholder"> + <span>{ __( 'Term description.' ) }</span> + </div> + </div> + </> + ); +} diff --git a/packages/block-library/src/term-description/editor.scss b/packages/block-library/src/term-description/editor.scss new file mode 100644 index 00000000000000..1eb196c9bfdc52 --- /dev/null +++ b/packages/block-library/src/term-description/editor.scss @@ -0,0 +1,4 @@ +.wp-block-term-description__placeholder { + padding: 1em 0; + border: 1px dashed; +} diff --git a/packages/block-library/src/term-description/index.js b/packages/block-library/src/term-description/index.js new file mode 100644 index 00000000000000..2981fc4677da7d --- /dev/null +++ b/packages/block-library/src/term-description/index.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { _x, __ } from '@wordpress/i18n'; +import { termDescription as icon } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import metadata from './block.json'; +import edit from './edit'; + +const { name } = metadata; +export { metadata, name }; + +export const settings = { + title: _x( 'Term Description', 'block title' ), + description: __( + 'Display the description of categories, tags and custom taxonomies when viewing an archive.' + ), + icon, + edit, +}; diff --git a/packages/block-library/src/term-description/index.php b/packages/block-library/src/term-description/index.php new file mode 100644 index 00000000000000..9ba99cec8905ae --- /dev/null +++ b/packages/block-library/src/term-description/index.php @@ -0,0 +1,40 @@ +<?php +/** + * Server-side rendering of the `core/term-description` block. + * + * @package WordPress + */ + +/** + * Renders the `core/term-description` block on the server. + * + * @param array $attributes Block attributes. + * + * @return string Returns the filtered post content of the current post. + */ +function render_block_core_term_description( $attributes ) { + + if ( ! is_category() && ! is_tag() && ! is_tax() ) { + return ''; + } + + $extra_attributes = ( isset( $attributes['textAlign'] ) ) + ? array( 'class' => 'has-text-align-' . $attributes['textAlign'] ) + : array(); + $wrapper_attributes = get_block_wrapper_attributes( $extra_attributes ); + + return '<div ' . $wrapper_attributes . '>' . term_description() . '</div>'; +} + +/** + * Registers the `core/term-description` block on the server. + */ +function register_block_core_term_description() { + register_block_type_from_metadata( + __DIR__ . '/term-description', + array( + 'render_callback' => 'render_block_core_term_description', + ) + ); +} +add_action( 'init', 'register_block_core_term_description' ); diff --git a/packages/block-library/src/text-columns/edit.js b/packages/block-library/src/text-columns/edit.js index ec69c75e1a4a9e..d9e2a0c6c1fd9f 100644 --- a/packages/block-library/src/text-columns/edit.js +++ b/packages/block-library/src/text-columns/edit.js @@ -21,8 +21,8 @@ export default function TextColumnsEdit( { attributes, setAttributes } ) { const { width, content, columns } = attributes; deprecated( 'The Text Columns block', { + since: '5.3', alternative: 'the Columns block', - plugin: 'Gutenberg', } ); return ( diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index be3cb69adf0bfd..2156e7654aa0a9 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -17,7 +17,10 @@ "supports": { "anchor": true, "__experimentalFontFamily": true, - "fontSize": true + "fontSize": true, + "spacing": { + "padding": true + } }, "style": "wp-block-verse", "editorStyle": "wp-block-verse-editor" diff --git a/packages/block-library/src/verse/edit.js b/packages/block-library/src/verse/edit.js index 08ea73bccb043e..f6fac870d89114 100644 --- a/packages/block-library/src/verse/edit.js +++ b/packages/block-library/src/verse/edit.js @@ -18,6 +18,7 @@ export default function VerseEdit( { attributes, setAttributes, mergeBlocks, + onRemove, } ) { const { textAlign, content } = attributes; const blockProps = useBlockProps( { @@ -48,6 +49,7 @@ export default function VerseEdit( { } } aria-label={ __( 'Verse text' ) } placeholder={ __( 'Write verseโ€ฆ' ) } + onRemove={ onRemove } onMerge={ mergeBlocks } textAlign={ textAlign } { ...blockProps } diff --git a/packages/block-library/src/verse/editor.scss b/packages/block-library/src/verse/editor.scss index f863f5e2556f71..30e577fbae1cad 100644 --- a/packages/block-library/src/verse/editor.scss +++ b/packages/block-library/src/verse/editor.scss @@ -1,4 +1,3 @@ pre.wp-block-verse { color: $gray-900; - padding: 1em; } diff --git a/packages/block-library/src/video/edit-common-settings.js b/packages/block-library/src/video/edit-common-settings.js index 3daeabf4e61be7..ce4601af2e06d2 100644 --- a/packages/block-library/src/video/edit-common-settings.js +++ b/packages/block-library/src/video/edit-common-settings.js @@ -82,6 +82,7 @@ const VideoSettings = ( { setAttributes, attributes } ) => { value={ preload } onChange={ onChangePreload } options={ options } + hideCancelButton={ true } /> </> ); diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js index 63116ee026745d..1db4ce9ad87267 100644 --- a/packages/block-library/src/video/edit.js +++ b/packages/block-library/src/video/edit.js @@ -147,13 +147,15 @@ function VideoEdit( { return ( <> - <BlockControls> + <BlockControls group="block"> <TracksEditor tracks={ tracks } onChange={ ( newTracks ) => { setAttributes( { tracks: newTracks } ); } } /> + </BlockControls> + <BlockControls group="other"> <MediaReplaceFlow mediaId={ id } mediaURL={ src } diff --git a/packages/block-library/src/video/tracks-editor.js b/packages/block-library/src/video/tracks-editor.js index 1b7682aa021394..bb20bbf794cbf6 100644 --- a/packages/block-library/src/video/tracks-editor.js +++ b/packages/block-library/src/video/tracks-editor.js @@ -7,7 +7,6 @@ import { MenuItem, FormFileUpload, MenuGroup, - ToolbarGroup, ToolbarButton, Dropdown, SVG, @@ -145,9 +144,6 @@ function SingleTrackEditor( { track, onChange, onClose, onRemove } ) { value={ kind } label={ __( 'Kind' ) } onChange={ ( newKind ) => { - if ( newKind === DEFAULT_KIND ) { - newKind = undefined; - } onChange( { ...track, kind: newKind, @@ -168,6 +164,10 @@ function SingleTrackEditor( { track, onChange, onClose, onRemove } ) { changes.srcLang = 'en'; hasChanges = true; } + if ( track.kind === undefined ) { + changes.kind = DEFAULT_KIND; + hasChanges = true; + } if ( hasChanges ) { onChange( { ...track, @@ -201,16 +201,14 @@ export default function TracksEditor( { tracks = [], onChange } ) { <Dropdown contentClassName="block-library-video-tracks-editor" renderToggle={ ( { isOpen, onToggle } ) => ( - <ToolbarGroup> - <ToolbarButton - label={ __( 'Text tracks' ) } - showTooltip - aria-expanded={ isOpen } - aria-haspopup="true" - onClick={ onToggle } - icon={ captionIcon } - /> - </ToolbarGroup> + <ToolbarButton + label={ __( 'Text tracks' ) } + showTooltip + aria-expanded={ isOpen } + aria-haspopup="true" + onClick={ onToggle } + icon={ captionIcon } + /> ) } renderContent={ ( {} ) => { if ( trackBeingEdited !== null ) { diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 5f66e3f4622f75..6840b77e78a28d 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -24,7 +24,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 83fe89c2c712e2..f85da354ec8ee4 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -25,7 +25,7 @@ "{src,build,build-module}/{index.js,store/index.js}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/autop": "file:../autop", "@wordpress/blob": "file:../blob", "@wordpress/block-serialization-default-parser": "file:../block-serialization-default-parser", diff --git a/packages/blocks/src/api/index.js b/packages/blocks/src/api/index.js index 9a529bdb97139f..625377c0df3c0f 100644 --- a/packages/blocks/src/api/index.js +++ b/packages/blocks/src/api/index.js @@ -56,7 +56,7 @@ export { // attributes needed to operate with the block later on. export { default as serialize, - getBlockContent, + getBlockInnerHTML as getBlockContent, getBlockDefaultClassName, getBlockMenuDefaultClassName, getSaveElement, diff --git a/packages/blocks/src/api/raw-handling/index.js b/packages/blocks/src/api/raw-handling/index.js index 3d062892e7fa7f..cc2c48fb80584c 100644 --- a/packages/blocks/src/api/raw-handling/index.js +++ b/packages/blocks/src/api/raw-handling/index.js @@ -26,6 +26,7 @@ export { pasteHandler } from './paste-handler'; export function deprecatedGetPhrasingContentSchema( context ) { deprecated( 'wp.blocks.getPhrasingContentSchema', { + since: '5.6', alternative: 'wp.dom.getPhrasingContentSchema', } ); return getPhrasingContentSchema( context ); diff --git a/packages/blocks/src/api/raw-handling/normalise-blocks.js b/packages/blocks/src/api/raw-handling/normalise-blocks.js index f50979a6486b61..f469b7000aa7f9 100644 --- a/packages/blocks/src/api/raw-handling/normalise-blocks.js +++ b/packages/blocks/src/api/raw-handling/normalise-blocks.js @@ -17,7 +17,7 @@ export default function normaliseBlocks( HTML ) { // Text nodes: wrap in a paragraph, or append to previous. if ( node.nodeType === node.TEXT_NODE ) { - if ( ! node.nodeValue.trim() ) { + if ( isEmpty( node ) ) { decu.removeChild( node ); } else { if ( ! accu.lastChild || accu.lastChild.nodeName !== 'P' ) { diff --git a/packages/blocks/src/api/raw-handling/paste-handler.js b/packages/blocks/src/api/raw-handling/paste-handler.js index 665cdc46136e9b..575aaf68642575 100644 --- a/packages/blocks/src/api/raw-handling/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/paste-handler.js @@ -13,7 +13,7 @@ import { getPhrasingContentSchema, removeInvalidHTML } from '@wordpress/dom'; */ import { htmlToBlocks } from './html-to-blocks'; import { hasBlockSupport } from '../registration'; -import { getBlockContent } from '../serializer'; +import { getBlockInnerHTML } from '../serializer'; import { parseWithGrammar } from '../parser'; import normaliseBlocks from './normalise-blocks'; import specialCommentConverter from './special-comment-converter'; @@ -127,7 +127,12 @@ export function pasteHandler( { // * There is a plain text version. // * There is no HTML version, or it has no formatting. if ( plainText && ( ! HTML || isPlain( HTML ) ) ) { - HTML = markdownConverter( plainText ); + HTML = plainText; + + // The markdown converter (Showdown) trims whitespace. + if ( ! /^\s+$/.test( plainText ) ) { + HTML = markdownConverter( HTML ); + } // Switch to inline mode if: // * The current mode is AUTO. @@ -211,22 +216,23 @@ export function pasteHandler( { } ) ); - // If we're allowed to return inline content, and there is only one inlineable block, - // and the original plain text content does not have any line breaks, then - // treat it as inline paste. + // If we're allowed to return inline content, and there is only one + // inlineable block, and the original plain text content does not have any + // line breaks, then treat it as inline paste. if ( mode === 'AUTO' && blocks.length === 1 && hasBlockSupport( blocks[ 0 ].name, '__unstablePasteTextInline', false ) ) { - const trimmedPlainText = plainText.trim(); + // Don't catch line breaks at the start or end. + const trimmedPlainText = plainText.replace( /^[\n]+|[\n]+$/g, '' ); if ( trimmedPlainText !== '' && trimmedPlainText.indexOf( '\n' ) === -1 ) { return removeInvalidHTML( - getBlockContent( blocks[ 0 ] ), + getBlockInnerHTML( blocks[ 0 ] ), phrasingContentSchema ); } diff --git a/packages/blocks/src/api/serializer.js b/packages/blocks/src/api/serializer.js index cf12a1625894de..fe68c9eae11692 100644 --- a/packages/blocks/src/api/serializer.js +++ b/packages/blocks/src/api/serializer.js @@ -267,9 +267,7 @@ export function serializeAttributes( attributes ) { * * @return {string} HTML. */ -export function getBlockContent( block ) { - // @todo why not getBlockInnerHtml? - +export function getBlockInnerHTML( block ) { // If block was parsed as invalid or encounters an error while generating // save content, use original content instead to avoid content loss. If a // block contains nested content, exempt it from this condition because we @@ -336,7 +334,7 @@ export function getCommentDelimitedContent( */ export function serializeBlock( block, { isInnerBlocks = false } = {} ) { const blockName = block.name; - const saveContent = getBlockContent( block ); + const saveContent = getBlockInnerHTML( block ); if ( blockName === getUnregisteredTypeHandlerName() || diff --git a/packages/blocks/src/api/test/parser.js b/packages/blocks/src/api/test/parser.js index add399395072bd..cba52a0f33d6aa 100644 --- a/packages/blocks/src/api/test/parser.js +++ b/packages/blocks/src/api/test/parser.js @@ -777,14 +777,14 @@ describe( 'block parser', () => { <!-- wp:column --> <div class="wp-block-column"> <!-- wp:group --> - <div class="wp-block-group"><div class="wp-block-group__inner-container"> + <div class="wp-block-group"> <!-- wp:list --> <ul><li>B</li><li>C</li></ul> <!-- /wp:list --> <!-- wp:paragraph --> <p>D</p> <!-- /wp:paragraph --> - </div></div> + </div> <!-- /wp:group --> </div> <!-- /wp:column --> @@ -839,14 +839,13 @@ describe( 'block parser', () => { innerContent: [ '<p>D</p>' ], }, ], - innerHTML: - '<div class="wp-block-group"><div class="wp-block-group__inner-container"></div></div>', + innerHTML: '<div class="wp-block-group"></div>', innerContent: [ - '<div class="wp-block-group"><div class="wp-block-group__inner-container">', + '<div class="wp-block-group">', null, '', null, - '</div></div>', + '</div>', ], }, ], diff --git a/packages/blocks/src/api/test/serializer.js b/packages/blocks/src/api/test/serializer.js index c659154db971d9..513eadeb84104c 100644 --- a/packages/blocks/src/api/test/serializer.js +++ b/packages/blocks/src/api/test/serializer.js @@ -12,7 +12,7 @@ import serialize, { serializeAttributes, getCommentDelimitedContent, serializeBlock, - getBlockContent, + getBlockInnerHTML, } from '../serializer'; import { getBlockTypes, @@ -380,7 +380,7 @@ describe( 'block serializer', () => { } ); } ); - describe( 'getBlockContent', () => { + describe( 'getBlockInnerHTML', () => { it( "should return the block's serialized inner HTML", () => { const blockType = { attributes: { @@ -403,7 +403,7 @@ describe( 'block serializer', () => { }, isValid: true, }; - expect( getBlockContent( block ) ).toBe( 'chicken' ); + expect( getBlockInnerHTML( block ) ).toBe( 'chicken' ); } ); } ); } ); diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 3bbe4ea1fa5619..f0cb1632805fe5 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -36,6 +36,7 @@ export const DEFAULT_CATEGORIES = [ { slug: 'media', title: __( 'Media' ) }, { slug: 'design', title: __( 'Design' ) }, { slug: 'widgets', title: __( 'Widgets' ) }, + { slug: 'theme', title: __( 'Theme' ) }, { slug: 'embed', title: __( 'Embeds' ) }, { slug: 'reusable', title: __( 'Reusable blocks' ) }, ]; diff --git a/packages/components/package.json b/packages/components/package.json index 1ed7ef42db7d37..94a149c75f907a 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -26,7 +26,7 @@ "src/**/*.scss" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@emotion/core": "^10.1.1", "@emotion/css": "^10.0.22", "@emotion/native": "^10.0.22", @@ -45,10 +45,10 @@ "@wordpress/primitives": "file:../primitives", "@wordpress/rich-text": "file:../rich-text", "@wordpress/warning": "file:../warning", - "@wp-g2/components": "^0.0.159", - "@wp-g2/context": "^0.0.159", - "@wp-g2/styles": "^0.0.159", - "@wp-g2/utils": "^0.0.159", + "@wp-g2/components": "^0.0.160", + "@wp-g2/context": "^0.0.160", + "@wp-g2/styles": "^0.0.160", + "@wp-g2/utils": "^0.0.160", "classnames": "^2.2.5", "dom-scroll-into-view": "^1.2.1", "downshift": "^6.0.15", diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js index e2548413b74d53..8138d355285fa6 100644 --- a/packages/components/src/autocomplete/index.js +++ b/packages/components/src/autocomplete/index.js @@ -451,9 +451,18 @@ function Autocomplete( { return false; } - return /^\S*$/.test( - text.slice( index + triggerPrefix.length ) + const textWithoutTrigger = text.slice( + index + triggerPrefix.length ); + + if ( + /^\s/.test( textWithoutTrigger ) || + /\s\s+$/.test( textWithoutTrigger ) + ) { + return false; + } + + return /[\u0000-\uFFFF]*$/.test( textWithoutTrigger ); } ); @@ -463,7 +472,9 @@ function Autocomplete( { } const safeTrigger = escapeRegExp( completer.triggerPrefix ); - const match = text.match( new RegExp( `${ safeTrigger }(\\S*)$` ) ); + const match = text.match( + new RegExp( `${ safeTrigger }([\u0000-\uFFFF]*)$` ) + ); const query = match && match[ 1 ]; setAutocompleter( completer ); diff --git a/packages/components/src/button/deprecated.js b/packages/components/src/button/deprecated.js index 6c676d00215811..34e99bd71f54a5 100644 --- a/packages/components/src/button/deprecated.js +++ b/packages/components/src/button/deprecated.js @@ -11,6 +11,7 @@ import Button from '../button'; function IconButton( { labelPosition, size, tooltip, label, ...props }, ref ) { deprecated( 'wp.components.IconButton', { + since: '5.4', alternative: 'wp.components.Button', } ); diff --git a/packages/components/src/button/index.js b/packages/components/src/button/index.js index 3af63e473d21fc..3415fb2a276369 100644 --- a/packages/components/src/button/index.js +++ b/packages/components/src/button/index.js @@ -51,6 +51,7 @@ export function Button( props, ref ) { if ( isDefault ) { deprecated( 'Button isDefault prop', { + since: '5.4', alternative: 'isSecondary', } ); } diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss index f0a50d993e5143..4e8921754bea03 100644 --- a/packages/components/src/button/style.scss +++ b/packages/components/src/button/style.scss @@ -1,6 +1,7 @@ .components-button { display: inline-flex; text-decoration: none; + font-weight: normal; font-size: $default-font-size; margin: 0; border: 0; diff --git a/packages/components/src/clipboard-button/index.js b/packages/components/src/clipboard-button/index.js index 2eec3b0d365e28..f73ab80c11a3ad 100644 --- a/packages/components/src/clipboard-button/index.js +++ b/packages/components/src/clipboard-button/index.js @@ -7,13 +7,16 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useRef, useEffect } from '@wordpress/element'; -import { useCopyOnClick } from '@wordpress/compose'; +import { useCopyToClipboard } from '@wordpress/compose'; +import deprecated from '@wordpress/deprecated'; /** * Internal dependencies */ import Button from '../button'; +const TIMEOUT = 4000; + export default function ClipboardButton( { className, children, @@ -22,23 +25,25 @@ export default function ClipboardButton( { text, ...buttonProps } ) { - const ref = useRef(); - const hasCopied = useCopyOnClick( ref, text ); - const lastHasCopied = useRef( hasCopied ); + deprecated( 'wp.components.ClipboardButton', { + since: '10.3', + plugin: 'Gutenberg', + alternative: 'wp.compose.useCopyToClipboard', + } ); - useEffect( () => { - if ( lastHasCopied.current === hasCopied ) { - return; - } + const timeoutId = useRef(); + const ref = useCopyToClipboard( text, () => { + onCopy(); + clearTimeout( timeoutId.current ); - if ( hasCopied ) { - onCopy(); - } else if ( onFinishCopy ) { - onFinishCopy(); + if ( onFinishCopy ) { + timeoutId.current = setTimeout( () => onFinishCopy(), TIMEOUT ); } + } ); - lastHasCopied.current = hasCopied; - }, [ onCopy, onFinishCopy, hasCopied ] ); + useEffect( () => { + clearTimeout( timeoutId.current ); + }, [] ); const classes = classnames( 'components-clipboard-button', className ); diff --git a/packages/components/src/clipboard-button/stories/index.js b/packages/components/src/clipboard-button/stories/index.js deleted file mode 100644 index 67052bbeab7cf1..00000000000000 --- a/packages/components/src/clipboard-button/stories/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * External dependencies - */ -import { boolean, text } from '@storybook/addon-knobs'; - -/** - * WordPress dependencies - */ -import { useState } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import ClipboardButton from '../'; - -export default { - title: 'Components/ClipboardButton', - component: ClipboardButton, -}; - -const ClipboardButtonWithState = ( { copied, ...props } ) => { - const [ isCopied, setCopied ] = useState( copied ); - - return ( - <ClipboardButton - { ...props } - onCopy={ () => setCopied( true ) } - onFinishCopy={ () => setCopied( false ) } - > - { isCopied ? 'Copied!' : `Copy "${ props.text }"` } - </ClipboardButton> - ); -}; - -export const _default = () => { - const isPrimary = boolean( 'Is primary', true ); - const copyText = text( 'Text', 'Text' ); - - return ( - <ClipboardButtonWithState isPrimary={ isPrimary } text={ copyText } /> - ); -}; diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md index 6fcdea239fa71e..1a796674d87259 100644 --- a/packages/components/src/date-time/README.md +++ b/packages/components/src/date-time/README.md @@ -84,3 +84,4 @@ This function will be called on each day, every time user browses into a differe - Type: `Function` - Required: No + diff --git a/packages/components/src/date-time/date.js b/packages/components/src/date-time/date.js index 6461cc00c9b902..9f6ff3b2691803 100644 --- a/packages/components/src/date-time/date.js +++ b/packages/components/src/date-time/date.js @@ -70,6 +70,9 @@ class DatePicker extends Component { }; onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) ); + + // Keep focus on the date picker. + this.keepFocusInside(); } /** diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.js index 64778f9da3914c..45f4e67c613c6d 100644 --- a/packages/components/src/dropdown-menu/index.js +++ b/packages/components/src/dropdown-menu/index.js @@ -54,15 +54,15 @@ function DropdownMenu( { } ) { if ( menuLabel ) { deprecated( '`menuLabel` prop in `DropdownComponent`', { + since: '5.3', alternative: '`menuProps` object and its `aria-label` property', - plugin: 'Gutenberg', } ); } if ( position ) { deprecated( '`position` prop in `DropdownComponent`', { + since: '5.3', alternative: '`popoverProps` object and its `position` property', - plugin: 'Gutenberg', } ); } diff --git a/packages/components/src/focusable-iframe/index.js b/packages/components/src/focusable-iframe/index.js index a2ee9b98169a9b..f98df583a20166 100644 --- a/packages/components/src/focusable-iframe/index.js +++ b/packages/components/src/focusable-iframe/index.js @@ -4,7 +4,7 @@ import { useEffect, useRef } from '@wordpress/element'; import { useMergeRefs } from '@wordpress/compose'; -export default function FocusableIframe( { iframeRef, onFocus, ...props } ) { +export default function FocusableIframe( { iframeRef, ...props } ) { const fallbackRef = useRef(); const ref = useMergeRefs( [ iframeRef, fallbackRef ] ); @@ -12,7 +12,6 @@ export default function FocusableIframe( { iframeRef, onFocus, ...props } ) { const iframe = fallbackRef.current; const { ownerDocument } = iframe; const { defaultView } = ownerDocument; - const { FocusEvent } = defaultView; /** * Checks whether the iframe is the activeElement, inferring that it has @@ -23,13 +22,7 @@ export default function FocusableIframe( { iframeRef, onFocus, ...props } ) { return; } - const focusEvent = new FocusEvent( 'focus', { bubbles: true } ); - - iframe.dispatchEvent( focusEvent ); - - if ( onFocus ) { - onFocus( focusEvent ); - } + iframe.focus(); } defaultView.addEventListener( 'blur', checkFocus ); @@ -37,7 +30,7 @@ export default function FocusableIframe( { iframeRef, onFocus, ...props } ) { return () => { defaultView.removeEventListener( 'blur', checkFocus ); }; - }, [ onFocus ] ); + }, [] ); // Disable reason: The rendered iframe is a pass-through component, // assigning props inherited from the rendering parent. It's the diff --git a/packages/components/src/guide/index.js b/packages/components/src/guide/index.js index 4bbc7198b668a7..dc0a4f22613e51 100644 --- a/packages/components/src/guide/index.js +++ b/packages/components/src/guide/index.js @@ -32,6 +32,7 @@ export default function Guide( { useEffect( () => { if ( Children.count( children ) ) { deprecated( 'Passing children to <Guide>', { + since: '5.5', alternative: 'the `pages` prop', } ); } @@ -78,11 +79,13 @@ export default function Guide( { <div className="components-guide__page"> { pages[ currentPage ].image } - <PageControl - currentPage={ currentPage } - numberOfPages={ pages.length } - setCurrentPage={ setCurrentPage } - /> + { pages.length > 1 && ( + <PageControl + currentPage={ currentPage } + numberOfPages={ pages.length } + setCurrentPage={ setCurrentPage } + /> + ) } { pages[ currentPage ].content } diff --git a/packages/components/src/guide/page.js b/packages/components/src/guide/page.js index 18014f4548f2ed..2b8cec1af5492c 100644 --- a/packages/components/src/guide/page.js +++ b/packages/components/src/guide/page.js @@ -7,6 +7,7 @@ import deprecated from '@wordpress/deprecated'; export default function GuidePage( props ) { useEffect( () => { deprecated( '<GuidePage>', { + since: '5.5', alternative: 'the `pages` prop in <Guide>', } ); }, [] ); diff --git a/packages/components/src/guide/style.scss b/packages/components/src/guide/style.scss index b2ca62dcb2ac7d..305c339da364b9 100644 --- a/packages/components/src/guide/style.scss +++ b/packages/components/src/guide/style.scss @@ -62,16 +62,18 @@ } &__page-control { - margin: $grid-unit-10 0 $grid-unit-10 0; + margin: 0; text-align: center; li { display: inline-block; + margin: 0; } .components-button { height: 30px; min-width: 20px; + margin: -6px 0; } } diff --git a/packages/components/src/guide/test/index.js b/packages/components/src/guide/test/index.js index f2e1a584c7ca35..0e38f63c20037e 100644 --- a/packages/components/src/guide/test/index.js +++ b/packages/components/src/guide/test/index.js @@ -71,6 +71,13 @@ describe( 'Guide', () => { ).toHaveLength( 1 ); } ); + it( "doesn't display the page control if there is only one page", () => { + const wrapper = shallow( + <Guide pages={ [ { content: <p>Page 1</p> } ] } /> + ); + expect( wrapper.find( PageControl ).exists() ).toBeFalsy(); + } ); + it( 'calls onFinish when the finish button is clicked', () => { const onFinish = jest.fn(); const wrapper = shallow( diff --git a/packages/components/src/higher-order/with-focus-return/index.js b/packages/components/src/higher-order/with-focus-return/index.js index c2a5db9a87a2ed..8f585c9b4d7178 100644 --- a/packages/components/src/higher-order/with-focus-return/index.js +++ b/packages/components/src/higher-order/with-focus-return/index.js @@ -54,6 +54,7 @@ export default createHigherOrderComponent( ( options ) => { export const Provider = ( { children } ) => { deprecated( 'wp.components.FocusReturnProvider component', { + since: '5.7', hint: 'This provider is not used anymore. You can just remove it from your codebase', } ); diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 6f9b4c2915e8f1..afe0c37691e405 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -75,7 +75,7 @@ export { default as ReadableContentView } from './mobile/readable-content-view'; export { default as CycleSelectControl } from './mobile/cycle-select-control'; export { default as Gradient } from './mobile/gradient'; export { default as ColorSettings } from './mobile/color-settings'; -export { default as FocalPointSettings } from './mobile/focal-point-settings'; +export { default as FocalPointSettingsPanel } from './mobile/focal-point-settings-panel'; export { LinkPicker } from './mobile/link-picker'; export { default as LinkPickerScreen } from './mobile/link-picker/link-picker-screen'; export { default as LinkSettings } from './mobile/link-settings'; diff --git a/packages/components/src/isolated-event-container/index.js b/packages/components/src/isolated-event-container/index.js index d3513953dd1314..4c856ff2a2019f 100644 --- a/packages/components/src/isolated-event-container/index.js +++ b/packages/components/src/isolated-event-container/index.js @@ -9,7 +9,9 @@ function stopPropagation( event ) { } export default forwardRef( ( { children, ...props }, ref ) => { - deprecated( 'wp.components.IsolatedEventContainer' ); + deprecated( 'wp.components.IsolatedEventContainer', { + since: '5.7', + } ); // Disable reason: this stops certain events from propagating outside of the component. // - onMouseDown is disabled as this can cause interactions with other DOM elements diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index aa58172dfa84d7..2f5aba85f9ab63 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -344,7 +344,14 @@ class BottomSheet extends Component { listStyle = { flexGrow: 1 }; } else if ( isMaxHeightSet ) { listStyle = { maxHeight }; + + // Allow setting a "static" height of the bottom sheet + // by settting the min height to the max height. + if ( this.props.setMinHeightToMaxHeight ) { + listStyle.minHeight = maxHeight; + } } + const listProps = { disableScrollViewPanResponder: true, bounces, diff --git a/packages/components/src/mobile/bottom-sheet/range-cell.native.js b/packages/components/src/mobile/bottom-sheet/range-cell.native.js index deb31b3da60f1b..ab3827843fa10e 100644 --- a/packages/components/src/mobile/bottom-sheet/range-cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/range-cell.native.js @@ -1,18 +1,13 @@ /** * External dependencies */ -import { - Platform, - AccessibilityInfo, - findNodeHandle, - View, -} from 'react-native'; +import { Platform, AccessibilityInfo, View } from 'react-native'; import Slider from '@react-native-community/slider'; /** * WordPress dependencies */ -import { _x, __, sprintf } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; import { withPreferredColorScheme } from '@wordpress/compose'; @@ -29,50 +24,101 @@ const isIOS = Platform.OS === 'ios'; class BottomSheetRangeCell extends Component { constructor( props ) { super( props ); - this.onChangeValue = this.onChangeValue.bind( this ); - this.onChange = this.onChange.bind( this ); - this.onCellPress = this.onCellPress.bind( this ); + this.onSliderChange = this.onSliderChange.bind( this ); + this.onTextInputChange = this.onTextInputChange.bind( this ); + this.a11yIncrementValue = this.a11yIncrementValue.bind( this ); + this.a11yDecrementValue = this.a11yDecrementValue.bind( this ); + this.a11yUpdateValue = this.a11yUpdateValue.bind( this ); const { value, defaultValue, minimumValue } = props; const initialValue = Number( value || defaultValue || minimumValue ); this.state = { - accessible: true, inputValue: initialValue, sliderValue: initialValue, }; } - onChangeValue( initialValue ) { + componentWillUnmount() { + clearTimeout( this.timeoutAnnounceValue ); + } + + onSliderChange( initialValue ) { const { decimalNum, onChange } = this.props; initialValue = toFixed( initialValue, decimalNum ); this.setState( { inputValue: initialValue } ); - this.announceCurrentValue( `${ initialValue }` ); onChange( initialValue ); } - onCellPress() { - this.setState( { accessible: false } ); - if ( this.sliderRef ) { - const reactTag = findNodeHandle( this.sliderRef ); - AccessibilityInfo.setAccessibilityFocus( reactTag ); + onTextInputChange( nextValue ) { + const { onChange, onComplete } = this.props; + this.setState( { + sliderValue: nextValue, + } ); + onChange( nextValue ); + if ( onComplete ) { + onComplete( nextValue ); + } + } + + /* + * Only used with screenreaders like VoiceOver and TalkBack. Increments the + * value of this setting programmatically. + */ + a11yIncrementValue() { + const { step = 5, maximumValue, decimalNum } = this.props; + const { inputValue } = this.state; + + const newValue = toFixed( inputValue + step, decimalNum ); + + if ( newValue <= maximumValue || maximumValue === undefined ) { + this.a11yUpdateValue( newValue ); } } - announceCurrentValue( value ) { - /* translators: %s: current cell value. */ - const announcement = sprintf( __( 'Current value is %s' ), value ); - AccessibilityInfo.announceForAccessibility( announcement ); + /* + * Only used with screenreaders like VoiceOver and TalkBack. Decrements the + * value of this setting programmatically. + */ + a11yDecrementValue() { + const { step = 5, minimumValue, decimalNum } = this.props; + const { sliderValue } = this.state; + + const newValue = toFixed( sliderValue - step, decimalNum ); + + if ( newValue >= minimumValue ) { + this.a11yUpdateValue( newValue ); + } } - onChange( nextValue ) { + a11yUpdateValue( newValue ) { const { onChange, onComplete } = this.props; this.setState( { - sliderValue: nextValue, + sliderValue: newValue, + inputValue: newValue, } ); - onChange( nextValue ); + onChange( newValue ); if ( onComplete ) { - onComplete( nextValue ); + onComplete( newValue ); + } + this.announceValue( newValue ); + } + + /* + * Only used with screenreaders like VoiceOver and TalkBack. + */ + announceValue( value ) { + const { label, unitLabel = '' } = this.props; + + if ( isIOS ) { + // On Android it triggers the accessibilityLabel with the value change, but + // on iOS we need to do this manually. + clearTimeout( this.timeoutAnnounceValue ); + this.timeoutAnnounceValue = setTimeout( () => { + AccessibilityInfo.announceForAccessibility( + `${ value } ${ unitLabel }, ${ label }` + ); + }, 300 ); } } @@ -94,22 +140,30 @@ class BottomSheetRangeCell extends Component { cellContainerStyle, onComplete, shouldDisplayTextInput = true, + unitLabel = '', + settingLabel = 'Value', + openUnitPicker, children, decimalNum, ...cellProps } = this.props; - const { accessible, inputValue, sliderValue } = this.state; + const { inputValue, sliderValue } = this.state; - const accessibilityLabel = sprintf( - /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: Current value. */ - _x( - '%1$s. Current value is %2$s', - 'Slider for picking a number inside a range' - ), - cellProps.label, - value - ); + const getAccessibilityHint = () => { + return openUnitPicker ? __( 'double-tap to change unit' ) : ''; + }; + + const getAccessibilityLabel = () => { + return sprintf( + /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: setting label (example: width), %3$s: Current value. %4$s: value measurement unit (example: pixels) */ + __( '%1$s. %2$s is %3$s %4$s.' ), + cellProps.label, + settingLabel, + value, + unitLabel + ); + }; const containerStyle = [ styles.container, @@ -117,64 +171,85 @@ class BottomSheetRangeCell extends Component { ]; return ( - <Cell - { ...cellProps } - cellContainerStyle={ [ - styles.cellContainerStyles, - cellContainerStyle, + <View + accessible={ true } + accessibilityRole="adjustable" + accessibilityActions={ [ + { name: 'increment' }, + { name: 'decrement' }, + { name: 'activate' }, ] } - cellRowContainerStyle={ containerStyle } - accessibilityRole={ 'none' } - leftAlign - editable={ false } - activeOpacity={ 1 } - accessible={ accessible } - onPress={ this.onCellPress } - valueStyle={ styles.valueStyle } - accessibilityLabel={ accessibilityLabel } - accessibilityHint={ - /* translators: accessibility text (hint for focusing a slider) */ - __( 'Double tap to change the value using slider' ) - } + onAccessibilityAction={ ( event ) => { + switch ( event.nativeEvent.actionName ) { + case 'increment': + this.a11yIncrementValue(); + break; + case 'decrement': + this.a11yDecrementValue(); + break; + case 'activate': + openUnitPicker(); + break; + } + } } + accessibilityLabel={ getAccessibilityLabel() } + accessibilityHint={ getAccessibilityHint() } > - <View style={ containerStyle }> - { preview } - <Slider - value={ sliderValue } - defaultValue={ defaultValue } - disabled={ disabled } - step={ step } - minimumValue={ minimumValue } - maximumValue={ maximumValue } - minimumTrackTintColor={ minimumTrackTintColor } - maximumTrackTintColor={ maximumTrackTintColor } - thumbTintColor={ thumbTintColor } - onValueChange={ this.onChangeValue } - onSlidingComplete={ onComplete } - ref={ ( slider ) => { - this.sliderRef = slider; - } } - style={ - isIOS ? styles.sliderIOS : styles.sliderAndroid - } - accessibilityRole={ 'adjustable' } - /> - { shouldDisplayTextInput && ( - <RangeTextInput - label={ cellProps.label } - onChange={ this.onChange } - defaultValue={ `${ inputValue }` } - value={ inputValue } - min={ minimumValue } - max={ maximumValue } - step={ step } - decimalNum={ decimalNum } - > - { children } - </RangeTextInput> - ) } + <View importantForAccessibility="no-hide-descendants"> + <Cell + { ...cellProps } + cellContainerStyle={ [ + styles.cellContainerStyles, + cellContainerStyle, + ] } + cellRowContainerStyle={ containerStyle } + leftAlign + editable={ false } + activeOpacity={ 1 } + accessible={ false } + valueStyle={ styles.valueStyle } + > + <View style={ containerStyle }> + { preview } + <Slider + value={ sliderValue } + defaultValue={ defaultValue } + disabled={ disabled } + step={ step } + minimumValue={ minimumValue } + maximumValue={ maximumValue } + minimumTrackTintColor={ minimumTrackTintColor } + maximumTrackTintColor={ maximumTrackTintColor } + thumbTintColor={ thumbTintColor } + onValueChange={ this.onSliderChange } + onSlidingComplete={ onComplete } + ref={ ( slider ) => { + this.sliderRef = slider; + } } + style={ + isIOS + ? styles.sliderIOS + : styles.sliderAndroid + } + /> + { shouldDisplayTextInput && ( + <RangeTextInput + label={ cellProps.label } + onChange={ this.onTextInputChange } + defaultValue={ `${ inputValue }` } + value={ inputValue } + min={ minimumValue } + max={ maximumValue } + step={ step } + decimalNum={ decimalNum } + > + { children } + </RangeTextInput> + ) } + </View> + </Cell> </View> - </Cell> + </View> ); } } diff --git a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js index ff8db3be5d5fbc..7e6d88d491dc34 100644 --- a/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/stepper-cell/index.native.js @@ -114,14 +114,14 @@ class BottomSheetStepperCell extends Component { } announceValue( value ) { - const { label } = this.props; + const { label, unitLabel = '' } = this.props; - if ( Platform.OS === 'ios' ) { + if ( isIOS ) { // On Android it triggers the accessibilityLabel with the value change clearTimeout( this.timeoutAnnounceValue ); this.timeoutAnnounceValue = setTimeout( () => { AccessibilityInfo.announceForAccessibility( - `${ value } ${ label }` + `${ value } ${ unitLabel } ${ label }` ); }, 300 ); } @@ -130,6 +130,8 @@ class BottomSheetStepperCell extends Component { render() { const { label, + settingLabel = 'Value', + unitLabel = '', icon, min, max, @@ -139,6 +141,7 @@ class BottomSheetStepperCell extends Component { shouldDisplayTextInput = false, preview, onChange, + openUnitPicker, decimalNum, cellContainerStyle, } = this.props; @@ -149,12 +152,20 @@ class BottomSheetStepperCell extends Component { styles.cellLabel, ! icon ? styles.cellLabelNoIcon : {}, ]; + + const getAccessibilityHint = () => { + return openUnitPicker ? __( 'double-tap to change unit' ) : ''; + }; + const accessibilityLabel = sprintf( - /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: Current value. */ - __( '%1$s. Current value is %2$s' ), + /* translators: accessibility text. Inform about current value. %1$s: Control label %2$s: setting label (example: width), %3$s: Current value. %4$s: value measurement unit (example: pixels) */ + __( '%1$s. %2$s is %3$s %4$s.' ), label, - value + settingLabel, + value, + unitLabel ); + const containerStyle = [ styles.rowContainer, isIOS ? styles.containerIOS : styles.containerAndroid, @@ -165,9 +176,11 @@ class BottomSheetStepperCell extends Component { accessible={ true } accessibilityRole="adjustable" accessibilityLabel={ accessibilityLabel } + accessibilityHint={ getAccessibilityHint() } accessibilityActions={ [ { name: 'increment' }, { name: 'decrement' }, + { name: 'activate' }, ] } onAccessibilityAction={ ( event ) => { switch ( event.nativeEvent.actionName ) { @@ -177,55 +190,67 @@ class BottomSheetStepperCell extends Component { case 'decrement': this.onDecrementValue(); break; + case 'activate': + if ( openUnitPicker ) { + openUnitPicker(); + } + break; } } } > - <Cell - accessibilityRole="none" - accessible={ false } - cellContainerStyle={ [ - styles.cellContainerStyle, - preview && styles.columnContainer, - cellContainerStyle, - ] } - cellRowContainerStyle={ - preview ? containerStyle : styles.cellRowStyles - } - disabled={ true } - editable={ false } - icon={ icon } - label={ label } - labelStyle={ labelStyle } - leftAlign={ true } - separatorType={ separatorType } - > - <View style={ preview && containerStyle }> - { preview } - <Stepper - isMaxValue={ isMaxValue } - isMinValue={ isMinValue } - onPressInDecrement={ this.onDecrementValuePressIn } - onPressInIncrement={ this.onIncrementValuePressIn } - onPressOut={ this.onPressOut } - value={ value } - shouldDisplayTextInput={ shouldDisplayTextInput } - > - { shouldDisplayTextInput && ( - <RangeTextInput - label={ label } - onChange={ onChange } - defaultValue={ `${ inputValue }` } - value={ inputValue } - min={ min } - step={ 1 } - decimalNum={ decimalNum } - > - { children } - </RangeTextInput> - ) } - </Stepper> - </View> - </Cell> + <View importantForAccessibility="no-hide-descendants"> + <Cell + accessible={ false } + cellContainerStyle={ [ + styles.cellContainerStyle, + preview && styles.columnContainer, + cellContainerStyle, + ] } + cellRowContainerStyle={ + preview ? containerStyle : styles.cellRowStyles + } + disabled={ true } + editable={ false } + icon={ icon } + label={ label } + labelStyle={ labelStyle } + leftAlign={ true } + separatorType={ separatorType } + > + <View style={ preview && containerStyle }> + { preview } + <Stepper + isMaxValue={ isMaxValue } + isMinValue={ isMinValue } + onPressInDecrement={ + this.onDecrementValuePressIn + } + onPressInIncrement={ + this.onIncrementValuePressIn + } + onPressOut={ this.onPressOut } + value={ value } + shouldDisplayTextInput={ + shouldDisplayTextInput + } + > + { shouldDisplayTextInput && ( + <RangeTextInput + label={ label } + onChange={ onChange } + defaultValue={ `${ inputValue }` } + value={ inputValue } + min={ min } + step={ 1 } + decimalNum={ decimalNum } + > + { children } + </RangeTextInput> + ) } + </Stepper> + </View> + </Cell> + </View> </View> ); } diff --git a/packages/components/src/mobile/focal-point-settings/index.native.js b/packages/components/src/mobile/focal-point-settings-panel/index.native.js similarity index 91% rename from packages/components/src/mobile/focal-point-settings/index.native.js rename to packages/components/src/mobile/focal-point-settings-panel/index.native.js index c959942ef9caa0..6b294526f1b0db 100644 --- a/packages/components/src/mobile/focal-point-settings/index.native.js +++ b/packages/components/src/mobile/focal-point-settings-panel/index.native.js @@ -17,7 +17,7 @@ import { BottomSheetContext, FocalPointPicker } from '@wordpress/components'; import NavigationHeader from '../bottom-sheet/navigation-header'; import styles from './styles.scss'; -const FocalPointSettingsMemo = memo( +const FocalPointSettingsPanelMemo = memo( ( { focalPoint, onFocalPointChange, @@ -62,12 +62,12 @@ const FocalPointSettingsMemo = memo( } ); -function FocalPointSettings( props ) { +function FocalPointSettingsPanel( props ) { const route = useRoute(); const { shouldEnableBottomSheetScroll } = useContext( BottomSheetContext ); return ( - <FocalPointSettingsMemo + <FocalPointSettingsPanelMemo shouldEnableBottomSheetScroll={ shouldEnableBottomSheetScroll } { ...props } { ...route.params } @@ -75,4 +75,4 @@ function FocalPointSettings( props ) { ); } -export default FocalPointSettings; +export default FocalPointSettingsPanel; diff --git a/packages/components/src/mobile/focal-point-settings/styles.native.scss b/packages/components/src/mobile/focal-point-settings-panel/styles.native.scss similarity index 100% rename from packages/components/src/mobile/focal-point-settings/styles.native.scss rename to packages/components/src/mobile/focal-point-settings-panel/styles.native.scss diff --git a/packages/components/src/modal/header.js b/packages/components/src/modal/header.js index 4f437656c262ac..7fcc145b05484e 100644 --- a/packages/components/src/modal/header.js +++ b/packages/components/src/modal/header.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { close } from '@wordpress/icons'; +import { closeSmall } from '@wordpress/icons'; /** * Internal dependencies @@ -40,7 +40,11 @@ const ModalHeader = ( { ) } </div> { isDismissible && ( - <Button onClick={ onClose } icon={ close } label={ label } /> + <Button + onClick={ onClose } + icon={ closeSmall } + label={ label } + /> ) } </div> ); diff --git a/packages/components/src/modal/index.js b/packages/components/src/modal/index.js index 925f28be42525d..2144972d42f187 100644 --- a/packages/components/src/modal/index.js +++ b/packages/components/src/modal/index.js @@ -122,6 +122,7 @@ class Modal extends Component { if ( isDismissable ) { deprecated( 'isDismissable prop of the Modal component', { + since: '5.4', alternative: 'isDismissible prop (renamed) of the Modal component', } ); diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss index 450cb553cf48c5..e25eeec49cdb57 100644 --- a/packages/components/src/modal/style.scss +++ b/packages/components/src/modal/style.scss @@ -23,9 +23,9 @@ left: 0; box-sizing: border-box; margin: 0; - border: $border-width solid $gray-300; background: $white; box-shadow: $shadow-modal; + border-radius: $radius-block-ui; overflow: auto; // Show a centered modal on bigger screens. @@ -61,7 +61,7 @@ .components-modal__header { box-sizing: border-box; border-bottom: $border-width solid $gray-300; - padding: 0 $grid-unit-30; + padding: 0 $grid-unit-40; display: flex; flex-direction: row; justify-content: space-between; @@ -75,7 +75,7 @@ position: relative; position: sticky; top: 0; - margin: 0 -#{$grid-unit-30} $grid-unit-30; + margin: 0 -#{$grid-unit-40} $grid-unit-30; // Rules inside this query are only run by Microsoft Edge. // Edge has bugs around position: sticky;, so it needs a separate top rule. @@ -123,7 +123,7 @@ .components-modal__content { box-sizing: border-box; height: 100%; - padding: 0 $grid-unit-30 $grid-unit-30; + padding: 0 $grid-unit-40 $grid-unit-30; // Rules inside this query are only run by Microsoft Edge. // This is a companion top padding to the fixed rule in line 77. diff --git a/packages/components/src/navigation/styles/navigation-styles.js b/packages/components/src/navigation/styles/navigation-styles.js index 8d5773ef4ea9af..48331fe94a789c 100644 --- a/packages/components/src/navigation/styles/navigation-styles.js +++ b/packages/components/src/navigation/styles/navigation-styles.js @@ -14,7 +14,7 @@ import { isRTL } from '@wordpress/i18n'; import { G2, UI } from '../../utils/colors-values'; import Button from '../../button'; import Text from '../../text'; -import { reduceMotion, space } from '../../utils'; +import { reduceMotion, space, rtl } from '../../utils'; export const NavigationUI = styled.div` width: 100%; @@ -163,6 +163,7 @@ export const ItemBaseUI = styled.li` width: 100%; color: ${ G2.lightGray.ui }; padding: ${ space( 1 ) } ${ space( 2 ) }; /* 8px 16px */ + ${ rtl( { textAlign: 'left' }, { textAlign: 'right' } ) } &:hover, &:focus:not( [aria-disabled='true'] ):active, diff --git a/packages/components/src/panel/style.scss b/packages/components/src/panel/style.scss index 62ec3f1c9c7598..17a2404b0c98ee 100644 --- a/packages/components/src/panel/style.scss +++ b/packages/components/src/panel/style.scss @@ -76,7 +76,7 @@ .components-panel__body-toggle.components-button { position: relative; - padding: $grid-unit-20; + padding: $grid-unit-20 $grid-unit-60 $grid-unit-20 $grid-unit-20; outline: none; width: 100%; font-weight: 500; diff --git a/packages/components/src/popover/index.js b/packages/components/src/popover/index.js index 06e077f9e5c40d..0cb8a975344fad 100644 --- a/packages/components/src/popover/index.js +++ b/packages/components/src/popover/index.js @@ -544,6 +544,7 @@ const Popover = ( { } ); deprecated( 'Popover onClickOutside prop', { + since: '5.3', alternative: 'onFocusOutside', } ); diff --git a/packages/components/src/query-controls/index.native.js b/packages/components/src/query-controls/index.native.js index e9456cd82000e3..f7a7c24c12af9a 100644 --- a/packages/components/src/query-controls/index.native.js +++ b/packages/components/src/query-controls/index.native.js @@ -68,6 +68,7 @@ const QueryControls = memo( value={ `${ orderBy }/${ order }` } options={ options } onChange={ onChange } + hideCancelButton={ true } /> ), onCategoryChange && ( @@ -77,6 +78,7 @@ const QueryControls = memo( noOptionLabel={ __( 'All' ) } selectedCategoryId={ selectedCategoryId } onChange={ onCategoryChange } + hideCancelButton={ true } /> ), onNumberOfItemsChange && ( diff --git a/packages/components/src/slot-fill/index.js b/packages/components/src/slot-fill/index.js index 3da527bf577d89..8c61fa2a444c3b 100644 --- a/packages/components/src/slot-fill/index.js +++ b/packages/components/src/slot-fill/index.js @@ -44,6 +44,7 @@ export function createSlotFill( name ) { const SlotComponent = ( props ) => <Slot name={ name } { ...props } />; SlotComponent.displayName = name + 'Slot'; + SlotComponent.__unstableName = name; return { Fill: FillComponent, diff --git a/packages/components/src/text-control/index.native.js b/packages/components/src/text-control/index.native.js index 2cb5d3e965c756..28160ed7f590bd 100644 --- a/packages/components/src/text-control/index.native.js +++ b/packages/components/src/text-control/index.native.js @@ -16,6 +16,7 @@ function TextControl( { instanceId, onChange, type = 'text', + placeholder, ...props } ) { const id = `inspector-text-control-${ instanceId }`; @@ -31,6 +32,7 @@ function TextControl( { value={ value } onChangeValue={ onChange } aria-describedby={ !! help ? id + '__help' : undefined } + valuePlaceholder={ placeholder } { ...props } /> ); diff --git a/packages/components/src/toolbar-group/style.scss b/packages/components/src/toolbar-group/style.scss index 98d0832e547362..dc1152e6794e04 100644 --- a/packages/components/src/toolbar-group/style.scss +++ b/packages/components/src/toolbar-group/style.scss @@ -67,60 +67,73 @@ div.components-toolbar { // Size multiple sequential buttons to be optically balanced. // Icons are 36px, as set by a 24px icon and 12px padding. -.components-accessible-toolbar .components-toolbar-group > .components-button.components-button.has-icon, -.components-toolbar div > .components-button.components-button.has-icon { - min-width: $block-toolbar-height - $grid-unit-15; - padding-left: $grid-unit-15 / 2; // 6px. - padding-right: $grid-unit-15 / 2; - - svg { - min-width: $button-size-small; // This is the optimal icon size, and we size the whole button after this. +.block-editor-block-toolbar > .components-toolbar > .block-editor-block-toolbar__slot, // When a plugin adds to a slot, the segment has a `components-toolbar` class. +.block-editor-block-toolbar > .components-toolbar-group > .block-editor-block-toolbar__slot, // When no plugin adds to slots, the segment has a `components-toolbar-group` class. +.block-editor-block-toolbar > .block-editor-block-toolbar__slot > .components-toolbar, // The nesting order is sometimes reversed. +.block-editor-block-toolbar > .block-editor-block-toolbar__slot > .components-dropdown, // Targets unique markup for the "Replace" button. +.block-editor-block-toolbar .block-editor-block-toolbar__slot .components-toolbar-group { // Inline formatting tools use this class. + + // Segments with just a single button. + > .components-button:first-child:last-child, + > .components-dropdown:first-child:last-child .components-button, + &.components-dropdown > .components-button.components-button, // Single segments where the dropdown is also the toolbar group (such as text align). + &.components-dropdown > * .components-button { + min-width: $block-toolbar-height; + padding-left: $grid-unit-15; + padding-right: $grid-unit-15; + + &::before { + left: $grid-unit-10; + right: $grid-unit-10; + } } - &::before { - left: 2px; - right: 2px; + // First. + // @todo, this unnamed div only shows up when plugins add to slots. We should remove the fragment. + > .components-button:first-child, + > div:first-child > .components-button, + > .components-dropdown:first-child .components-button { + min-width: $block-toolbar-height - $grid-unit-15 / 2; + padding-left: $grid-unit-15 - $border-width; + padding-right: $grid-unit-15 / 2; + + &::before { + left: $grid-unit-10; + right: 2px; + } } -} -// First button in a group. -.components-accessible-toolbar .components-toolbar-group > .components-button:first-child.has-icon, -.components-accessible-toolbar .components-toolbar-group > div:first-child > .components-button.has-icon, -.components-toolbar div:first-child .components-button.has-icon { - min-width: $block-toolbar-height - $grid-unit-15 / 2; - padding-left: $grid-unit-15 - $border-width; - padding-right: $grid-unit-15 / 2; - - &::before { - left: $grid-unit-10; - right: 2px; - } -} + // Middle. + // @todo, this unnamed div only shows up when plugins add to slots. We should remove the fragment. + > .components-button, + > div > .components-button, + > .components-dropdown .components-button { + min-width: $block-toolbar-height - $grid-unit-15; + padding-left: $grid-unit-15 / 2; // 6px. + padding-right: $grid-unit-15 / 2; + + svg { + min-width: $button-size-small; // This is the optimal icon size, and we size the whole button after this. + } -// Last button in a group. -.components-accessible-toolbar .components-toolbar-group > .components-button:last-of-type.has-icon, -.components-accessible-toolbar .components-toolbar-group > div:last-child > .components-button.has-icon, -.components-toolbar div:last-child .components-button.has-icon { - min-width: $block-toolbar-height - $grid-unit-15 / 2; - padding-left: $grid-unit-15 / 2; - padding-right: $grid-unit-15 - $border-width; - - &::before { - left: 2px; - right: $grid-unit-10; + &::before { + left: 2px; + right: 2px; + } } -} -// Single buttons should remain 48px. -.components-accessible-toolbar .components-toolbar-group > .components-button:first-of-type:last-of-type.has-icon, -.components-accessible-toolbar .components-toolbar-group > div:first-child:last-child > .components-button.has-icon, -.components-toolbar div:first-child:last-child > .components-button.has-icon { - min-width: $block-toolbar-height; - padding-left: $grid-unit-15; - padding-right: $grid-unit-15; - - &::before { - left: $grid-unit-10; - right: $grid-unit-10; + // Last. + // @todo, this unnamed div only shows up when plugins add to slots. We should remove the fragment. + > .components-button:last-child, + > div:last-child > .components-button, + > .components-dropdown:last-child .components-button { + min-width: $block-toolbar-height - $grid-unit-15 / 2; + padding-left: $grid-unit-15 / 2; + padding-right: $grid-unit-15 - $border-width; + + &::before { + left: 2px; + right: $grid-unit-10; + } } } diff --git a/packages/components/src/toolbar/index.js b/packages/components/src/toolbar/index.js index b94886bd9ce749..3bb887cc1fd335 100644 --- a/packages/components/src/toolbar/index.js +++ b/packages/components/src/toolbar/index.js @@ -28,6 +28,7 @@ import ToolbarContainer from './toolbar-container'; function Toolbar( { className, label, ...props }, ref ) { if ( ! label ) { deprecated( 'Using Toolbar without label prop', { + since: '5.6', alternative: 'ToolbarGroup component', link: 'https://developer.wordpress.org/block-editor/components/toolbar/', diff --git a/packages/components/src/ui/button-group/test/__snapshots__/index.js.snap b/packages/components/src/ui/button-group/test/__snapshots__/index.js.snap index ea2343cd92c680..4d776a309dfaa1 100644 --- a/packages/components/src/ui/button-group/test/__snapshots__/index.js.snap +++ b/packages/components/src/ui/button-group/test/__snapshots__/index.js.snap @@ -409,6 +409,7 @@ exports[`props should render correctly 1`] = ` id="ButtonGroup-1" role="radio" tabindex="0" + type="button" > <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d emotion-0" @@ -437,6 +438,7 @@ exports[`props should render correctly 1`] = ` id="ButtonGroup-2" role="radio" tabindex="-1" + type="button" > <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d emotion-0" @@ -465,6 +467,7 @@ exports[`props should render correctly 1`] = ` id="ButtonGroup-3" role="radio" tabindex="-1" + type="button" > <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d emotion-0" diff --git a/packages/components/src/ui/button/component.js b/packages/components/src/ui/button/component.js index 4a6f17ae2c2126..70cf5a6190b869 100644 --- a/packages/components/src/ui/button/component.js +++ b/packages/components/src/ui/button/component.js @@ -47,6 +47,7 @@ function Button( props, forwardedRef ) { isSubtle = false, onClick = noop, size = 'medium', + type = 'button', variant = 'secondary', describedBy, ...otherProps @@ -114,6 +115,7 @@ function Button( props, forwardedRef ) { onClick={ handleOnClick } ref={ forwardedRef } aria-describedby={ describedById } + type={ type } { ...otherProps } > { children } diff --git a/packages/components/src/ui/button/test/__snapshots__/index.js.snap b/packages/components/src/ui/button/test/__snapshots__/index.js.snap index c4668adb1f4954..ef04b87133f3ec 100644 --- a/packages/components/src/ui/button/test/__snapshots__/index.js.snap +++ b/packages/components/src/ui/button/test/__snapshots__/index.js.snap @@ -34,12 +34,12 @@ Snapshot Diff: data-g2-component="Button" data-icon="false" - href="#" + type="button" > <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d css-1493c17" data-g2-c16t="true" - data-g2-component="ButtonContent" -@@ -20,6 +19,6 @@ +@@ -21,6 +20,6 @@ aria-hidden="true" class="components-elevation wp-components-elevation ic-i1zzlu ic-192yz5d css-8abvx7" data-g2-c16t="true" @@ -340,6 +340,7 @@ exports[`props should render correctly 1`] = ` data-g2-c16t="true" data-g2-component="Button" data-icon="false" + type="button" > <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d emotion-0" @@ -371,7 +372,7 @@ Snapshot Diff: data-destructive="false" data-focused="false" data-g2-c16t="true" -@@ -12,11 +11,11 @@ +@@ -13,11 +12,11 @@ <span class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d css-1493c17" data-g2-c16t="true" @@ -412,7 +413,7 @@ Snapshot Diff: - First value + Second value -@@ -14,29 +14,10 @@ +@@ -15,29 +15,10 @@ data-g2-component="ButtonContent" > Lorem @@ -449,20 +450,21 @@ Snapshot Diff: - First value + Second value -@@ -4,19 +4,12 @@ +@@ -4,20 +4,13 @@ data-active="false" data-destructive="false" data-focused="false" data-g2-c16t="true" data-g2-component="Button" - data-icon="true" -- > ++ data-icon="false" + type="button" + > - <span - class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d css-1dor7w6" - data-g2-c16t="true" - data-g2-component="ButtonIcon" -+ data-icon="false" - > +- > - <svg /> - </span> <span @@ -582,10 +584,10 @@ Snapshot Diff: - First value + Second value -@@ -7,19 +7,10 @@ - data-g2-c16t="true" +@@ -8,19 +8,10 @@ data-g2-component="Button" data-icon="false" + type="button" > <span - class="components-flex-item wp-components-flex-item ic-1p3tlc8 ic-192yz5d css-1dor7w6" @@ -635,7 +637,7 @@ Snapshot Diff: - First value + Second value -@@ -14,19 +14,10 @@ +@@ -15,19 +15,10 @@ data-g2-component="ButtonContent" > Lorem diff --git a/packages/components/src/ui/control-group/test/__snapshots__/index.js.snap b/packages/components/src/ui/control-group/test/__snapshots__/index.js.snap index 5424bcfd068637..8b76670df18854 100644 --- a/packages/components/src/ui/control-group/test/__snapshots__/index.js.snap +++ b/packages/components/src/ui/control-group/test/__snapshots__/index.js.snap @@ -997,8 +997,8 @@ exports[`props should render mixed control types 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); display: block; diff --git a/packages/components/src/ui/control-label/test/__snapshots__/index.js.snap b/packages/components/src/ui/control-label/test/__snapshots__/index.js.snap index b29cd6d788b001..360b5d7f92c901 100644 --- a/packages/components/src/ui/control-label/test/__snapshots__/index.js.snap +++ b/packages/components/src/ui/control-label/test/__snapshots__/index.js.snap @@ -33,8 +33,8 @@ exports[`props should render correctly 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); display: inline-block; @@ -105,8 +105,8 @@ exports[`props should render no truncate 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); display: inline-block; @@ -181,8 +181,8 @@ exports[`props should render size 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); display: inline-block; diff --git a/packages/components/src/ui/form-group/test/__snapshots__/index.js.snap b/packages/components/src/ui/form-group/test/__snapshots__/index.js.snap index 8005b5772213a1..288e655271f9b6 100644 --- a/packages/components/src/ui/form-group/test/__snapshots__/index.js.snap +++ b/packages/components/src/ui/form-group/test/__snapshots__/index.js.snap @@ -55,8 +55,8 @@ exports[`props should render alignLabel 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); text-align: right; @@ -165,8 +165,8 @@ exports[`props should render vertically 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); text-align: left; diff --git a/packages/components/src/ui/heading/README.md b/packages/components/src/ui/heading/README.md new file mode 100644 index 00000000000000..80f098ed93e71a --- /dev/null +++ b/packages/components/src/ui/heading/README.md @@ -0,0 +1,24 @@ +# Heading + +`Heading` renders headings and titles using the library's typography system. + +## Usage + +```jsx +import { Heading } from '@wordpress/components/ui'; + +function Example() { + return <Heading>Code is Poetry</Heading>; +} +``` + +## Props + + +`Heading` uses `Text` underneath, so we have access to all of `Text`'s props except for `size` which is replaced by `level`. For a complete list of those props, check out [`Text`](../text/#props). + +##### level + +**Type**: `1 | 2 | 3 | 4 | 5 | 6` + +Passing any of the heading levels to `level` will both render the correct typographic text size as well as the semantic element corresponding to the level (`h1` for `1` for example). diff --git a/packages/components/src/ui/heading/component.ts b/packages/components/src/ui/heading/component.ts new file mode 100644 index 00000000000000..c6705325cdcaea --- /dev/null +++ b/packages/components/src/ui/heading/component.ts @@ -0,0 +1,25 @@ +/** + * Internal dependencies + */ +import { createComponent } from '../utils'; +import { useHeading } from './hook'; + +/** + * `Heading` renders headings and titles using the library's typography system. + * + * @example + * ```jsx + * import { Heading } from `@wordpress/components/ui` + * + * function Example() { + * return <Heading>Code is Poetry</Heading>; + * } + * ``` + */ +const Heading = createComponent( { + as: 'h1', + useHook: useHeading, + name: 'Heading', +} ); + +export default Heading; diff --git a/packages/components/src/ui/heading/hook.ts b/packages/components/src/ui/heading/hook.ts new file mode 100644 index 00000000000000..79d8977c4da1ba --- /dev/null +++ b/packages/components/src/ui/heading/hook.ts @@ -0,0 +1,83 @@ +/** + * External dependencies + */ +import { useContextSystem } from '@wp-g2/context'; +import { getHeadingFontSize, ui } from '@wp-g2/styles'; +import type { ViewOwnProps } from '@wp-g2/create-styles'; + +/** + * Internal dependencies + */ +import type { Props as TextProps } from '../text/types'; +import { useText } from '../text'; + +export type HeadingSize = + | 1 + | 2 + | 3 + | 4 + | 5 + | 6 + | '1' + | '2' + | '3' + | '4' + | '5' + | '6'; + +export interface HeadingProps extends Omit< TextProps, 'size' > { + /** + * `Heading` will typically render the sizes `1`, `2`, `3`, `4`, `5`, or `6`, which map to `h1`-`h6`. + * + * @default 3 + * + * @example + * ```jsx + * import { Heading, View } from `@wp-g2/components` + * + * function Example() { + * return ( + * <View> + * <Heading level="1">Code is Poetry</Heading> + * <Heading level="2">Code is Poetry</Heading> + * <Heading level="3">Code is Poetry</Heading> + * <Heading level="4">Code is Poetry</Heading> + * <Heading level="5">Code is Poetry</Heading> + * <Heading level="6">Code is Poetry</Heading> + * </View> + * ); + * } + * ``` + */ + level: HeadingSize; +} + +export function useHeading( props: ViewOwnProps< HeadingProps, 'h1' > ) { + const { as: asProp, level = 2, ...otherProps } = useContextSystem( + props, + 'Heading' + ); + + const as = asProp || `h${ level }`; + + const a11yProps: { + role?: string; + 'aria-level'?: string | number; + } = {}; + if ( typeof as === 'string' && as[ 0 ] !== 'h' ) { + // if not a semantic `h` element, add a11y props: + a11yProps.role = 'heading'; + a11yProps[ 'aria-level' ] = level; + } + + const textProps = useText( { + color: ui.get( 'colorTextHeading' ), + size: getHeadingFontSize( level ), + isBlock: true, + // @ts-ignore We're passing a variable so `string` is safe + weight: ui.get( 'fontWeightHeading' ), + ...otherProps, + } ); + + return { ...textProps, ...a11yProps, as }; +} diff --git a/packages/components/src/ui/heading/index.ts b/packages/components/src/ui/heading/index.ts new file mode 100644 index 00000000000000..aec7615839ad93 --- /dev/null +++ b/packages/components/src/ui/heading/index.ts @@ -0,0 +1,3 @@ +export { default as Heading } from './component'; +export * from './component'; +export * from './hook'; diff --git a/packages/components/src/ui/heading/stories/index.js b/packages/components/src/ui/heading/stories/index.js new file mode 100644 index 00000000000000..9254152b628170 --- /dev/null +++ b/packages/components/src/ui/heading/stories/index.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import { Heading } from '../index'; + +export default { + component: Heading, + title: 'G2 Components (Experimental)/Heading', +}; + +export const _default = () => { + return ( + <> + <Heading level={ 1 }>Heading</Heading> + <Heading level={ 2 }>Heading</Heading> + <Heading level={ 3 }>Heading</Heading> + <Heading level={ 4 }>Heading</Heading> + <Heading level={ 5 }>Heading</Heading> + <Heading level={ 6 }>Heading</Heading> + </> + ); +}; diff --git a/packages/components/src/ui/heading/test/__snapshots__/index.js.snap b/packages/components/src/ui/heading/test/__snapshots__/index.js.snap new file mode 100644 index 00000000000000..a0505ff6fe8881 --- /dev/null +++ b/packages/components/src/ui/heading/test/__snapshots__/index.js.snap @@ -0,0 +1,87 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`props should render correctly 1`] = ` +.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 { + box-sizing: border-box; + -moz-osx-font-smoothing: grayscale; + -webkit-font-smoothing: antialiased; + font-family: Inter,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif; + font-family: var(--wp-g2-font-family); + font-size: 13px; + font-size: var(--wp-g2-font-size); + font-weight: normal; + font-weight: var(--wp-g2-font-weight); + margin: 0; + color: #1e1e1e; + color: var(--wp-g2-color-text); + line-height: 1.2; + line-height: var(--wp-g2-font-line-height-base); + color: #050505; + color: var(--wp-g2-color-text-heading); + font-size: calc(1.95 * 13px); + font-size: var(--wp-g2-font-size-h-2); + font-weight: 600; + font-weight: var(--wp-g2-font-weight-heading); + display: block; +} + +@media (prefers-reduced-motion) { + .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 { + -webkit-transition: none !important; + transition: none !important; + } +} + +[data-system-ui-reduced-motion-mode="true"] .emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0.emotion-0 { + -webkit-transition: none !important; + transition: none !important; +} + +<h2 + class="components-truncate wp-components-truncate components-text wp-components-text components-heading wp-components-heading ic-1ng8wck ic-7u3orf ic-1m45iq3 ic-192yz5d emotion-0" + data-g2-c16t="true" + data-g2-component="Heading" +> + Code is Poetry +</h2> +`; + +exports[`props should render level as a number 1`] = ` +Snapshot Diff: +- Received styles ++ Base styles + +@@ -4,11 +4,11 @@ + "-webkit-font-smoothing": "antialiased", + "box-sizing": "border-box", + "color": "var(--wp-g2-color-text-heading)", + "display": "block", + "font-family": "var(--wp-g2-font-family)", +- "font-size": "var(--wp-g2-font-size-h-4)", ++ "font-size": "var(--wp-g2-font-size-h-2)", + "font-weight": "var(--wp-g2-font-weight-heading)", + "line-height": "var(--wp-g2-font-line-height-base)", + "margin": "0", + }, + ] +`; + +exports[`props should render level as a string 1`] = ` +Snapshot Diff: +- Received styles ++ Base styles + +@@ -4,11 +4,11 @@ + "-webkit-font-smoothing": "antialiased", + "box-sizing": "border-box", + "color": "var(--wp-g2-color-text-heading)", + "display": "block", + "font-family": "var(--wp-g2-font-family)", +- "font-size": "var(--wp-g2-font-size-h-4)", ++ "font-size": "var(--wp-g2-font-size-h-2)", + "font-weight": "var(--wp-g2-font-weight-heading)", + "line-height": "var(--wp-g2-font-line-height-base)", + "margin": "0", + }, + ] +`; diff --git a/packages/components/src/ui/heading/test/index.js b/packages/components/src/ui/heading/test/index.js new file mode 100644 index 00000000000000..454a50a393c0e0 --- /dev/null +++ b/packages/components/src/ui/heading/test/index.js @@ -0,0 +1,67 @@ +/** + * External dependencies + */ +import { render } from '@testing-library/react'; + +/** + * Internal dependencies + */ +import { Heading } from '../index'; + +describe( 'props', () => { + let base; + beforeEach( () => { + base = render( <Heading>Code is Poetry</Heading> ); + } ); + + test( 'should render correctly', () => { + expect( base.container.firstChild ).toMatchSnapshot(); + } ); + + test( 'should render level as a number', () => { + const { container } = render( + <Heading level={ 4 }>Code is Poetry</Heading> + ); + expect( container.firstChild ).toMatchStyleDiffSnapshot( + base.container.firstChild + ); + } ); + + test( 'should render level as a string', () => { + const { container } = render( + <Heading level="4">Code is Poetry</Heading> + ); + expect( container.firstChild ).toMatchStyleDiffSnapshot( + base.container.firstChild + ); + } ); + + test( 'should allow as prop', () => { + const { container } = render( + <Heading level="1" as="span"> + Code is Poetry + </Heading> + ); + expect( container.firstChild.tagName ).toBe( 'SPAN' ); + } ); + + test( 'should render a11y props when not using a semantic element', () => { + const { container } = render( + <Heading level="3" as="div"> + Code is Poetry + </Heading> + ); + expect( container.firstChild.getAttribute( 'role' ) ).toBe( 'heading' ); + expect( container.firstChild.getAttribute( 'aria-level' ) ).toBe( '3' ); + } ); + + test( 'should not render a11y props when using a semantic element', () => { + const { container } = render( + <Heading level="1" as="h4"> + Code is Poetry + </Heading> + ); + expect( container.firstChild.getAttribute( 'role' ) ).toBeNull(); + expect( container.firstChild.getAttribute( 'aria-level' ) ).toBeNull(); + } ); +} ); diff --git a/packages/components/src/ui/index.js b/packages/components/src/ui/index.js index 716f72a624dd4b..1276b0dfd896cf 100644 --- a/packages/components/src/ui/index.js +++ b/packages/components/src/ui/index.js @@ -7,6 +7,7 @@ export * from './flex'; export * from './form-group'; export * from './grid'; export * from './h-stack'; +export * from './heading'; export * from './shortcut'; export * from './spinner'; export * from './text'; diff --git a/packages/components/src/ui/text/test/__snapshots__/text.js.snap b/packages/components/src/ui/text/test/__snapshots__/text.js.snap index 77f8835bbbbb6b..199b820be1f32c 100644 --- a/packages/components/src/ui/text/test/__snapshots__/text.js.snap +++ b/packages/components/src/ui/text/test/__snapshots__/text.js.snap @@ -16,8 +16,8 @@ exports[`Text should render highlighted words with highlightCaseSensitive 1`] = color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); } @@ -71,8 +71,8 @@ exports[`Text snapshot tests should render correctly 1`] = ` color: var(--wp-g2-color-text); line-height: 1.2; line-height: var(--wp-g2-font-line-height-base); - font-size: calc(1 * 13px); - font-size: calc(1 * var(--wp-g2-font-size)); + font-size: calc((13 / 13) * 13px); + font-size: calc((13 / 13) * var(--wp-g2-font-size)); font-weight: normal; font-weight: var(--wp-g2-font-weight); } diff --git a/packages/components/src/unit-control/index.native.js b/packages/components/src/unit-control/index.native.js index ada1bc77af7731..7bd020e26e5a69 100644 --- a/packages/components/src/unit-control/index.native.js +++ b/packages/components/src/unit-control/index.native.js @@ -16,7 +16,7 @@ import RangeCell from '../mobile/bottom-sheet/range-cell'; import StepperCell from '../mobile/bottom-sheet/stepper-cell'; import Picker from '../mobile/picker'; import styles from './style.scss'; -import { CSS_UNITS, hasUnits } from './utils'; +import { CSS_UNITS, hasUnits, parseA11yLabelForUnit } from './utils'; /** * WordPress dependencies @@ -136,6 +136,8 @@ function UnitControl( { defaultValue={ initialControlValue } shouldDisplayTextInput decimalNum={ unit === 'px' ? 0 : decimalNum } + openUnitPicker={ onPickerPresent } + unitLabel={ parseA11yLabelForUnit( unit ) } { ...props } > { renderUnitPicker() } @@ -147,9 +149,12 @@ function UnitControl( { minimumValue={ min } maximumValue={ max } value={ value } + unit={ unit } defaultValue={ initialControlValue } separatorType={ separatorType } decimalNum={ decimalNum } + openUnitPicker={ onPickerPresent } + unitLabel={ parseA11yLabelForUnit( unit ) } { ...props } > { renderUnitPicker() } diff --git a/packages/components/src/unit-control/utils.js b/packages/components/src/unit-control/utils.js index 8abd21dea85bf5..74ddc01669d759 100644 --- a/packages/components/src/unit-control/utils.js +++ b/packages/components/src/unit-control/utils.js @@ -3,13 +3,41 @@ */ import { isEmpty } from 'lodash'; +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; + +/** + * Units of measurements. `a11yLabel` is used by screenreaders. + */ export const CSS_UNITS = [ - { value: 'px', label: 'px', default: 0 }, - { value: '%', label: '%', default: 10 }, - { value: 'em', label: 'em', default: 0 }, - { value: 'rem', label: 'rem', default: 0 }, - { value: 'vw', label: 'vw', default: 10 }, - { value: 'vh', label: 'vh', default: 10 }, + { value: 'px', label: 'px', default: 0, a11yLabel: __( 'pixels' ) }, + { value: '%', label: '%', default: 10, a11yLabel: __( 'percent' ) }, + { + value: 'em', + label: 'em', + default: 0, + a11yLabel: _x( 'ems', 'Relative to parent font size (em)' ), + }, + { + value: 'rem', + label: 'rem', + default: 0, + a11yLabel: _x( 'rems', 'Relative to root font size (rem)' ), + }, + { + value: 'vw', + label: 'vw', + default: 10, + a11yLabel: __( 'viewport widths' ), + }, + { + value: 'vh', + label: 'vh', + default: 10, + a11yLabel: __( 'viewport heights' ), + }, ]; export const DEFAULT_UNIT = CSS_UNITS[ 0 ]; @@ -101,3 +129,15 @@ export function getValidParsedUnit( next, units, fallbackValue, fallbackUnit ) { return [ baseValue, baseUnit ]; } + +/** + * Takes a unit value and finds the matching accessibility label for the + * unit abbreviation. + * + * @param {string} unit Unit value (example: px) + * @return {string} a11y label for the unit abbreviation + */ +export function parseA11yLabelForUnit( unit ) { + const match = CSS_UNITS.find( ( item ) => item.value === unit ); + return match?.a11yLabel ? match?.a11yLabel : match?.value; +} diff --git a/packages/compose/README.md b/packages/compose/README.md index b5b6fb6b2bc042..a5d36e2bae5ee8 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -159,6 +159,8 @@ _Returns_ <a name="useCopyOnClick" href="#useCopyOnClick">#</a> **useCopyOnClick** +> **Deprecated** + Copies the text to the clipboard when the element is clicked. _Parameters_ @@ -171,6 +173,19 @@ _Returns_ - `boolean`: Whether or not the text has been copied. Resets after the timeout. +<a name="useCopyToClipboard" href="#useCopyToClipboard">#</a> **useCopyToClipboard** + +Copies the given text to the clipboard when the element is clicked. + +_Parameters_ + +- _text_ `text|Function`: The text to copy. Use a function if not already available and expensive to compute. +- _onSuccess_ `Function`: Called when to text is copied. + +_Returns_ + +- `RefObject`: A ref to assign to the target element. + <a name="useDebounce" href="#useDebounce">#</a> **useDebounce** Debounces a function with Lodash's `debounce`. A new debounced function will diff --git a/packages/compose/package.json b/packages/compose/package.json index 6e5889490f85b5..9165672206d97b 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -25,7 +25,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", diff --git a/packages/compose/src/higher-order/with-global-events/index.js b/packages/compose/src/higher-order/with-global-events/index.js index 9c77c3ab2c4e5e..6e0a2ad57f7c6b 100644 --- a/packages/compose/src/higher-order/with-global-events/index.js +++ b/packages/compose/src/higher-order/with-global-events/index.js @@ -43,6 +43,7 @@ const listener = new Listener(); */ export default function withGlobalEvents( eventTypesToHandlers ) { deprecated( 'wp.compose.withGlobalEvents', { + since: '5.7', alternative: 'useEffect', } ); diff --git a/packages/compose/src/hooks/use-copy-on-click/index.js b/packages/compose/src/hooks/use-copy-on-click/index.js index fc1caa2d97d0b9..33a14c0224add3 100644 --- a/packages/compose/src/hooks/use-copy-on-click/index.js +++ b/packages/compose/src/hooks/use-copy-on-click/index.js @@ -7,10 +7,13 @@ import Clipboard from 'clipboard'; * WordPress dependencies */ import { useRef, useEffect, useState } from '@wordpress/element'; +import deprecated from '@wordpress/deprecated'; /** * Copies the text to the clipboard when the element is clicked. * + * @deprecated + * * @param {Object} ref Reference with the element. * @param {string|Function} text The text to copy. * @param {number} timeout Optional timeout to reset the returned @@ -20,6 +23,12 @@ import { useRef, useEffect, useState } from '@wordpress/element'; * timeout. */ export default function useCopyOnClick( ref, text, timeout = 4000 ) { + deprecated( 'wp.compose.useCopyOnClick', { + since: '10.3', + plugin: 'Gutenberg', + alternative: 'wp.compose.useCopyToClipboard', + } ); + const clipboard = useRef(); const [ hasCopied, setHasCopied ] = useState( false ); diff --git a/packages/compose/src/hooks/use-copy-to-clipboard/index.js b/packages/compose/src/hooks/use-copy-to-clipboard/index.js new file mode 100644 index 00000000000000..b9cbf38fa3e996 --- /dev/null +++ b/packages/compose/src/hooks/use-copy-to-clipboard/index.js @@ -0,0 +1,66 @@ +/** + * External dependencies + */ +import Clipboard from 'clipboard'; + +/** + * WordPress dependencies + */ +import { useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import useRefEffect from '../use-ref-effect'; + +/** @typedef {import('@wordpress/element').RefObject} RefObject */ + +function useUpdatedRef( value ) { + const ref = useRef( value ); + ref.current = value; + return ref; +} + +/** + * Copies the given text to the clipboard when the element is clicked. + * + * @param {text|Function} text The text to copy. Use a function if not + * already available and expensive to compute. + * @param {Function} onSuccess Called when to text is copied. + * + * @return {RefObject} A ref to assign to the target element. + */ +export default function useCopyToClipboard( text, onSuccess ) { + // Store the dependencies as refs and continuesly update them so they're + // fresh when the callback is called. + const textRef = useUpdatedRef( text ); + const onSuccesRef = useUpdatedRef( onSuccess ); + return useRefEffect( ( node ) => { + // Clipboard listens to click events. + const clipboard = new Clipboard( node, { + text() { + return typeof textRef.current === 'function' + ? textRef.current() + : textRef.current; + }, + } ); + + clipboard.on( 'success', ( { clearSelection } ) => { + // Clearing selection will move focus back to the triggering + // button, ensuring that it is not reset to the body, and + // further that it is kept within the rendered node. + clearSelection(); + // Handle ClipboardJS focus bug, see + // https://github.com/zenorocha/clipboard.js/issues/680 + node.focus(); + + if ( onSuccesRef.current ) { + onSuccesRef.current(); + } + } ); + + return () => { + clipboard.destroy(); + }; + }, [] ); +} diff --git a/packages/compose/src/hooks/use-merge-refs/index.js b/packages/compose/src/hooks/use-merge-refs/index.js index 0a6d012b2b5a84..08e6c4cc797401 100644 --- a/packages/compose/src/hooks/use-merge-refs/index.js +++ b/packages/compose/src/hooks/use-merge-refs/index.js @@ -47,9 +47,14 @@ export default function useMergeRefs( refs ) { } ); previousRefs.current = refs; - didElementChange.current = false; }, refs ); + // No dependencies, must be reset after every render so ref callbacks are + // correctly called after a ref change. + useLayoutEffect( () => { + didElementChange.current = false; + } ); + // There should be no dependencies so that `callback` is only called when // the node changes. return useCallback( ( value ) => { diff --git a/packages/compose/src/hooks/use-merge-refs/test/index.js b/packages/compose/src/hooks/use-merge-refs/test/index.js index 0c156abe0c0a5b..d9b6fda25d3360 100644 --- a/packages/compose/src/hooks/use-merge-refs/test/index.js +++ b/packages/compose/src/hooks/use-merge-refs/test/index.js @@ -218,4 +218,59 @@ describe( 'useMergeRefs', () => { [ [], [ newElement, null ] ], ] ); } ); + + it( 'should work for dependency change after node change', () => { + const rootElement = document.getElementById( 'root' ); + + ReactDOM.render( <MergedRefs />, rootElement ); + + const originalElement = rootElement.firstElementChild; + + ReactDOM.render( <MergedRefs tagName="button" />, rootElement ); + + const newElement = rootElement.firstElementChild; + + // After a render with the original element and a second render with the + // new element, expect the initial callback functions to be called with + // the original element, then null, then the new element. + // Again, the new callback functions should not be called! There has + // been no dependency change. + expect( renderCallback.history ).toEqual( [ + [ + [ originalElement, null, newElement ], + [ originalElement, null, newElement ], + ], + [ [], [] ], + ] ); + + ReactDOM.render( + <MergedRefs tagName="button" count={ 1 } />, + rootElement + ); + + // After a third render with a dependency change, expect the inital + // callback function to be called with null and the new callback + // function to be called with the new element. Note that for callback + // one no dependencies have changed. + expect( renderCallback.history ).toEqual( [ + [ + [ originalElement, null, newElement ], + [ originalElement, null, newElement, null ], + ], + [ [], [] ], + [ [], [ newElement ] ], + ] ); + + ReactDOM.render( null, rootElement ); + + // Unmount: current callback functions should be called with null. + expect( renderCallback.history ).toEqual( [ + [ + [ originalElement, null, newElement, null ], + [ originalElement, null, newElement, null ], + ], + [ [], [] ], + [ [], [ newElement, null ] ], + ] ); + } ); } ); diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 5d1c1d0e5b0327..5a42d7f19d8c16 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -15,6 +15,7 @@ export { default as withState } from './higher-order/with-state'; // Hooks export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing'; export { default as useCopyOnClick } from './hooks/use-copy-on-click'; +export { default as useCopyToClipboard } from './hooks/use-copy-to-clipboard'; export { default as __experimentalUseDialog } from './hooks/use-dialog'; export { default as __experimentalUseDragging } from './hooks/use-dragging'; export { default as useFocusOnMount } from './hooks/use-focus-on-mount'; diff --git a/packages/core-data/package.json b/packages/core-data/package.json index ab1d11f279d748..919eb3f2f5cac9 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -26,7 +26,7 @@ "{src,build,build-module}/index.js" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blocks": "file:../blocks", "@wordpress/data": "file:../data", diff --git a/packages/core-data/src/resolvers.js b/packages/core-data/src/resolvers.js index f227c05fb21437..1d210f8a7b4b08 100644 --- a/packages/core-data/src/resolvers.js +++ b/packages/core-data/src/resolvers.js @@ -294,6 +294,7 @@ export function* getEmbedPreview( url ) { */ export function* hasUploadPermissions() { deprecated( "select( 'core' ).hasUploadPermissions()", { + since: '5.2', alternative: "select( 'core' ).canUser( 'create', 'media' )", } ); yield* canUser( 'create', 'media' ); diff --git a/packages/core-data/src/selectors.js b/packages/core-data/src/selectors.js index 5f4a9501f9bd54..8414a5b51c8c88 100644 --- a/packages/core-data/src/selectors.js +++ b/packages/core-data/src/selectors.js @@ -634,6 +634,7 @@ export function isPreviewEmbedFallback( state, url ) { */ export function hasUploadPermissions( state ) { deprecated( "select( 'core' ).hasUploadPermissions()", { + since: '5.2', alternative: "select( 'core' ).canUser( 'create', 'media' )", } ); return defaultTo( canUser( state, 'create', 'media' ), true ); diff --git a/packages/create-block-tutorial-template/CHANGELOG.md b/packages/create-block-tutorial-template/CHANGELOG.md index 30d693efee8c44..004a0c6cacbda9 100644 --- a/packages/create-block-tutorial-template/CHANGELOG.md +++ b/packages/create-block-tutorial-template/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancement + +- Scaffolded plugin requires WordPress 5.7 now ([#29757](https://github.com/WordPress/gutenberg/pull/29757). + ## 1.1.0 (2021-03-17) ### Enhancement diff --git a/packages/create-block-tutorial-template/templates/$slug.php.mustache b/packages/create-block-tutorial-template/templates/$slug.php.mustache index f32a5807d63456..fc419db2d85180 100644 --- a/packages/create-block-tutorial-template/templates/$slug.php.mustache +++ b/packages/create-block-tutorial-template/templates/$slug.php.mustache @@ -1,22 +1,24 @@ <?php /** - * Plugin Name: {{title}} + * Plugin Name: {{title}} {{#description}} - * Description: {{description}} + * Description: {{description}} {{/description}} - * Version: {{version}} + * Requires at least: 5.7 + * Requires PHP: 7.0 + * Version: {{version}} {{#author}} - * Author: {{author}} + * Author: {{author}} {{/author}} {{#license}} - * License: {{license}} + * License: {{license}} {{/license}} {{#licenseURI}} - * License URI: {{{licenseURI}}} + * License URI: {{{licenseURI}}} {{/licenseURI}} - * Text Domain: {{textdomain}} + * Text Domain: {{textdomain}} * - * @package {{namespace}} + * @package {{namespace}} */ /** diff --git a/packages/create-block-tutorial-template/templates/readme.txt.mustache b/packages/create-block-tutorial-template/templates/readme.txt.mustache index 087d9392316fa9..534dc5432cad4d 100644 --- a/packages/create-block-tutorial-template/templates/readme.txt.mustache +++ b/packages/create-block-tutorial-template/templates/readme.txt.mustache @@ -3,10 +3,8 @@ Contributors: {{author}} {{/author}} Tags: block -Requires at least: 5.5.3 Tested up to: 5.7.0 Stable tag: {{version}} -Requires PHP: 7.0.0 {{#license}} License: {{license}} {{/license}} diff --git a/packages/create-block-tutorial-template/templates/src/index.js.mustache b/packages/create-block-tutorial-template/templates/src/index.js.mustache index 8fa6ae10231969..ad697a563786b5 100644 --- a/packages/create-block-tutorial-template/templates/src/index.js.mustache +++ b/packages/create-block-tutorial-template/templates/src/index.js.mustache @@ -28,11 +28,6 @@ import save from './save'; * @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block */ registerBlockType( '{{namespace}}/{{slug}}', { - /** - * @see https://make.wordpress.org/core/2020/11/18/block-api-version-2/ - */ - apiVersion: {{apiVersion}}, - /** * Used to construct a preview for the block to be shown in the block inserter. */ diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index 3168a98472f561..0f1f8eb155ee95 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased +### Enhancement + +- Scaffolded plugin requires WordPress 5.7 now ([#29757](https://github.com/WordPress/gutenberg/pull/29757)). + +### New Features + +- Add new `theme` category to select for the block type ([#30089](https://github.com/WordPress/gutenberg/pull/30089)). + ## 2.1.0 (2021-03-17) ### New Features diff --git a/packages/create-block/README.md b/packages/create-block/README.md index ddd0241a652465..4f302a967256cf 100644 --- a/packages/create-block/README.md +++ b/packages/create-block/README.md @@ -177,7 +177,7 @@ The following configurable variables are used with the template files. Template - `title` (no default) - a display title for your block. - `description` (no default) - a short description for your block. - `dashicon` (no default) - an icon property thats makes it easier to identify a block, see https://developer.wordpress.org/resource/dashicons/. -- `category` (default: `'widgets'`) - blocks are grouped into categories to help users browse and discover them. The categories provided by core are `text`, `media`, `design`, `widgets`, and `embed`. +- `category` (default: `'widgets'`) - blocks are grouped into categories to help users browse and discover them. The categories provided by core are `text`, `media`, `design`, `widgets`, `theme`, and `embed`. - `attributes` (no default) - see https://developer.wordpress.org/block-editor/developers/block-api/block-attributes/. - `supports` (no default) - optional block extended support features, see https://developer.wordpress.org/block-editor/developers/block-api/block-supports/. - `author` (default: `'The WordPress Contributors'`) diff --git a/packages/create-block/lib/prompts.js b/packages/create-block/lib/prompts.js index 0396737e47cc47..973560fd4dc825 100644 --- a/packages/create-block/lib/prompts.js +++ b/packages/create-block/lib/prompts.js @@ -70,7 +70,7 @@ const category = { type: 'list', name: 'category', message: 'The category name to help users browse and discover your block:', - choices: [ 'text', 'media', 'design', 'widgets', 'embed' ], + choices: [ 'text', 'media', 'design', 'widgets', 'theme', 'embed' ], }; const author = { diff --git a/packages/create-block/lib/templates/es5/$slug.php.mustache b/packages/create-block/lib/templates/es5/$slug.php.mustache index 6d61a469c37d72..8bc76340ce1745 100644 --- a/packages/create-block/lib/templates/es5/$slug.php.mustache +++ b/packages/create-block/lib/templates/es5/$slug.php.mustache @@ -1,22 +1,24 @@ <?php /** - * Plugin Name: {{title}} + * Plugin Name: {{title}} {{#description}} - * Description: {{description}} + * Description: {{description}} {{/description}} - * Version: {{version}} + * Requires at least: 5.7 + * Requires PHP: 7.0 + * Version: {{version}} {{#author}} - * Author: {{author}} + * Author: {{author}} {{/author}} {{#license}} - * License: {{license}} + * License: {{license}} {{/license}} {{#licenseURI}} - * License URI: {{{licenseURI}}} + * License URI: {{{licenseURI}}} {{/licenseURI}} - * Text Domain: {{textdomain}} + * Text Domain: {{textdomain}} * - * @package {{namespace}} + * @package {{namespace}} */ /** @@ -33,6 +35,7 @@ function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() { '{{namespace}}-{{slug}}-block-editor', plugins_url( $index_js, __FILE__ ), array( + 'wp-block-editor', 'wp-blocks', 'wp-i18n', 'wp-element', diff --git a/packages/create-block/lib/templates/es5/readme.txt.mustache b/packages/create-block/lib/templates/es5/readme.txt.mustache index b94fbac1a420f2..534dc5432cad4d 100644 --- a/packages/create-block/lib/templates/es5/readme.txt.mustache +++ b/packages/create-block/lib/templates/es5/readme.txt.mustache @@ -3,10 +3,8 @@ Contributors: {{author}} {{/author}} Tags: block -Requires at least: 5.6.0 Tested up to: 5.7.0 Stable tag: {{version}} -Requires PHP: 7.0.0 {{#license}} License: {{license}} {{/license}} diff --git a/packages/create-block/lib/templates/esnext/$slug.php.mustache b/packages/create-block/lib/templates/esnext/$slug.php.mustache index f32a5807d63456..fc419db2d85180 100644 --- a/packages/create-block/lib/templates/esnext/$slug.php.mustache +++ b/packages/create-block/lib/templates/esnext/$slug.php.mustache @@ -1,22 +1,24 @@ <?php /** - * Plugin Name: {{title}} + * Plugin Name: {{title}} {{#description}} - * Description: {{description}} + * Description: {{description}} {{/description}} - * Version: {{version}} + * Requires at least: 5.7 + * Requires PHP: 7.0 + * Version: {{version}} {{#author}} - * Author: {{author}} + * Author: {{author}} {{/author}} {{#license}} - * License: {{license}} + * License: {{license}} {{/license}} {{#licenseURI}} - * License URI: {{{licenseURI}}} + * License URI: {{{licenseURI}}} {{/licenseURI}} - * Text Domain: {{textdomain}} + * Text Domain: {{textdomain}} * - * @package {{namespace}} + * @package {{namespace}} */ /** diff --git a/packages/create-block/lib/templates/esnext/readme.txt.mustache b/packages/create-block/lib/templates/esnext/readme.txt.mustache index b94fbac1a420f2..534dc5432cad4d 100644 --- a/packages/create-block/lib/templates/esnext/readme.txt.mustache +++ b/packages/create-block/lib/templates/esnext/readme.txt.mustache @@ -3,10 +3,8 @@ Contributors: {{author}} {{/author}} Tags: block -Requires at least: 5.6.0 Tested up to: 5.7.0 Stable tag: {{version}} -Requires PHP: 7.0.0 {{#license}} License: {{license}} {{/license}} diff --git a/packages/create-block/lib/templates/esnext/src/index.js.mustache b/packages/create-block/lib/templates/esnext/src/index.js.mustache index 6eb0e76b75e9f9..6b258d5b390c5f 100644 --- a/packages/create-block/lib/templates/esnext/src/index.js.mustache +++ b/packages/create-block/lib/templates/esnext/src/index.js.mustache @@ -26,11 +26,6 @@ import save from './save'; * @see https://developer.wordpress.org/block-editor/developers/block-api/#registering-a-block */ registerBlockType( '{{namespace}}/{{slug}}', { - /** - * @see https://make.wordpress.org/core/2020/11/18/block-api-version-2/ - */ - apiVersion: {{apiVersion}}, - /** * @see ./edit.js */ diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 19ecad22223d50..bfad7781f4f9d4 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -20,11 +20,17 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.11.2", + "@wordpress/a11y": "file:../a11y", "@wordpress/block-editor": "file:../block-editor", "@wordpress/block-library": "file:../block-library", "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "classnames": "^2.2.6", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/packages/customize-widgets/src/components/inspector/block-inspector-button.js b/packages/customize-widgets/src/components/inspector/block-inspector-button.js new file mode 100644 index 00000000000000..bbf45103063f62 --- /dev/null +++ b/packages/customize-widgets/src/components/inspector/block-inspector-button.js @@ -0,0 +1,15 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { MenuItem } from '@wordpress/components'; + +function BlockInspectorButton( { isInspectorOpened = false, ...props } ) { + const label = isInspectorOpened + ? __( 'Hide more settings' ) + : __( 'Show more settings' ); + + return <MenuItem { ...props }>{ label }</MenuItem>; +} + +export default BlockInspectorButton; diff --git a/packages/customize-widgets/src/components/inspector/index.js b/packages/customize-widgets/src/components/inspector/index.js new file mode 100644 index 00000000000000..6acce947f23171 --- /dev/null +++ b/packages/customize-widgets/src/components/inspector/index.js @@ -0,0 +1,123 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { useEffect, useRef } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { + BlockInspector, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { __ } from '@wordpress/i18n'; +import { useInstanceId } from '@wordpress/compose'; + +export { default as BlockInspectorButton } from './block-inspector-button'; + +function InspectorSectionMeta( { closeInspector, inspectorTitleId } ) { + return ( + <div className="customize-section-description-container section-meta"> + <div className="customize-section-title"> + { /* Disable reason: We want to use the exiting style of the back button. */ } + <button // eslint-disable-line react/forbid-elements + type="button" + className="customize-section-back" + tabIndex="0" + onClick={ closeInspector } + > + <span className="screen-reader-text">Back</span> + </button> + <h3 id={ inspectorTitleId }> + <span className="customize-action"> + { __( 'Customizing โ–ธ Widgets' ) } + </span> + { __( 'Block Settings' ) } + </h3> + </div> + </div> + ); +} + +export default function Inspector( { + isOpened, + isAnimating, + setInspectorOpenState, +} ) { + const selectedBlockClientId = useSelect( ( select ) => + select( blockEditorStore ).getSelectedBlockClientId() + ); + const selectedBlockRef = useRef(); + + useEffect( () => { + selectedBlockRef.current = document.querySelector( + `[data-block="${ selectedBlockClientId }"]` + ); + }, [ selectedBlockClientId ] ); + + const inspectorTitleId = useInstanceId( + Inspector, + 'customize-widgets-block-settings' + ); + + useEffect( () => { + const openedSidebarSection = document.querySelector( + '.control-section-sidebar.open' + ); + + if ( isOpened ) { + openedSidebarSection.classList.add( 'is-inspector-open' ); + } else { + openedSidebarSection.classList.remove( 'is-inspector-open' ); + } + + // In case the "transitionend" event for some reasons doesn't fire. + // (Like when it's set to "display: none", or when the transition property is removed.) + // See https://github.com/w3c/csswg-drafts/issues/3043 + const timer = setTimeout( () => { + setInspectorOpenState( 'TRANSITION_END' ); + }, 180 ); + + return () => { + openedSidebarSection.classList.remove( 'is-inspector-open' ); + clearTimeout( timer ); + }; + }, [ isOpened, setInspectorOpenState ] ); + + return ( + <div + role="region" + aria-labelledby={ inspectorTitleId } + className={ classnames( + 'customize-pane-child', + 'accordion-section-content', + 'accordion-section', + 'customize-widgets-layout__inspector', + { + open: isOpened, + // Needed to keep the inspector visible while closing. + busy: isAnimating, + } + ) } + onTransitionEnd={ () => { + setInspectorOpenState( 'TRANSITION_END' ); + } } + > + <InspectorSectionMeta + closeInspector={ () => { + setInspectorOpenState( 'CLOSE' ); + + // Wait a tick so that the block editor can transition back from being hidden. + window.requestAnimationFrame( () => { + selectedBlockRef.current?.focus(); + } ); + } } + inspectorTitleId={ inspectorTitleId } + /> + + <BlockInspector /> + </div> + ); +} diff --git a/packages/customize-widgets/src/components/inspector/style.scss b/packages/customize-widgets/src/components/inspector/style.scss new file mode 100644 index 00000000000000..42b284a9b466a7 --- /dev/null +++ b/packages/customize-widgets/src/components/inspector/style.scss @@ -0,0 +1,25 @@ +// To override the style in block-editor/block-inspector. +.block-editor-block-inspector h3 { + margin-bottom: 0; +} + +#customize-theme-controls .customize-pane-child.accordion-section-content.customize-widgets-layout__inspector { + background: $white; + box-sizing: border-box; + + * { + box-sizing: inherit; + } + + .block-editor-block-inspector { + margin: -$grid-unit-15; + } +} + +#customize-theme-controls .customize-pane-child.accordion-section-content.control-section-sidebar.open { + transition: 0.18s transform cubic-bezier(0.645, 0.045, 0.355, 1); /* easeInOutCubic */ + + &.is-inspector-open { + transform: translateX(-100%); + } +} diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index a0b28a72d90cbe..93da7ee2cb1138 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -1,46 +1,115 @@ /** * WordPress dependencies */ +import { useReducer, createPortal } from '@wordpress/element'; import { BlockEditorProvider, BlockList, BlockSelectionClearer, ObserveTyping, WritingFlow, + BlockEditorKeyboardShortcuts, + __experimentalBlockSettingsMenuFirstItem, } from '@wordpress/block-editor'; import { DropZoneProvider, - FocusReturnProvider, SlotFillProvider, + Popover, } from '@wordpress/components'; /** * Internal dependencies */ +import Inspector, { BlockInspectorButton } from '../inspector'; import useSidebarBlockEditor from './use-sidebar-block-editor'; +const inspectorOpenStateReducer = ( state, action ) => { + switch ( action ) { + case 'OPEN': + return { + open: true, + busy: true, + }; + case 'TRANSITION_END': + return { + ...state, + busy: false, + }; + case 'CLOSE': + return { + open: false, + busy: true, + }; + default: + throw new Error( 'Unexpected action' ); + } +}; + export default function SidebarBlockEditor( { sidebar } ) { const [ blocks, onInput, onChange ] = useSidebarBlockEditor( sidebar ); + const [ + { open: isInspectorOpened, busy: isInspectorAnimating }, + setInspectorOpenState, + ] = useReducer( inspectorOpenStateReducer, { open: false, busy: false } ); + const parentContainer = document.getElementById( + 'customize-theme-controls' + ); return ( - <SlotFillProvider> - <DropZoneProvider> - <FocusReturnProvider> - <BlockEditorProvider - value={ blocks } - onInput={ onInput } - onChange={ onChange } - > - <BlockSelectionClearer> - <WritingFlow> - <ObserveTyping> - <BlockList /> - </ObserveTyping> - </WritingFlow> - </BlockSelectionClearer> - </BlockEditorProvider> - </FocusReturnProvider> - </DropZoneProvider> - </SlotFillProvider> + <> + <BlockEditorKeyboardShortcuts.Register /> + <SlotFillProvider> + <DropZoneProvider> + <div hidden={ isInspectorOpened && ! isInspectorAnimating }> + <BlockEditorProvider + value={ blocks } + onInput={ onInput } + onChange={ onChange } + useSubRegistry={ false } + > + <BlockEditorKeyboardShortcuts /> + + <BlockSelectionClearer> + <WritingFlow> + <ObserveTyping> + <BlockList /> + </ObserveTyping> + </WritingFlow> + </BlockSelectionClearer> + </BlockEditorProvider> + + <Popover.Slot name="block-toolbar" /> + </div> + + { createPortal( + <Inspector + isOpened={ isInspectorOpened } + isAnimating={ isInspectorAnimating } + setInspectorOpenState={ setInspectorOpenState } + />, + parentContainer + ) } + + <__experimentalBlockSettingsMenuFirstItem> + { ( { onClose } ) => ( + <BlockInspectorButton + onClick={ () => { + // Open the inspector, + setInspectorOpenState( 'OPEN' ); + // Then close the dropdown menu. + onClose(); + } } + /> + ) } + </__experimentalBlockSettingsMenuFirstItem> + + { + // We have to portal this to the parent of both the editor and the inspector, + // so that the popovers will appear above both of them. + createPortal( <Popover.Slot />, parentContainer ) + } + </DropZoneProvider> + </SlotFillProvider> + </> ); } diff --git a/packages/customize-widgets/src/create-sidebar-control.js b/packages/customize-widgets/src/create-sidebar-control.js deleted file mode 100644 index 464d2524704da6..00000000000000 --- a/packages/customize-widgets/src/create-sidebar-control.js +++ /dev/null @@ -1,25 +0,0 @@ -/** - * WordPress dependencies - */ -import { render } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import SidebarBlockEditor from './components/sidebar-block-editor'; -import SidebarAdapter from './components/sidebar-block-editor/sidebar-adapter'; - -const { wp } = window; - -export default function createSidebarControl() { - return wp.customize.Control.extend( { - ready() { - render( - <SidebarBlockEditor - sidebar={ new SidebarAdapter( this.setting, wp.customize ) } - />, - this.container[ 0 ] - ); - }, - } ); -} diff --git a/packages/customize-widgets/src/customize-sidebar-constructors.js b/packages/customize-widgets/src/customize-sidebar-constructors.js new file mode 100644 index 00000000000000..662f1a4e02c8ad --- /dev/null +++ b/packages/customize-widgets/src/customize-sidebar-constructors.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { render, unmountComponentAtNode } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import SidebarBlockEditor from './components/sidebar-block-editor'; +import SidebarAdapter from './components/sidebar-block-editor/sidebar-adapter'; + +const { + wp: { customize }, +} = window; + +class SidebarSection extends customize.Section { + onChangeExpanded( expanded, args ) { + super.onChangeExpanded( expanded, args ); + + const controls = this.controls(); + controls.forEach( ( control ) => { + control.onChangeExpanded( expanded, args ); + } ); + } +} + +class SidebarControl extends customize.Control { + onChangeExpanded() { + this.render(); + } + expanded() { + return customize.section( this.section() ).expanded(); + } + render() { + if ( this.expanded() ) { + render( + <SidebarBlockEditor + sidebar={ new SidebarAdapter( this.setting, customize ) } + />, + this.container[ 0 ] + ); + } else { + unmountComponentAtNode( this.container[ 0 ] ); + } + } + ready() { + this.render(); + } +} + +export { SidebarSection, SidebarControl }; diff --git a/packages/customize-widgets/src/index.js b/packages/customize-widgets/src/index.js index 47838b0a94da61..da765421d7598a 100644 --- a/packages/customize-widgets/src/index.js +++ b/packages/customize-widgets/src/index.js @@ -11,7 +11,10 @@ import { /** * Internal dependencies */ -import createSidebarControl from './create-sidebar-control'; +import { + SidebarSection, + SidebarControl, +} from './customize-sidebar-constructors'; const { wp } = window; @@ -56,7 +59,8 @@ export function initialize() { }, } ); - wp.customize.controlConstructor.sidebar_block_editor = createSidebarControl(); + wp.customize.sectionConstructor.sidebar = SidebarSection; + wp.customize.controlConstructor.sidebar_block_editor = SidebarControl; } wp.domReady( initialize ); diff --git a/packages/customize-widgets/src/style.scss b/packages/customize-widgets/src/style.scss index 70b786d12ed055..30a330ff4ab4dd 100644 --- a/packages/customize-widgets/src/style.scss +++ b/packages/customize-widgets/src/style.scss @@ -1 +1 @@ -// TODO +@import "./components/inspector/style.scss"; diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index fd87420e782b99..75d94c55c37c3c 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -23,7 +23,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/data": "file:../data", "@wordpress/deprecated": "file:../deprecated" diff --git a/packages/data-controls/src/index.js b/packages/data-controls/src/index.js index 3e9fa9e9b6c75b..62671b41f139ff 100644 --- a/packages/data-controls/src/index.js +++ b/packages/data-controls/src/index.js @@ -39,6 +39,7 @@ export function apiFetch( request ) { */ export function select( ...args ) { deprecated( '`select` control in `@wordpress/data-controls`', { + since: '5.7', alternative: 'built-in `resolveSelect` control in `@wordpress/data`', } ); @@ -53,6 +54,7 @@ export function select( ...args ) { */ export function syncSelect( ...args ) { deprecated( '`syncSelect` control in `@wordpress/data-controls`', { + since: '5.7', alternative: 'built-in `select` control in `@wordpress/data`', } ); @@ -67,6 +69,7 @@ export function syncSelect( ...args ) { */ export function dispatch( ...args ) { deprecated( '`dispatch` control in `@wordpress/data-controls`', { + since: '5.7', alternative: 'built-in `dispatch` control in `@wordpress/data`', } ); diff --git a/packages/data/package.json b/packages/data/package.json index 35aae7999202aa..f601037fff3216 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -24,7 +24,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:../compose", "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", diff --git a/packages/data/src/plugins/controls/index.js b/packages/data/src/plugins/controls/index.js index cd247c0b911b31..d442dfa7c1f26c 100644 --- a/packages/data/src/plugins/controls/index.js +++ b/packages/data/src/plugins/controls/index.js @@ -5,6 +5,7 @@ import deprecated from '@wordpress/deprecated'; export default ( registry ) => { deprecated( 'wp.data.plugins.controls', { + since: '5.4', hint: 'The controls plugins is now baked-in.', } ); return registry; diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index 1d86fddbc7590c..831375aefd6a12 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- Bundle type definitions. + ## 3.14.0 (2021-03-17) ## 3.0.1 (2018-12-12) diff --git a/packages/date/README.md b/packages/date/README.md index 8ac658a2197a8c..d2d9c04ba7a91e 100644 --- a/packages/date/README.md +++ b/packages/date/README.md @@ -28,8 +28,8 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `Date|string|Moment|null`: Date object or string, parsable by moment.js. -- _timezone_ `string|number|null`: Timezone to output result in or a UTC offset. Defaults to timezone from site. +- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. +- _timezone_ `string | undefined`: Timezone to output result in or a UTC offset. Defaults to timezone from site. _Returns_ @@ -50,8 +50,8 @@ _Related_ _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `Date|string|Moment|null`: Date object or string, parsable by moment.js. -- _timezone_ `string|number|boolean|null`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. +- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. +- _timezone_ `string | boolean | undefined`: Timezone to output result in or a UTC offset. Defaults to timezone from site. Notice: `boolean` is effectively deprecated, but still supported for backward compatibility reasons. _Returns_ @@ -64,7 +64,7 @@ Formats a date. Does not alter the date's timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `Date|string|Moment|null`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -89,7 +89,7 @@ Formats a date (like `date()` in PHP), in the UTC timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `Date|string|Moment|null`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -103,7 +103,7 @@ and using the UTC timezone. _Parameters_ - _dateFormat_ `string`: PHP-style formatting string. See php.net/date. -- _dateValue_ `Date|string|Moment|null`: Date object or string, parsable by moment.js. +- _dateValue_ `Moment | Date | string | undefined`: Date object or string, parsable by moment.js. _Returns_ @@ -127,7 +127,7 @@ Adds a locale to moment, using the format supplied by `wp_localize_script()`. _Parameters_ -- _dateSettings_ `Object`: Settings, including locale data. +- _dateSettings_ `DateSettings`: Settings, including locale data. <!-- END TOKEN(Autogenerated API docs) --> diff --git a/packages/date/package.json b/packages/date/package.json index 196facec202d6d..94df3dbcfbd620 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -21,8 +21,9 @@ "main": "build/index.js", "module": "build-module/index.js", "react-native": "src/index", + "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "moment": "^2.22.1", "moment-timezone": "^0.5.31" }, diff --git a/packages/date/src/index.js b/packages/date/src/index.js index 4145e3fcd5223d..6487a17be1b23d 100644 --- a/packages/date/src/index.js +++ b/packages/date/src/index.js @@ -6,6 +6,50 @@ import 'moment-timezone/moment-timezone'; import 'moment-timezone/moment-timezone-utils'; /** @typedef {import('moment').Moment} Moment */ +/** @typedef {import('moment').LocaleSpecification} MomentLocaleSpecification */ + +/** + * @typedef MeridiemConfig + * @property {string} am Lowercase AM. + * @property {string} AM Uppercase AM. + * @property {string} pm Lowercase PM. + * @property {string} PM Uppercase PM. + */ + +/** + * @typedef FormatsConfig + * @property {string} time Time format. + * @property {string} date Date format. + * @property {string} datetime Datetime format. + * @property {string} datetimeAbbreviated Abbreviated datetime format. + */ + +/** + * @typedef TimezoneConfig + * @property {string} offset Offset setting. + * @property {string} string The timezone as a string (e.g., `'America/Los_Angeles'`). + * @property {string} abbr Abbreviation for the timezone. + */ + +/* eslint-disable jsdoc/valid-types */ +/** + * @typedef L10nSettings + * @property {string} locale Moment locale. + * @property {MomentLocaleSpecification['months']} months Locale months. + * @property {MomentLocaleSpecification['monthsShort']} monthsShort Locale months short. + * @property {MomentLocaleSpecification['weekdays']} weekdays Locale weekdays. + * @property {MomentLocaleSpecification['weekdaysShort']} weekdaysShort Locale weekdays short. + * @property {MeridiemConfig} meridiem Meridiem config. + * @property {MomentLocaleSpecification['relativeTime']} relative Relative time config. + */ +/* eslint-enable jsdoc/valid-types */ + +/** + * @typedef DateSettings + * @property {L10nSettings} l10n Localization settings. + * @property {FormatsConfig} formats Date/time formats config. + * @property {TimezoneConfig} timezone Timezone settings. + */ const WP_ZONE = 'WP'; @@ -15,6 +59,7 @@ const VALID_UTC_OFFSET = /^[+-][0-1][0-9](:?[0-9][0-9])?$/; // Changes made here will likely need to be made in `lib/client-assets.php` as // well because it uses the `setSettings()` function to change these settings. +/** @type {DateSettings} */ let settings = { l10n: { locale: 'en', @@ -86,7 +131,7 @@ let settings = { /** * Adds a locale to moment, using the format supplied by `wp_localize_script()`. * - * @param {Object} dateSettings Settings, including locale data. + * @param {DateSettings} dateSettings Settings, including locale data. */ export function setSettings( dateSettings ) { settings = dateSettings; @@ -112,10 +157,13 @@ export function setSettings( dateSettings ) { }, longDateFormat: { LT: dateSettings.formats.time, + // @ts-ignore Forcing this to `null` LTS: null, + // @ts-ignore Forcing this to `null` L: null, LL: dateSettings.formats.date, LLL: dateSettings.formats.datetime, + // @ts-ignore Forcing this to `null` LLLL: null, }, // From human_time_diff? @@ -177,8 +225,6 @@ const HOUR_IN_SECONDS = 60 * MINUTE_IN_SECONDS; * * This should only be used through {@link wp.date.format}, not * directly. - * - * @type {Object} */ const formatMap = { // Day @@ -212,7 +258,7 @@ const formatMap = { */ z( momentDate ) { // DDD - 1 - return '' + parseInt( momentDate.format( 'DDD' ), 10 ) - 1; + return ( parseInt( momentDate.format( 'DDD' ), 10 ) - 1 ).toString(); }, // Week @@ -228,7 +274,7 @@ const formatMap = { * * @param {Moment} momentDate Moment instance. * - * @return {string} Formatted date. + * @return {number} Formatted date. */ t( momentDate ) { return momentDate.daysInMonth(); @@ -257,7 +303,7 @@ const formatMap = { * * @param {Moment} momentDate Moment instance. * - * @return {string} Formatted date. + * @return {number} Formatted date. */ B( momentDate ) { const timezoned = momentLib( momentDate ).utcOffset( 60 ); @@ -265,10 +311,12 @@ const formatMap = { minutes = parseInt( timezoned.format( 'm' ), 10 ), hours = parseInt( timezoned.format( 'H' ), 10 ); return parseInt( - ( seconds + - minutes * MINUTE_IN_SECONDS + - hours * HOUR_IN_SECONDS ) / - 86.4, + ( + ( seconds + + minutes * MINUTE_IN_SECONDS + + hours * HOUR_IN_SECONDS ) / + 86.4 + ).toString(), 10 ); }, @@ -300,13 +348,16 @@ const formatMap = { * * @param {Moment} momentDate Moment instance. * - * @return {string} Formatted date. + * @return {number} Formatted date. */ Z( momentDate ) { // Timezone offset in seconds. const offset = momentDate.format( 'Z' ); const sign = offset[ 0 ] === '-' ? -1 : 1; - const parts = offset.substring( 1 ).split( ':' ); + const parts = offset + .substring( 1 ) + .split( ':' ) + .map( ( n ) => parseInt( n, 10 ) ); return ( sign * ( parts[ 0 ] * HOUR_IN_MINUTES + parts[ 1 ] ) * @@ -324,14 +375,14 @@ const formatMap = { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|Moment|null} dateValue Date object or string, + * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * * @return {string} Formatted date. */ export function format( dateFormat, dateValue = new Date() ) { let i, char; - let newFormat = []; + const newFormat = []; const momentDate = momentLib( dateValue ); for ( i = 0; i < dateFormat.length; i++ ) { char = dateFormat[ i ]; @@ -343,12 +394,14 @@ export function format( dateFormat, dateValue = new Date() ) { continue; } if ( char in formatMap ) { - if ( typeof formatMap[ char ] !== 'string' ) { + const formatter = + formatMap[ /** @type {keyof formatMap} */ ( char ) ]; + if ( typeof formatter !== 'string' ) { // If the format is a function, call it. - newFormat.push( '[' + formatMap[ char ]( momentDate ) + ']' ); + newFormat.push( '[' + formatter( momentDate ) + ']' ); } else { // Otherwise, add as a formatting string. - newFormat.push( formatMap[ char ] ); + newFormat.push( formatter ); } } else { newFormat.push( '[' + char + ']' ); @@ -356,8 +409,7 @@ export function format( dateFormat, dateValue = new Date() ) { } // Join with [] between to separate characters, and replace // unneeded separators with static text. - newFormat = newFormat.join( '[]' ); - return momentDate.format( newFormat ); + return momentDate.format( newFormat.join( '[]' ) ); } /** @@ -365,9 +417,9 @@ export function format( dateFormat, dateValue = new Date() ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|Moment|null} dateValue Date object or string, parsable + * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable * by moment.js. - * @param {string|number|null} timezone Timezone to output result in or a + * @param {string | undefined} timezone Timezone to output result in or a * UTC offset. Defaults to timezone from * site. * @@ -386,7 +438,7 @@ export function date( dateFormat, dateValue = new Date(), timezone ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|Moment|null} dateValue Date object or string, + * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * * @return {string} Formatted date in English. @@ -404,9 +456,9 @@ export function gmdate( dateFormat, dateValue = new Date() ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|Moment|null} dateValue Date object or string, parsable by + * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable by * moment.js. - * @param {string|number|boolean|null} timezone Timezone to output result in or a + * @param {string | boolean | undefined} timezone Timezone to output result in or a * UTC offset. Defaults to timezone from * site. Notice: `boolean` is effectively * deprecated, but still supported for @@ -437,7 +489,7 @@ export function dateI18n( dateFormat, dateValue = new Date(), timezone ) { * * @param {string} dateFormat PHP-style formatting string. * See php.net/date. - * @param {Date|string|Moment|null} dateValue Date object or string, + * @param {Moment | Date | string | undefined} dateValue Date object or string, * parsable by moment.js. * * @return {string} Formatted date. @@ -480,9 +532,9 @@ export function getDate( dateString ) { /** * Creates a moment instance using the given timezone or, if none is provided, using global settings. * - * @param {Date|string|Moment|null} dateValue Date object or string, parsable + * @param {Moment | Date | string | undefined} dateValue Date object or string, parsable * by moment.js. - * @param {string|number|null} timezone Timezone to output result in or a + * @param {string | undefined} timezone Timezone to output result in or a * UTC offset. Defaults to timezone from * site. * diff --git a/packages/date/tsconfig.json b/packages/date/tsconfig.json new file mode 100644 index 00000000000000..f108b30216124f --- /dev/null +++ b/packages/date/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types", + "noUnusedParameters": false + }, + "include": [ "src/**/*" ] +} diff --git a/packages/deprecated/README.md b/packages/deprecated/README.md index 491752b291f104..7528df707e4c78 100644 --- a/packages/deprecated/README.md +++ b/packages/deprecated/README.md @@ -42,19 +42,21 @@ _Usage_ import deprecated from '@wordpress/deprecated'; deprecated( 'Eating meat', { - version: 'the future', + since: '2019.01.01' + version: '2020.01.01', alternative: 'vegetables', plugin: 'the earth', hint: 'You may find it beneficial to transition gradually.', } ); -// Logs: 'Eating meat is deprecated and will be removed from the earth in the future. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' +// Logs: 'Eating meat is deprecated since version 2019.01.01 and will be removed from the earth in version 2020.01.01. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' ``` _Parameters_ - _feature_ `string`: Name of the deprecated feature. - _options_ `[Object]`: Personalisation options +- _options.since_ `[string]`: Version in which the feature was deprecated. - _options.version_ `[string]`: Version in which the feature will be removed. - _options.alternative_ `[string]`: Feature to use instead - _options.plugin_ `[string]`: Plugin name if it's a plugin feature diff --git a/packages/deprecated/package.json b/packages/deprecated/package.json index e3bcb3c5216639..6a9a552ac0863f 100644 --- a/packages/deprecated/package.json +++ b/packages/deprecated/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/hooks": "file:../hooks" }, "publishConfig": { diff --git a/packages/deprecated/src/index.js b/packages/deprecated/src/index.js index 13f11a24a0df77..d9b367c11ce557 100644 --- a/packages/deprecated/src/index.js +++ b/packages/deprecated/src/index.js @@ -16,6 +16,7 @@ export const logged = Object.create( null ); * * @param {string} feature Name of the deprecated feature. * @param {Object} [options] Personalisation options + * @param {string} [options.since] Version in which the feature was deprecated. * @param {string} [options.version] Version in which the feature will be removed. * @param {string} [options.alternative] Feature to use instead * @param {string} [options.plugin] Plugin name if it's a plugin feature @@ -27,19 +28,21 @@ export const logged = Object.create( null ); * import deprecated from '@wordpress/deprecated'; * * deprecated( 'Eating meat', { - * version: 'the future', + * since: '2019.01.01' + * version: '2020.01.01', * alternative: 'vegetables', * plugin: 'the earth', * hint: 'You may find it beneficial to transition gradually.', * } ); * - * // Logs: 'Eating meat is deprecated and will be removed from the earth in the future. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' + * // Logs: 'Eating meat is deprecated since version 2019.01.01 and will be removed from the earth in version 2020.01.01. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' * ``` */ export default function deprecated( feature, options = {} ) { - const { version, alternative, plugin, link, hint } = options; + const { since, version, alternative, plugin, link, hint } = options; const pluginMessage = plugin ? ` from ${ plugin }` : ''; + const sinceMessage = since ? ` since version ${ since }` : ''; const versionMessage = version ? ` and will be removed${ pluginMessage } in version ${ version }` : ''; @@ -48,7 +51,7 @@ export default function deprecated( feature, options = {} ) { : ''; const linkMessage = link ? ` See: ${ link }` : ''; const hintMessage = hint ? ` Note: ${ hint }` : ''; - const message = `${ feature } is deprecated${ versionMessage }.${ useInsteadMessage }${ linkMessage }${ hintMessage }`; + const message = `${ feature } is deprecated${ sinceMessage }${ versionMessage }.${ useInsteadMessage }${ linkMessage }${ hintMessage }`; // Skip if already logged. if ( message in logged ) { @@ -60,6 +63,7 @@ export default function deprecated( feature, options = {} ) { * * @param {string} feature Name of the deprecated feature. * @param {?Object} options Personalisation options + * @param {string} options.since Version in which the feature was deprecated. * @param {?string} options.version Version in which the feature will be removed. * @param {?string} options.alternative Feature to use instead * @param {?string} options.plugin Plugin name if it's a plugin feature diff --git a/packages/deprecated/src/test/index.js b/packages/deprecated/src/test/index.js index 94a4823dcd4a94..62f02079f0d195 100644 --- a/packages/deprecated/src/test/index.js +++ b/packages/deprecated/src/test/index.js @@ -21,6 +21,14 @@ describe( 'deprecated', () => { expect( console ).toHaveWarnedWith( 'Eating meat is deprecated.' ); } ); + it( 'should show a deprecation warning with a since', () => { + deprecated( 'Eating meat', { since: '2019.01.01' } ); + + expect( console ).toHaveWarnedWith( + 'Eating meat is deprecated since version 2019.01.01.' + ); + } ); + it( 'should show a deprecation warning with a version', () => { deprecated( 'Eating meat', { version: '2020.01.01' } ); @@ -31,12 +39,13 @@ describe( 'deprecated', () => { it( 'should show a deprecation warning with an alternative', () => { deprecated( 'Eating meat', { + since: '2019.01.01', version: '2020.01.01', alternative: 'vegetables', } ); expect( console ).toHaveWarnedWith( - 'Eating meat is deprecated and will be removed in version 2020.01.01. Please use vegetables instead.' + 'Eating meat is deprecated since version 2019.01.01 and will be removed in version 2020.01.01. Please use vegetables instead.' ); } ); @@ -67,6 +76,7 @@ describe( 'deprecated', () => { it( 'should show a deprecation warning with a hint', () => { deprecated( 'Eating meat', { + since: '2019.01.01', version: '2020.01.01', alternative: 'vegetables', plugin: 'the earth', @@ -74,7 +84,7 @@ describe( 'deprecated', () => { } ); expect( console ).toHaveWarnedWith( - 'Eating meat is deprecated and will be removed from the earth in version 2020.01.01. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' + 'Eating meat is deprecated since version 2019.01.01 and will be removed from the earth in version 2020.01.01. Please use vegetables instead. Note: You may find it beneficial to transition gradually.' ); } ); diff --git a/packages/docgen/package.json b/packages/docgen/package.json index 53e7770baa5fd4..e6846fde1d4184 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -27,7 +27,7 @@ "docgen": "./bin/cli.js" }, "dependencies": { - "@babel/core": "^7.12.9", + "@babel/core": "^7.13.10", "comment-parser": "^1.1.1", "lodash": "^4.17.19", "mdast-util-inject": "1.1.0", diff --git a/packages/dom-ready/package.json b/packages/dom-ready/package.json index bb24cbfddd953c..0f16aa2adb3e55 100644 --- a/packages/dom-ready/package.json +++ b/packages/dom-ready/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/dom/package.json b/packages/dom/package.json index 722da356eb8b0f..2fabf1204c0e64 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -24,7 +24,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/packages/dom/src/dom.js b/packages/dom/src/dom.js deleted file mode 100644 index 74ecd9f518beb4..00000000000000 --- a/packages/dom/src/dom.js +++ /dev/null @@ -1,1004 +0,0 @@ -/** - * External dependencies - */ -import { includes, noop } from 'lodash'; - -/** - * Internal dependencies - */ -import { isPhrasingContent } from './phrasing-content'; - -function getComputedStyle( node ) { - return node.ownerDocument.defaultView.getComputedStyle( node ); -} - -/** - * Gets the height of the range without ignoring zero width rectangles, which - * some browsers ignore when creating a union. - * - * @param {Range} range The range to check. - */ -function getRangeHeight( range ) { - const rects = Array.from( range.getClientRects() ); - - if ( ! rects.length ) { - return; - } - - const highestTop = Math.min( ...rects.map( ( { top } ) => top ) ); - const lowestBottom = Math.max( ...rects.map( ( { bottom } ) => bottom ) ); - - return lowestBottom - highestTop; -} - -/** - * Returns true if the given selection object is in the forward direction, or - * false otherwise. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition - * - * @param {Selection} selection Selection object to check. - * - * @return {boolean} Whether the selection is forward. - */ -function isSelectionForward( selection ) { - const { anchorNode, focusNode, anchorOffset, focusOffset } = selection; - - const position = anchorNode.compareDocumentPosition( focusNode ); - - // Disable reason: `Node#compareDocumentPosition` returns a bitmask value, - // so bitwise operators are intended. - /* eslint-disable no-bitwise */ - // Compare whether anchor node precedes focus node. If focus node (where - // end of selection occurs) is after the anchor node, it is forward. - if ( position & anchorNode.DOCUMENT_POSITION_PRECEDING ) { - return false; - } - - if ( position & anchorNode.DOCUMENT_POSITION_FOLLOWING ) { - return true; - } - /* eslint-enable no-bitwise */ - - // `compareDocumentPosition` returns 0 when passed the same node, in which - // case compare offsets. - if ( position === 0 ) { - return anchorOffset <= focusOffset; - } - - // This should never be reached, but return true as default case. - return true; -} - -/** - * Check whether the selection is at the edge of the container. Checks for - * horizontal position by default. Set `onlyVertical` to true to check only - * vertically. - * - * @param {Element} container Focusable element. - * @param {boolean} isReverse Set to true to check left, false to check right. - * @param {boolean} onlyVertical Set to true to check only vertical position. - * - * @return {boolean} True if at the edge, false if not. - */ -function isEdge( container, isReverse, onlyVertical ) { - if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { - if ( container.selectionStart !== container.selectionEnd ) { - return false; - } - - if ( isReverse ) { - return container.selectionStart === 0; - } - - return container.value.length === container.selectionStart; - } - - if ( ! container.isContentEditable ) { - return true; - } - - const { ownerDocument } = container; - const { defaultView } = ownerDocument; - - const selection = defaultView.getSelection(); - - if ( ! selection.rangeCount ) { - return false; - } - - const range = selection.getRangeAt( 0 ); - const collapsedRange = range.cloneRange(); - const isForward = isSelectionForward( selection ); - const isCollapsed = selection.isCollapsed; - - // Collapse in direction of selection. - if ( ! isCollapsed ) { - collapsedRange.collapse( ! isForward ); - } - - const collapsedRangeRect = getRectangleFromRange( collapsedRange ); - const rangeRect = getRectangleFromRange( range ); - - if ( ! collapsedRangeRect || ! rangeRect ) { - return false; - } - - // Only consider the multiline selection at the edge if the direction is - // towards the edge. The selection is multiline if it is taller than the - // collapsed selection. - if ( - ! isCollapsed && - getRangeHeight( range ) > collapsedRangeRect.height && - isForward === isReverse - ) { - return false; - } - - // In the case of RTL scripts, the horizontal edge is at the opposite side. - const { direction } = getComputedStyle( container ); - const isReverseDir = direction === 'rtl' ? ! isReverse : isReverse; - - const containerRect = container.getBoundingClientRect(); - - // To check if a selection is at the edge, we insert a test selection at the - // edge of the container and check if the selections have the same vertical - // or horizontal position. If they do, the selection is at the edge. - // This method proves to be better than a DOM-based calculation for the - // horizontal edge, since it ignores empty textnodes and a trailing line - // break element. In other words, we need to check visual positioning, not - // DOM positioning. - // It also proves better than using the computed style for the vertical - // edge, because we cannot know the padding and line height reliably in - // pixels. `getComputedStyle` may return a value with different units. - const x = isReverseDir ? containerRect.left + 1 : containerRect.right - 1; - const y = isReverse ? containerRect.top + 1 : containerRect.bottom - 1; - const testRange = hiddenCaretRangeFromPoint( - ownerDocument, - x, - y, - container - ); - - if ( ! testRange ) { - return false; - } - - const testRect = getRectangleFromRange( testRange ); - - if ( ! testRect ) { - return false; - } - - const verticalSide = isReverse ? 'top' : 'bottom'; - const horizontalSide = isReverseDir ? 'left' : 'right'; - const verticalDiff = testRect[ verticalSide ] - rangeRect[ verticalSide ]; - const horizontalDiff = - testRect[ horizontalSide ] - collapsedRangeRect[ horizontalSide ]; - - // Allow the position to be 1px off. - const hasVerticalDiff = Math.abs( verticalDiff ) <= 1; - const hasHorizontalDiff = Math.abs( horizontalDiff ) <= 1; - - return onlyVertical - ? hasVerticalDiff - : hasVerticalDiff && hasHorizontalDiff; -} - -/** - * Check whether the selection is horizontally at the edge of the container. - * - * @param {Element} container Focusable element. - * @param {boolean} isReverse Set to true to check left, false for right. - * - * @return {boolean} True if at the horizontal edge, false if not. - */ -export function isHorizontalEdge( container, isReverse ) { - return isEdge( container, isReverse ); -} - -/** - * Check whether the selection is vertically at the edge of the container. - * - * @param {Element} container Focusable element. - * @param {boolean} isReverse Set to true to check top, false for bottom. - * - * @return {boolean} True if at the vertical edge, false if not. - */ -export function isVerticalEdge( container, isReverse ) { - return isEdge( container, isReverse, true ); -} - -/** - * Get the rectangle of a given Range. - * - * @param {Range} range The range. - * - * @return {DOMRect} The rectangle. - */ -export function getRectangleFromRange( range ) { - // For uncollapsed ranges, get the rectangle that bounds the contents of the - // range; this a rectangle enclosing the union of the bounding rectangles - // for all the elements in the range. - if ( ! range.collapsed ) { - return range.getBoundingClientRect(); - } - - const { startContainer } = range; - const { ownerDocument } = startContainer; - - // Correct invalid "BR" ranges. The cannot contain any children. - if ( startContainer.nodeName === 'BR' ) { - const { parentNode } = startContainer; - const index = Array.from( parentNode.childNodes ).indexOf( - startContainer - ); - - range = ownerDocument.createRange(); - range.setStart( parentNode, index ); - range.setEnd( parentNode, index ); - } - - let rect = range.getClientRects()[ 0 ]; - - // If the collapsed range starts (and therefore ends) at an element node, - // `getClientRects` can be empty in some browsers. This can be resolved - // by adding a temporary text node with zero-width space to the range. - // - // See: https://stackoverflow.com/a/6847328/995445 - if ( ! rect ) { - const padNode = ownerDocument.createTextNode( '\u200b' ); - // Do not modify the live range. - range = range.cloneRange(); - range.insertNode( padNode ); - rect = range.getClientRects()[ 0 ]; - padNode.parentNode.removeChild( padNode ); - } - - return rect; -} - -/** - * Get the rectangle for the selection in a container. - * - * @param {Window} win The window of the selection. - * - * @return {?DOMRect} The rectangle. - */ -export function computeCaretRect( win ) { - const selection = win.getSelection(); - const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; - - if ( ! range ) { - return; - } - - return getRectangleFromRange( range ); -} - -/** - * Places the caret at start or end of a given element. - * - * @param {Element} container Focusable element. - * @param {boolean} isReverse True for end, false for start. - */ -export function placeCaretAtHorizontalEdge( container, isReverse ) { - if ( ! container ) { - return; - } - - if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { - container.focus(); - if ( isReverse ) { - container.selectionStart = container.value.length; - container.selectionEnd = container.value.length; - } else { - container.selectionStart = 0; - container.selectionEnd = 0; - } - return; - } - - container.focus(); - - if ( ! container.isContentEditable ) { - return; - } - - // Select on extent child of the container, not the container itself. This - // avoids the selection always being `endOffset` of 1 when placed at end, - // where `startContainer`, `endContainer` would always be container itself. - const rangeTarget = container[ isReverse ? 'lastChild' : 'firstChild' ]; - - // If no range target, it implies that the container is empty. Focusing is - // sufficient for caret to be placed correctly. - if ( ! rangeTarget ) { - return; - } - - const { ownerDocument } = container; - const { defaultView } = ownerDocument; - const selection = defaultView.getSelection(); - const range = ownerDocument.createRange(); - - range.selectNodeContents( rangeTarget ); - range.collapse( ! isReverse ); - - selection.removeAllRanges(); - selection.addRange( range ); -} - -/** - * Polyfill. - * Get a collapsed range for a given point. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint - * - * @param {Document} doc The document of the range. - * @param {number} x Horizontal position within the current viewport. - * @param {number} y Vertical position within the current viewport. - * - * @return {?Range} The best range for the given point. - */ -function caretRangeFromPoint( doc, x, y ) { - if ( doc.caretRangeFromPoint ) { - return doc.caretRangeFromPoint( x, y ); - } - - if ( ! doc.caretPositionFromPoint ) { - return null; - } - - const point = doc.caretPositionFromPoint( x, y ); - - // If x or y are negative, outside viewport, or there is no text entry node. - // https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint - if ( ! point ) { - return null; - } - - const range = doc.createRange(); - - range.setStart( point.offsetNode, point.offset ); - range.collapse( true ); - - return range; -} - -/** - * Get a collapsed range for a given point. - * Gives the container a temporary high z-index (above any UI). - * This is preferred over getting the UI nodes and set styles there. - * - * @param {Document} doc The document of the range. - * @param {number} x Horizontal position within the current viewport. - * @param {number} y Vertical position within the current viewport. - * @param {Element} container Container in which the range is expected to be found. - * - * @return {?Range} The best range for the given point. - */ -function hiddenCaretRangeFromPoint( doc, x, y, container ) { - const originalZIndex = container.style.zIndex; - const originalPosition = container.style.position; - - // A z-index only works if the element position is not static. - container.style.zIndex = '10000'; - container.style.position = 'relative'; - - const range = caretRangeFromPoint( doc, x, y ); - - container.style.zIndex = originalZIndex; - container.style.position = originalPosition; - - return range; -} - -/** - * Places the caret at the top or bottom of a given element. - * - * @param {Element} container Focusable element. - * @param {boolean} isReverse True for bottom, false for top. - * @param {DOMRect} [rect] The rectangle to position the caret with. - * @param {boolean} [mayUseScroll=true] True to allow scrolling, false to disallow. - */ -export function placeCaretAtVerticalEdge( - container, - isReverse, - rect, - mayUseScroll = true -) { - if ( ! container ) { - return; - } - - if ( ! rect || ! container.isContentEditable ) { - placeCaretAtHorizontalEdge( container, isReverse ); - return; - } - - // Offset by a buffer half the height of the caret rect. This is needed - // because caretRangeFromPoint may default to the end of the selection if - // offset is too close to the edge. It's unclear how to precisely calculate - // this threshold; it may be the padded area of some combination of line - // height, caret height, and font size. The buffer offset is effectively - // equivalent to a point at half the height of a line of text. - const buffer = rect.height / 2; - const editableRect = container.getBoundingClientRect(); - const x = rect.left; - const y = isReverse - ? editableRect.bottom - buffer - : editableRect.top + buffer; - - const { ownerDocument } = container; - const { defaultView } = ownerDocument; - const range = hiddenCaretRangeFromPoint( ownerDocument, x, y, container ); - - if ( ! range || ! container.contains( range.startContainer ) ) { - if ( - mayUseScroll && - ( ! range || - ! range.startContainer || - ! range.startContainer.contains( container ) ) - ) { - // Might be out of view. - // Easier than attempting to calculate manually. - container.scrollIntoView( isReverse ); - placeCaretAtVerticalEdge( container, isReverse, rect, false ); - return; - } - - placeCaretAtHorizontalEdge( container, isReverse ); - return; - } - - const selection = defaultView.getSelection(); - selection.removeAllRanges(); - selection.addRange( range ); - container.focus(); - // Editable was already focussed, it goes back to old range... - // This fixes it. - selection.removeAllRanges(); - selection.addRange( range ); -} - -/** - * Check whether the given element is a text field, where text field is defined - * by the ability to select within the input, or that it is contenteditable. - * - * See: https://html.spec.whatwg.org/#textFieldSelection - * - * @param {HTMLElement} element The HTML element. - * - * @return {boolean} True if the element is an text field, false if not. - */ -export function isTextField( element ) { - const { nodeName, contentEditable } = element; - const nonTextInputs = [ - 'button', - 'checkbox', - 'hidden', - 'file', - 'radio', - 'image', - 'range', - 'reset', - 'submit', - 'number', - ]; - return ( - ( nodeName === 'INPUT' && ! nonTextInputs.includes( element.type ) ) || - nodeName === 'TEXTAREA' || - contentEditable === 'true' - ); -} - -/** - * Check whether the given element is an input field of type number - * and has a valueAsNumber - * - * @param {HTMLElement} element The HTML element. - * - * @return {boolean} True if the element is input and holds a number. - */ -export function isNumberInput( element ) { - const { nodeName, type, valueAsNumber } = element; - - return nodeName === 'INPUT' && type === 'number' && !! valueAsNumber; -} - -/** - * Check whether the current document has selected text. This applies to ranges - * of text in the document, and not selection inside <input> and <textarea> - * elements. - * - * See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects. - * - * @param {Document} doc The document to check. - * - * @return {boolean} True if there is selection, false if not. - */ -export function documentHasTextSelection( doc ) { - const selection = doc.defaultView.getSelection(); - const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; - return range && ! range.collapsed; -} - -/** - * Check whether the given element, assumed an input field or textarea, - * contains a (uncollapsed) selection of text. - * - * Note: this is perhaps an abuse of the term "selection", since these elements - * manage selection differently and aren't covered by Selection#collapsed. - * - * See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects. - * - * @param {HTMLElement} element The HTML element. - * - * @return {boolean} Whether the input/textareaa element has some "selection". - */ -function inputFieldHasUncollapsedSelection( element ) { - if ( ! isTextField( element ) && ! isNumberInput( element ) ) { - return false; - } - try { - const { selectionStart, selectionEnd } = element; - - return selectionStart !== null && selectionStart !== selectionEnd; - } catch ( error ) { - // Safari throws an exception when trying to get `selectionStart` - // on non-text <input> elements (which, understandably, don't - // have the text selection API). We catch this via a try/catch - // block, as opposed to a more explicit check of the element's - // input types, because of Safari's non-standard behavior. This - // also means we don't have to worry about the list of input - // types that support `selectionStart` changing as the HTML spec - // evolves over time. - return false; - } -} - -/** - * Check whether the current document has any sort of selection. This includes - * ranges of text across elements and any selection inside <input> and - * <textarea> elements. - * - * @param {Document} doc The document to check. - * - * @return {boolean} Whether there is any sort of "selection" in the document. - */ -export function documentHasUncollapsedSelection( doc ) { - return ( - documentHasTextSelection( doc ) || - inputFieldHasUncollapsedSelection( doc.activeElement ) - ); -} - -/** - * Check whether the current document has a selection. This checks for both - * focus in an input field and general text selection. - * - * @param {Document} doc The document to check. - * - * @return {boolean} True if there is selection, false if not. - */ -export function documentHasSelection( doc ) { - return ( - isTextField( doc.activeElement ) || - isNumberInput( doc.activeElement ) || - documentHasTextSelection( doc ) - ); -} - -/** - * Check whether the contents of the element have been entirely selected. - * Returns true if there is no possibility of selection. - * - * @param {Element} element The element to check. - * - * @return {boolean} True if entirely selected, false if not. - */ -export function isEntirelySelected( element ) { - if ( includes( [ 'INPUT', 'TEXTAREA' ], element.nodeName ) ) { - return ( - element.selectionStart === 0 && - element.value.length === element.selectionEnd - ); - } - - if ( ! element.isContentEditable ) { - return true; - } - - const { ownerDocument } = element; - const { defaultView } = ownerDocument; - const selection = defaultView.getSelection(); - const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; - - if ( ! range ) { - return true; - } - - const { startContainer, endContainer, startOffset, endOffset } = range; - - if ( - startContainer === element && - endContainer === element && - startOffset === 0 && - endOffset === element.childNodes.length - ) { - return true; - } - - const lastChild = element.lastChild; - const lastChildContentLength = - lastChild.nodeType === lastChild.TEXT_NODE - ? lastChild.data.length - : lastChild.childNodes.length; - - return ( - startContainer === element.firstChild && - endContainer === element.lastChild && - startOffset === 0 && - endOffset === lastChildContentLength - ); -} - -/** - * Given a DOM node, finds the closest scrollable container node. - * - * @param {Element} node Node from which to start. - * - * @return {?Element} Scrollable container node, if found. - */ -export function getScrollContainer( node ) { - if ( ! node ) { - return; - } - - // Scrollable if scrollable height exceeds displayed... - if ( node.scrollHeight > node.clientHeight ) { - // ...except when overflow is defined to be hidden or visible - const { overflowY } = getComputedStyle( node ); - if ( /(auto|scroll)/.test( overflowY ) ) { - return node; - } - } - - // Continue traversing - return getScrollContainer( node.parentNode ); -} - -/** - * Returns the closest positioned element, or null under any of the conditions - * of the offsetParent specification. Unlike offsetParent, this function is not - * limited to HTMLElement and accepts any Node (e.g. Node.TEXT_NODE). - * - * @see https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent - * - * @param {Node} node Node from which to find offset parent. - * - * @return {?Node} Offset parent. - */ -export function getOffsetParent( node ) { - // Cannot retrieve computed style or offset parent only anything other than - // an element node, so find the closest element node. - let closestElement; - while ( ( closestElement = node.parentNode ) ) { - if ( closestElement.nodeType === closestElement.ELEMENT_NODE ) { - break; - } - } - - if ( ! closestElement ) { - return null; - } - - // If the closest element is already positioned, return it, as offsetParent - // does not otherwise consider the node itself. - if ( getComputedStyle( closestElement ).position !== 'static' ) { - return closestElement; - } - - return closestElement.offsetParent; -} - -/** - * Given two DOM nodes, replaces the former with the latter in the DOM. - * - * @param {Element} processedNode Node to be removed. - * @param {Element} newNode Node to be inserted in its place. - * @return {void} - */ -export function replace( processedNode, newNode ) { - insertAfter( newNode, processedNode.parentNode ); - remove( processedNode ); -} - -/** - * Given a DOM node, removes it from the DOM. - * - * @param {Element} node Node to be removed. - * @return {void} - */ -export function remove( node ) { - node.parentNode.removeChild( node ); -} - -/** - * Given two DOM nodes, inserts the former in the DOM as the next sibling of - * the latter. - * - * @param {Element} newNode Node to be inserted. - * @param {Element} referenceNode Node after which to perform the insertion. - * @return {void} - */ -export function insertAfter( newNode, referenceNode ) { - referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); -} - -/** - * Unwrap the given node. This means any child nodes are moved to the parent. - * - * @param {Node} node The node to unwrap. - * - * @return {void} - */ -export function unwrap( node ) { - const parent = node.parentNode; - - while ( node.firstChild ) { - parent.insertBefore( node.firstChild, node ); - } - - parent.removeChild( node ); -} - -/** - * Replaces the given node with a new node with the given tag name. - * - * @param {Element} node The node to replace - * @param {string} tagName The new tag name. - * - * @return {Element} The new node. - */ -export function replaceTag( node, tagName ) { - const newNode = node.ownerDocument.createElement( tagName ); - - while ( node.firstChild ) { - newNode.appendChild( node.firstChild ); - } - - node.parentNode.replaceChild( newNode, node ); - - return newNode; -} - -/** - * Wraps the given node with a new node with the given tag name. - * - * @param {Element} newNode The node to insert. - * @param {Element} referenceNode The node to wrap. - */ -export function wrap( newNode, referenceNode ) { - referenceNode.parentNode.insertBefore( newNode, referenceNode ); - newNode.appendChild( referenceNode ); -} - -/** - * Removes any HTML tags from the provided string. - * - * @param {string} html The string containing html. - * - * @return {string} The text content with any html removed. - */ -export function __unstableStripHTML( html ) { - const document = new window.DOMParser().parseFromString( - html, - 'text/html' - ); - return document.body.textContent || ''; -} - -/** - * Given a schema, unwraps or removes nodes, attributes and classes on a node - * list. - * - * @param {NodeList} nodeList The nodeList to filter. - * @param {Document} doc The document of the nodeList. - * @param {Object} schema An array of functions that can mutate with the provided node. - * @param {Object} inline Whether to clean for inline mode. - */ -function cleanNodeList( nodeList, doc, schema, inline ) { - Array.from( nodeList ).forEach( ( node ) => { - const tag = node.nodeName.toLowerCase(); - - // It's a valid child, if the tag exists in the schema without an isMatch - // function, or with an isMatch function that matches the node. - if ( - schema.hasOwnProperty( tag ) && - ( ! schema[ tag ].isMatch || schema[ tag ].isMatch( node ) ) - ) { - if ( node.nodeType === node.ELEMENT_NODE ) { - const { - attributes = [], - classes = [], - children, - require = [], - allowEmpty, - } = schema[ tag ]; - - // If the node is empty and it's supposed to have children, - // remove the node. - if ( children && ! allowEmpty && isEmpty( node ) ) { - remove( node ); - return; - } - - if ( node.hasAttributes() ) { - // Strip invalid attributes. - Array.from( node.attributes ).forEach( ( { name } ) => { - if ( - name !== 'class' && - ! includes( attributes, name ) - ) { - node.removeAttribute( name ); - } - } ); - - // Strip invalid classes. - // In jsdom-jscore, 'node.classList' can be undefined. - // TODO: Explore patching this in jsdom-jscore. - if ( node.classList && node.classList.length ) { - const mattchers = classes.map( ( item ) => { - if ( typeof item === 'string' ) { - return ( className ) => className === item; - } else if ( item instanceof RegExp ) { - return ( className ) => item.test( className ); - } - - return noop; - } ); - - Array.from( node.classList ).forEach( ( name ) => { - if ( - ! mattchers.some( ( isMatch ) => - isMatch( name ) - ) - ) { - node.classList.remove( name ); - } - } ); - - if ( ! node.classList.length ) { - node.removeAttribute( 'class' ); - } - } - } - - if ( node.hasChildNodes() ) { - // Do not filter any content. - if ( children === '*' ) { - return; - } - - // Continue if the node is supposed to have children. - if ( children ) { - // If a parent requires certain children, but it does - // not have them, drop the parent and continue. - if ( - require.length && - ! node.querySelector( require.join( ',' ) ) - ) { - cleanNodeList( - node.childNodes, - doc, - schema, - inline - ); - unwrap( node ); - // If the node is at the top, phrasing content, and - // contains children that are block content, unwrap - // the node because it is invalid. - } else if ( - node.parentNode.nodeName === 'BODY' && - isPhrasingContent( node ) - ) { - cleanNodeList( - node.childNodes, - doc, - schema, - inline - ); - - if ( - Array.from( node.childNodes ).some( - ( child ) => ! isPhrasingContent( child ) - ) - ) { - unwrap( node ); - } - } else { - cleanNodeList( - node.childNodes, - doc, - children, - inline - ); - } - // Remove children if the node is not supposed to have any. - } else { - while ( node.firstChild ) { - remove( node.firstChild ); - } - } - } - } - // Invalid child. Continue with schema at the same place and unwrap. - } else { - cleanNodeList( node.childNodes, doc, schema, inline ); - - // For inline mode, insert a line break when unwrapping nodes that - // are not phrasing content. - if ( - inline && - ! isPhrasingContent( node ) && - node.nextElementSibling - ) { - insertAfter( doc.createElement( 'br' ), node ); - } - - unwrap( node ); - } - } ); -} - -/** - * Recursively checks if an element is empty. An element is not empty if it - * contains text or contains elements with attributes such as images. - * - * @param {Element} element The element to check. - * - * @return {boolean} Whether or not the element is empty. - */ -export function isEmpty( element ) { - if ( ! element.hasChildNodes() ) { - return true; - } - - return Array.from( element.childNodes ).every( ( node ) => { - if ( node.nodeType === node.TEXT_NODE ) { - return ! node.nodeValue.trim(); - } - - if ( node.nodeType === node.ELEMENT_NODE ) { - if ( node.nodeName === 'BR' ) { - return true; - } else if ( node.hasAttributes() ) { - return false; - } - - return isEmpty( node ); - } - - return true; - } ); -} - -/** - * Given a schema, unwraps or removes nodes, attributes and classes on HTML. - * - * @param {string} HTML The HTML to clean up. - * @param {Object} schema Schema for the HTML. - * @param {Object} inline Whether to clean for inline mode. - * - * @return {string} The cleaned up HTML. - */ -export function removeInvalidHTML( HTML, schema, inline ) { - const doc = document.implementation.createHTMLDocument( '' ); - - doc.body.innerHTML = HTML; - - cleanNodeList( doc.body.childNodes, doc, schema, inline ); - - return doc.body.innerHTML; -} diff --git a/packages/dom/src/dom/caret-range-from-point.js b/packages/dom/src/dom/caret-range-from-point.js new file mode 100644 index 00000000000000..b72cf5ec154e5b --- /dev/null +++ b/packages/dom/src/dom/caret-range-from-point.js @@ -0,0 +1,36 @@ +/** + * Polyfill. + * Get a collapsed range for a given point. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint + * + * @param {Document} doc The document of the range. + * @param {number} x Horizontal position within the current viewport. + * @param {number} y Vertical position within the current viewport. + * + * @return {?Range} The best range for the given point. + */ +export default function caretRangeFromPoint( doc, x, y ) { + if ( doc.caretRangeFromPoint ) { + return doc.caretRangeFromPoint( x, y ); + } + + if ( ! doc.caretPositionFromPoint ) { + return null; + } + + const point = doc.caretPositionFromPoint( x, y ); + + // If x or y are negative, outside viewport, or there is no text entry node. + // https://developer.mozilla.org/en-US/docs/Web/API/Document/caretRangeFromPoint + if ( ! point ) { + return null; + } + + const range = doc.createRange(); + + range.setStart( point.offsetNode, point.offset ); + range.collapse( true ); + + return range; +} diff --git a/packages/dom/src/dom/clean-node-list.js b/packages/dom/src/dom/clean-node-list.js new file mode 100644 index 00000000000000..45bea2becdffd0 --- /dev/null +++ b/packages/dom/src/dom/clean-node-list.js @@ -0,0 +1,166 @@ +/** + * External dependencies + */ +import { includes, noop } from 'lodash'; + +/** + * Internal dependencies + */ +import isEmpty from './is-empty'; +import remove from './remove'; +import unwrap from './unwrap'; +import { isPhrasingContent } from '../phrasing-content'; +import insertAfter from './insert-after'; + +/** + * Given a schema, unwraps or removes nodes, attributes and classes on a node + * list. + * + * @param {NodeList} nodeList The nodeList to filter. + * @param {Document} doc The document of the nodeList. + * @param {Object} schema An array of functions that can mutate with the provided node. + * @param {Object} inline Whether to clean for inline mode. + */ +export default function cleanNodeList( nodeList, doc, schema, inline ) { + Array.from( nodeList ).forEach( ( node ) => { + const tag = node.nodeName.toLowerCase(); + + // It's a valid child, if the tag exists in the schema without an isMatch + // function, or with an isMatch function that matches the node. + if ( + schema.hasOwnProperty( tag ) && + ( ! schema[ tag ].isMatch || schema[ tag ].isMatch( node ) ) + ) { + if ( node.nodeType === node.ELEMENT_NODE ) { + const { + attributes = [], + classes = [], + children, + require = [], + allowEmpty, + } = schema[ tag ]; + + // If the node is empty and it's supposed to have children, + // remove the node. + if ( children && ! allowEmpty && isEmpty( node ) ) { + remove( node ); + return; + } + + if ( node.hasAttributes() ) { + // Strip invalid attributes. + Array.from( node.attributes ).forEach( ( { name } ) => { + if ( + name !== 'class' && + ! includes( attributes, name ) + ) { + node.removeAttribute( name ); + } + } ); + + // Strip invalid classes. + // In jsdom-jscore, 'node.classList' can be undefined. + // TODO: Explore patching this in jsdom-jscore. + if ( node.classList && node.classList.length ) { + const mattchers = classes.map( ( item ) => { + if ( typeof item === 'string' ) { + return ( className ) => className === item; + } else if ( item instanceof RegExp ) { + return ( className ) => item.test( className ); + } + + return noop; + } ); + + Array.from( node.classList ).forEach( ( name ) => { + if ( + ! mattchers.some( ( isMatch ) => + isMatch( name ) + ) + ) { + node.classList.remove( name ); + } + } ); + + if ( ! node.classList.length ) { + node.removeAttribute( 'class' ); + } + } + } + + if ( node.hasChildNodes() ) { + // Do not filter any content. + if ( children === '*' ) { + return; + } + + // Continue if the node is supposed to have children. + if ( children ) { + // If a parent requires certain children, but it does + // not have them, drop the parent and continue. + if ( + require.length && + ! node.querySelector( require.join( ',' ) ) + ) { + cleanNodeList( + node.childNodes, + doc, + schema, + inline + ); + unwrap( node ); + // If the node is at the top, phrasing content, and + // contains children that are block content, unwrap + // the node because it is invalid. + } else if ( + node.parentNode.nodeName === 'BODY' && + isPhrasingContent( node ) + ) { + cleanNodeList( + node.childNodes, + doc, + schema, + inline + ); + + if ( + Array.from( node.childNodes ).some( + ( child ) => ! isPhrasingContent( child ) + ) + ) { + unwrap( node ); + } + } else { + cleanNodeList( + node.childNodes, + doc, + children, + inline + ); + } + // Remove children if the node is not supposed to have any. + } else { + while ( node.firstChild ) { + remove( node.firstChild ); + } + } + } + } + // Invalid child. Continue with schema at the same place and unwrap. + } else { + cleanNodeList( node.childNodes, doc, schema, inline ); + + // For inline mode, insert a line break when unwrapping nodes that + // are not phrasing content. + if ( + inline && + ! isPhrasingContent( node ) && + node.nextElementSibling + ) { + insertAfter( doc.createElement( 'br' ), node ); + } + + unwrap( node ); + } + } ); +} diff --git a/packages/dom/src/dom/compute-caret-rect.js b/packages/dom/src/dom/compute-caret-rect.js new file mode 100644 index 00000000000000..f2442678e116b4 --- /dev/null +++ b/packages/dom/src/dom/compute-caret-rect.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import getRectangleFromRange from './get-rectangle-from-range'; + +/** + * Get the rectangle for the selection in a container. + * + * @param {Window} win The window of the selection. + * + * @return {?DOMRect} The rectangle. + */ +export default function computeCaretRect( win ) { + const selection = win.getSelection(); + const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; + + if ( ! range ) { + return; + } + + return getRectangleFromRange( range ); +} diff --git a/packages/dom/src/dom/document-has-selection.js b/packages/dom/src/dom/document-has-selection.js new file mode 100644 index 00000000000000..6abd7916325ec9 --- /dev/null +++ b/packages/dom/src/dom/document-has-selection.js @@ -0,0 +1,22 @@ +/** + * Internal dependencies + */ +import isTextField from './is-text-field'; +import isNumberInput from './is-number-input'; +import documentHasTextSelection from './document-has-text-selection'; + +/** + * Check whether the current document has a selection. This checks for both + * focus in an input field and general text selection. + * + * @param {Document} doc The document to check. + * + * @return {boolean} True if there is selection, false if not. + */ +export default function documentHasSelection( doc ) { + return ( + isTextField( doc.activeElement ) || + isNumberInput( doc.activeElement ) || + documentHasTextSelection( doc ) + ); +} diff --git a/packages/dom/src/dom/document-has-text-selection.js b/packages/dom/src/dom/document-has-text-selection.js new file mode 100644 index 00000000000000..d81a46c15b021f --- /dev/null +++ b/packages/dom/src/dom/document-has-text-selection.js @@ -0,0 +1,16 @@ +/** + * Check whether the current document has selected text. This applies to ranges + * of text in the document, and not selection inside <input> and <textarea> + * elements. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects. + * + * @param {Document} doc The document to check. + * + * @return {boolean} True if there is selection, false if not. + */ +export default function documentHasTextSelection( doc ) { + const selection = doc.defaultView.getSelection(); + const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; + return range && ! range.collapsed; +} diff --git a/packages/dom/src/dom/document-has-uncollapsed-selection.js b/packages/dom/src/dom/document-has-uncollapsed-selection.js new file mode 100644 index 00000000000000..7cb68f00ec3ad1 --- /dev/null +++ b/packages/dom/src/dom/document-has-uncollapsed-selection.js @@ -0,0 +1,21 @@ +/** + * Internal dependencies + */ +import documentHasTextSelection from './document-has-text-selection'; +import inputFieldHasUncollapsedSelection from './input-field-has-uncollapsed-selection'; + +/** + * Check whether the current document has any sort of selection. This includes + * ranges of text across elements and any selection inside <input> and + * <textarea> elements. + * + * @param {Document} doc The document to check. + * + * @return {boolean} Whether there is any sort of "selection" in the document. + */ +export default function documentHasUncollapsedSelection( doc ) { + return ( + documentHasTextSelection( doc ) || + inputFieldHasUncollapsedSelection( doc.activeElement ) + ); +} diff --git a/packages/dom/src/dom/get-computed-style.js b/packages/dom/src/dom/get-computed-style.js new file mode 100644 index 00000000000000..76488965c1fd40 --- /dev/null +++ b/packages/dom/src/dom/get-computed-style.js @@ -0,0 +1,3 @@ +export default function getComputedStyle( node ) { + return node.ownerDocument.defaultView.getComputedStyle( node ); +} diff --git a/packages/dom/src/dom/get-offset-parent.js b/packages/dom/src/dom/get-offset-parent.js new file mode 100644 index 00000000000000..8310447c5b9851 --- /dev/null +++ b/packages/dom/src/dom/get-offset-parent.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import getComputedStyle from './get-computed-style'; + +/** + * Returns the closest positioned element, or null under any of the conditions + * of the offsetParent specification. Unlike offsetParent, this function is not + * limited to HTMLElement and accepts any Node (e.g. Node.TEXT_NODE). + * + * @see https://drafts.csswg.org/cssom-view/#dom-htmlelement-offsetparent + * + * @param {Node} node Node from which to find offset parent. + * + * @return {?Node} Offset parent. + */ +export default function getOffsetParent( node ) { + // Cannot retrieve computed style or offset parent only anything other than + // an element node, so find the closest element node. + let closestElement; + while ( ( closestElement = node.parentNode ) ) { + if ( closestElement.nodeType === closestElement.ELEMENT_NODE ) { + break; + } + } + + if ( ! closestElement ) { + return null; + } + + // If the closest element is already positioned, return it, as offsetParent + // does not otherwise consider the node itself. + if ( getComputedStyle( closestElement ).position !== 'static' ) { + return closestElement; + } + + return closestElement.offsetParent; +} diff --git a/packages/dom/src/dom/get-range-height.js b/packages/dom/src/dom/get-range-height.js new file mode 100644 index 00000000000000..3a31155ae648d4 --- /dev/null +++ b/packages/dom/src/dom/get-range-height.js @@ -0,0 +1,18 @@ +/** + * Gets the height of the range without ignoring zero width rectangles, which + * some browsers ignore when creating a union. + * + * @param {Range} range The range to check. + */ +export default function getRangeHeight( range ) { + const rects = Array.from( range.getClientRects() ); + + if ( ! rects.length ) { + return; + } + + const highestTop = Math.min( ...rects.map( ( { top } ) => top ) ); + const lowestBottom = Math.max( ...rects.map( ( { bottom } ) => bottom ) ); + + return lowestBottom - highestTop; +} diff --git a/packages/dom/src/dom/get-rectangle-from-range.js b/packages/dom/src/dom/get-rectangle-from-range.js new file mode 100644 index 00000000000000..96fdd9b62ff4e5 --- /dev/null +++ b/packages/dom/src/dom/get-rectangle-from-range.js @@ -0,0 +1,48 @@ +/** + * Get the rectangle of a given Range. + * + * @param {Range} range The range. + * + * @return {DOMRect} The rectangle. + */ +export default function getRectangleFromRange( range ) { + // For uncollapsed ranges, get the rectangle that bounds the contents of the + // range; this a rectangle enclosing the union of the bounding rectangles + // for all the elements in the range. + if ( ! range.collapsed ) { + return range.getBoundingClientRect(); + } + + const { startContainer } = range; + const { ownerDocument } = startContainer; + + // Correct invalid "BR" ranges. The cannot contain any children. + if ( startContainer.nodeName === 'BR' ) { + const { parentNode } = startContainer; + const index = Array.from( parentNode.childNodes ).indexOf( + startContainer + ); + + range = ownerDocument.createRange(); + range.setStart( parentNode, index ); + range.setEnd( parentNode, index ); + } + + let rect = range.getClientRects()[ 0 ]; + + // If the collapsed range starts (and therefore ends) at an element node, + // `getClientRects` can be empty in some browsers. This can be resolved + // by adding a temporary text node with zero-width space to the range. + // + // See: https://stackoverflow.com/a/6847328/995445 + if ( ! rect ) { + const padNode = ownerDocument.createTextNode( '\u200b' ); + // Do not modify the live range. + range = range.cloneRange(); + range.insertNode( padNode ); + rect = range.getClientRects()[ 0 ]; + padNode.parentNode.removeChild( padNode ); + } + + return rect; +} diff --git a/packages/dom/src/dom/get-scroll-container.js b/packages/dom/src/dom/get-scroll-container.js new file mode 100644 index 00000000000000..995db7431db826 --- /dev/null +++ b/packages/dom/src/dom/get-scroll-container.js @@ -0,0 +1,29 @@ +/** + * Internal dependencies + */ +import getComputedStyle from './get-computed-style'; + +/** + * Given a DOM node, finds the closest scrollable container node. + * + * @param {Element} node Node from which to start. + * + * @return {?Element} Scrollable container node, if found. + */ +export default function getScrollContainer( node ) { + if ( ! node ) { + return; + } + + // Scrollable if scrollable height exceeds displayed... + if ( node.scrollHeight > node.clientHeight ) { + // ...except when overflow is defined to be hidden or visible + const { overflowY } = getComputedStyle( node ); + if ( /(auto|scroll)/.test( overflowY ) ) { + return node; + } + } + + // Continue traversing + return getScrollContainer( node.parentNode ); +} diff --git a/packages/dom/src/dom/hidden-caret-range-from-point.js b/packages/dom/src/dom/hidden-caret-range-from-point.js new file mode 100644 index 00000000000000..f772fdaafb5d7d --- /dev/null +++ b/packages/dom/src/dom/hidden-caret-range-from-point.js @@ -0,0 +1,38 @@ +/** + * Internal dependencies + */ +import caretRangeFromPoint from './caret-range-from-point'; +import getComputedStyle from './get-computed-style'; + +/** + * Get a collapsed range for a given point. + * Gives the container a temporary high z-index (above any UI). + * This is preferred over getting the UI nodes and set styles there. + * + * @param {Document} doc The document of the range. + * @param {number} x Horizontal position within the current viewport. + * @param {number} y Vertical position within the current viewport. + * @param {Element} container Container in which the range is expected to be found. + * + * @return {?Range} The best range for the given point. + */ +export default function hiddenCaretRangeFromPoint( doc, x, y, container ) { + const originalZIndex = container.style.zIndex; + const originalPosition = container.style.position; + + const { position = 'static' } = getComputedStyle( container ); + + // A z-index only works if the element position is not static. + if ( position === 'static' ) { + container.style.position = 'relative'; + } + + container.style.zIndex = '10000'; + + const range = caretRangeFromPoint( doc, x, y ); + + container.style.zIndex = originalZIndex; + container.style.position = originalPosition; + + return range; +} diff --git a/packages/dom/src/dom/index.js b/packages/dom/src/dom/index.js new file mode 100644 index 00000000000000..65431e55140245 --- /dev/null +++ b/packages/dom/src/dom/index.js @@ -0,0 +1,23 @@ +export { default as computeCaretRect } from './compute-caret-rect'; +export { default as documentHasTextSelection } from './document-has-text-selection'; +export { default as documentHasUncollapsedSelection } from './document-has-uncollapsed-selection'; +export { default as documentHasSelection } from './document-has-selection'; +export { default as getRectangleFromRange } from './get-rectangle-from-range'; +export { default as getScrollContainer } from './get-scroll-container'; +export { default as getOffsetParent } from './get-offset-parent'; +export { default as isEntirelySelected } from './is-entirely-selected'; +export { default as isHorizontalEdge } from './is-horizontal-edge'; +export { default as isNumberInput } from './is-number-input'; +export { default as isTextField } from './is-text-field'; +export { default as isVerticalEdge } from './is-vertical-edge'; +export { default as placeCaretAtHorizontalEdge } from './place-caret-at-horizontal-edge'; +export { default as placeCaretAtVerticalEdge } from './place-caret-at-vertical-edge'; +export { default as replace } from './replace'; +export { default as remove } from './remove'; +export { default as insertAfter } from './insert-after'; +export { default as unwrap } from './unwrap'; +export { default as replaceTag } from './replace-tag'; +export { default as wrap } from './wrap'; +export { default as __unstableStripHTML } from './strip-html'; +export { default as isEmpty } from './is-empty'; +export { default as removeInvalidHTML } from './remove-invalid-html'; diff --git a/packages/dom/src/dom/input-field-has-uncollapsed-selection.js b/packages/dom/src/dom/input-field-has-uncollapsed-selection.js new file mode 100644 index 00000000000000..41835fb1899d7e --- /dev/null +++ b/packages/dom/src/dom/input-field-has-uncollapsed-selection.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +import isTextField from './is-text-field'; +import isNumberInput from './is-number-input'; + +/** + * Check whether the given element, assumed an input field or textarea, + * contains a (uncollapsed) selection of text. + * + * Note: this is perhaps an abuse of the term "selection", since these elements + * manage selection differently and aren't covered by Selection#collapsed. + * + * See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects. + * + * @param {HTMLElement} element The HTML element. + * + * @return {boolean} Whether the input/textareaa element has some "selection". + */ +export default function inputFieldHasUncollapsedSelection( element ) { + if ( ! isTextField( element ) && ! isNumberInput( element ) ) { + return false; + } + try { + const { selectionStart, selectionEnd } = element; + + return selectionStart !== null && selectionStart !== selectionEnd; + } catch ( error ) { + // Safari throws an exception when trying to get `selectionStart` + // on non-text <input> elements (which, understandably, don't + // have the text selection API). We catch this via a try/catch + // block, as opposed to a more explicit check of the element's + // input types, because of Safari's non-standard behavior. This + // also means we don't have to worry about the list of input + // types that support `selectionStart` changing as the HTML spec + // evolves over time. + return false; + } +} diff --git a/packages/dom/src/dom/insert-after.js b/packages/dom/src/dom/insert-after.js new file mode 100644 index 00000000000000..c81cd0276c3f1a --- /dev/null +++ b/packages/dom/src/dom/insert-after.js @@ -0,0 +1,11 @@ +/** + * Given two DOM nodes, inserts the former in the DOM as the next sibling of + * the latter. + * + * @param {Element} newNode Node to be inserted. + * @param {Element} referenceNode Node after which to perform the insertion. + * @return {void} + */ +export default function insertAfter( newNode, referenceNode ) { + referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling ); +} diff --git a/packages/dom/src/dom/is-edge.js b/packages/dom/src/dom/is-edge.js new file mode 100644 index 00000000000000..f3780ee1343018 --- /dev/null +++ b/packages/dom/src/dom/is-edge.js @@ -0,0 +1,128 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * Internal dependencies + */ +import getComputedStyle from './get-computed-style'; +import getRangeHeight from './get-range-height'; +import getRectangleFromRange from './get-rectangle-from-range'; +import isSelectionForward from './is-selection-forward'; +import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point'; + +/** + * Check whether the selection is at the edge of the container. Checks for + * horizontal position by default. Set `onlyVertical` to true to check only + * vertically. + * + * @param {Element} container Focusable element. + * @param {boolean} isReverse Set to true to check left, false to check right. + * @param {boolean} onlyVertical Set to true to check only vertical position. + * + * @return {boolean} True if at the edge, false if not. + */ +export default function isEdge( container, isReverse, onlyVertical ) { + if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { + if ( container.selectionStart !== container.selectionEnd ) { + return false; + } + + if ( isReverse ) { + return container.selectionStart === 0; + } + + return container.value.length === container.selectionStart; + } + + if ( ! container.isContentEditable ) { + return true; + } + + const { ownerDocument } = container; + const { defaultView } = ownerDocument; + + const selection = defaultView.getSelection(); + + if ( ! selection.rangeCount ) { + return false; + } + + const range = selection.getRangeAt( 0 ); + const collapsedRange = range.cloneRange(); + const isForward = isSelectionForward( selection ); + const isCollapsed = selection.isCollapsed; + + // Collapse in direction of selection. + if ( ! isCollapsed ) { + collapsedRange.collapse( ! isForward ); + } + + const collapsedRangeRect = getRectangleFromRange( collapsedRange ); + const rangeRect = getRectangleFromRange( range ); + + if ( ! collapsedRangeRect || ! rangeRect ) { + return false; + } + + // Only consider the multiline selection at the edge if the direction is + // towards the edge. The selection is multiline if it is taller than the + // collapsed selection. + if ( + ! isCollapsed && + getRangeHeight( range ) > collapsedRangeRect.height && + isForward === isReverse + ) { + return false; + } + + // In the case of RTL scripts, the horizontal edge is at the opposite side. + const { direction } = getComputedStyle( container ); + const isReverseDir = direction === 'rtl' ? ! isReverse : isReverse; + + const containerRect = container.getBoundingClientRect(); + + // To check if a selection is at the edge, we insert a test selection at the + // edge of the container and check if the selections have the same vertical + // or horizontal position. If they do, the selection is at the edge. + // This method proves to be better than a DOM-based calculation for the + // horizontal edge, since it ignores empty textnodes and a trailing line + // break element. In other words, we need to check visual positioning, not + // DOM positioning. + // It also proves better than using the computed style for the vertical + // edge, because we cannot know the padding and line height reliably in + // pixels. `getComputedStyle` may return a value with different units. + const x = isReverseDir ? containerRect.left + 1 : containerRect.right - 1; + const y = isReverse ? containerRect.top + 1 : containerRect.bottom - 1; + const testRange = hiddenCaretRangeFromPoint( + ownerDocument, + x, + y, + container + ); + + if ( ! testRange ) { + return false; + } + + const testRect = getRectangleFromRange( testRange ); + + if ( ! testRect ) { + return false; + } + + const verticalSide = isReverse ? 'top' : 'bottom'; + const horizontalSide = isReverseDir ? 'left' : 'right'; + const verticalDiff = testRect[ verticalSide ] - rangeRect[ verticalSide ]; + const horizontalDiff = + testRect[ horizontalSide ] - collapsedRangeRect[ horizontalSide ]; + + // Allow the position to be 1px off. + const hasVerticalDiff = Math.abs( verticalDiff ) <= 1; + const hasHorizontalDiff = Math.abs( horizontalDiff ) <= 1; + + return onlyVertical + ? hasVerticalDiff + : hasVerticalDiff && hasHorizontalDiff; +} diff --git a/packages/dom/src/dom/is-empty.js b/packages/dom/src/dom/is-empty.js new file mode 100644 index 00000000000000..cffe10ec9ad0cd --- /dev/null +++ b/packages/dom/src/dom/is-empty.js @@ -0,0 +1,26 @@ +/** + * Recursively checks if an element is empty. An element is not empty if it + * contains text or contains elements with attributes such as images. + * + * @param {Element} element The element to check. + * + * @return {boolean} Whether or not the element is empty. + */ +export default function isEmpty( element ) { + switch ( element.nodeType ) { + case element.TEXT_NODE: + // We cannot use \s since it includes special spaces which we want + // to preserve. + return /^[ \f\n\r\t\v\u00a0]*$/.test( element.nodeValue ); + case element.ELEMENT_NODE: + if ( element.hasAttributes() ) { + return false; + } else if ( ! element.hasChildNodes() ) { + return true; + } + + return Array.from( element.childNodes ).every( isEmpty ); + default: + return true; + } +} diff --git a/packages/dom/src/dom/is-entirely-selected.js b/packages/dom/src/dom/is-entirely-selected.js new file mode 100644 index 00000000000000..05f6f656f5a522 --- /dev/null +++ b/packages/dom/src/dom/is-entirely-selected.js @@ -0,0 +1,58 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * Check whether the contents of the element have been entirely selected. + * Returns true if there is no possibility of selection. + * + * @param {Element} element The element to check. + * + * @return {boolean} True if entirely selected, false if not. + */ +export default function isEntirelySelected( element ) { + if ( includes( [ 'INPUT', 'TEXTAREA' ], element.nodeName ) ) { + return ( + element.selectionStart === 0 && + element.value.length === element.selectionEnd + ); + } + + if ( ! element.isContentEditable ) { + return true; + } + + const { ownerDocument } = element; + const { defaultView } = ownerDocument; + const selection = defaultView.getSelection(); + const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; + + if ( ! range ) { + return true; + } + + const { startContainer, endContainer, startOffset, endOffset } = range; + + if ( + startContainer === element && + endContainer === element && + startOffset === 0 && + endOffset === element.childNodes.length + ) { + return true; + } + + const lastChild = element.lastChild; + const lastChildContentLength = + lastChild.nodeType === lastChild.TEXT_NODE + ? lastChild.data.length + : lastChild.childNodes.length; + + return ( + startContainer === element.firstChild && + endContainer === element.lastChild && + startOffset === 0 && + endOffset === lastChildContentLength + ); +} diff --git a/packages/dom/src/dom/is-horizontal-edge.js b/packages/dom/src/dom/is-horizontal-edge.js new file mode 100644 index 00000000000000..a9b947d1b8643d --- /dev/null +++ b/packages/dom/src/dom/is-horizontal-edge.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import isEdge from './is-edge'; + +/** + * Check whether the selection is horizontally at the edge of the container. + * + * @param {Element} container Focusable element. + * @param {boolean} isReverse Set to true to check left, false for right. + * + * @return {boolean} True if at the horizontal edge, false if not. + */ +export default function isHorizontalEdge( container, isReverse ) { + return isEdge( container, isReverse ); +} diff --git a/packages/dom/src/dom/is-number-input.js b/packages/dom/src/dom/is-number-input.js new file mode 100644 index 00000000000000..2d11b16dc477fc --- /dev/null +++ b/packages/dom/src/dom/is-number-input.js @@ -0,0 +1,13 @@ +/** + * Check whether the given element is an input field of type number + * and has a valueAsNumber + * + * @param {HTMLElement} element The HTML element. + * + * @return {boolean} True if the element is input and holds a number. + */ +export default function isNumberInput( element ) { + const { nodeName, type, valueAsNumber } = element; + + return nodeName === 'INPUT' && type === 'number' && !! valueAsNumber; +} diff --git a/packages/dom/src/dom/is-selection-forward.js b/packages/dom/src/dom/is-selection-forward.js new file mode 100644 index 00000000000000..e8a7bceebebd64 --- /dev/null +++ b/packages/dom/src/dom/is-selection-forward.js @@ -0,0 +1,38 @@ +/** + * Returns true if the given selection object is in the forward direction, or + * false otherwise. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Node/compareDocumentPosition + * + * @param {Selection} selection Selection object to check. + * + * @return {boolean} Whether the selection is forward. + */ +export default function isSelectionForward( selection ) { + const { anchorNode, focusNode, anchorOffset, focusOffset } = selection; + + const position = anchorNode.compareDocumentPosition( focusNode ); + + // Disable reason: `Node#compareDocumentPosition` returns a bitmask value, + // so bitwise operators are intended. + /* eslint-disable no-bitwise */ + // Compare whether anchor node precedes focus node. If focus node (where + // end of selection occurs) is after the anchor node, it is forward. + if ( position & anchorNode.DOCUMENT_POSITION_PRECEDING ) { + return false; + } + + if ( position & anchorNode.DOCUMENT_POSITION_FOLLOWING ) { + return true; + } + /* eslint-enable no-bitwise */ + + // `compareDocumentPosition` returns 0 when passed the same node, in which + // case compare offsets. + if ( position === 0 ) { + return anchorOffset <= focusOffset; + } + + // This should never be reached, but return true as default case. + return true; +} diff --git a/packages/dom/src/dom/is-text-field.js b/packages/dom/src/dom/is-text-field.js new file mode 100644 index 00000000000000..ce683866f7ff60 --- /dev/null +++ b/packages/dom/src/dom/is-text-field.js @@ -0,0 +1,30 @@ +/** + * Check whether the given element is a text field, where text field is defined + * by the ability to select within the input, or that it is contenteditable. + * + * See: https://html.spec.whatwg.org/#textFieldSelection + * + * @param {HTMLElement} element The HTML element. + * + * @return {boolean} True if the element is an text field, false if not. + */ +export default function isTextField( element ) { + const { nodeName, contentEditable } = element; + const nonTextInputs = [ + 'button', + 'checkbox', + 'hidden', + 'file', + 'radio', + 'image', + 'range', + 'reset', + 'submit', + 'number', + ]; + return ( + ( nodeName === 'INPUT' && ! nonTextInputs.includes( element.type ) ) || + nodeName === 'TEXTAREA' || + contentEditable === 'true' + ); +} diff --git a/packages/dom/src/dom/is-vertical-edge.js b/packages/dom/src/dom/is-vertical-edge.js new file mode 100644 index 00000000000000..d1b74e4cb13755 --- /dev/null +++ b/packages/dom/src/dom/is-vertical-edge.js @@ -0,0 +1,16 @@ +/** + * Internal dependencies + */ +import isEdge from './is-edge'; + +/** + * Check whether the selection is vertically at the edge of the container. + * + * @param {Element} container Focusable element. + * @param {boolean} isReverse Set to true to check top, false for bottom. + * + * @return {boolean} True if at the vertical edge, false if not. + */ +export default function isVerticalEdge( container, isReverse ) { + return isEdge( container, isReverse, true ); +} diff --git a/packages/dom/src/dom/place-caret-at-horizontal-edge.js b/packages/dom/src/dom/place-caret-at-horizontal-edge.js new file mode 100644 index 00000000000000..3aa0d8fbe69107 --- /dev/null +++ b/packages/dom/src/dom/place-caret-at-horizontal-edge.js @@ -0,0 +1,61 @@ +/** + * External dependencies + */ +import { includes } from 'lodash'; + +/** + * Places the caret at start or end of a given element. + * + * @param {Element} container Focusable element. + * @param {boolean} isReverse True for end, false for start. + */ +export default function placeCaretAtHorizontalEdge( container, isReverse ) { + if ( ! container ) { + return; + } + + container.focus(); + + if ( includes( [ 'INPUT', 'TEXTAREA' ], container.tagName ) ) { + // The element may not support selection setting. + if ( typeof container.selectionStart !== 'number' ) { + return; + } + + if ( isReverse ) { + container.selectionStart = container.value.length; + container.selectionEnd = container.value.length; + } else { + container.selectionStart = 0; + container.selectionEnd = 0; + } + + return; + } + + if ( ! container.isContentEditable ) { + return; + } + + // Select on extent child of the container, not the container itself. This + // avoids the selection always being `endOffset` of 1 when placed at end, + // where `startContainer`, `endContainer` would always be container itself. + const rangeTarget = container[ isReverse ? 'lastChild' : 'firstChild' ]; + + // If no range target, it implies that the container is empty. Focusing is + // sufficient for caret to be placed correctly. + if ( ! rangeTarget ) { + return; + } + + const { ownerDocument } = container; + const { defaultView } = ownerDocument; + const selection = defaultView.getSelection(); + const range = ownerDocument.createRange(); + + range.selectNodeContents( rangeTarget ); + range.collapse( ! isReverse ); + + selection.removeAllRanges(); + selection.addRange( range ); +} diff --git a/packages/dom/src/dom/place-caret-at-vertical-edge.js b/packages/dom/src/dom/place-caret-at-vertical-edge.js new file mode 100644 index 00000000000000..aa8fbbb15fbf04 --- /dev/null +++ b/packages/dom/src/dom/place-caret-at-vertical-edge.js @@ -0,0 +1,73 @@ +/** + * Internal dependencies + */ +import placeCaretAtHorizontalEdge from './place-caret-at-horizontal-edge'; +import hiddenCaretRangeFromPoint from './hidden-caret-range-from-point'; + +/** + * Places the caret at the top or bottom of a given element. + * + * @param {Element} container Focusable element. + * @param {boolean} isReverse True for bottom, false for top. + * @param {DOMRect} [rect] The rectangle to position the caret with. + * @param {boolean} [mayUseScroll=true] True to allow scrolling, false to disallow. + */ +export default function placeCaretAtVerticalEdge( + container, + isReverse, + rect, + mayUseScroll = true +) { + if ( ! container ) { + return; + } + + if ( ! rect || ! container.isContentEditable ) { + placeCaretAtHorizontalEdge( container, isReverse ); + return; + } + + // Offset by a buffer half the height of the caret rect. This is needed + // because caretRangeFromPoint may default to the end of the selection if + // offset is too close to the edge. It's unclear how to precisely calculate + // this threshold; it may be the padded area of some combination of line + // height, caret height, and font size. The buffer offset is effectively + // equivalent to a point at half the height of a line of text. + const buffer = rect.height / 2; + const editableRect = container.getBoundingClientRect(); + const x = rect.left; + const y = isReverse + ? editableRect.bottom - buffer + : editableRect.top + buffer; + + const { ownerDocument } = container; + const { defaultView } = ownerDocument; + const range = hiddenCaretRangeFromPoint( ownerDocument, x, y, container ); + + if ( ! range || ! container.contains( range.startContainer ) ) { + if ( + mayUseScroll && + ( ! range || + ! range.startContainer || + ! range.startContainer.contains( container ) ) + ) { + // Might be out of view. + // Easier than attempting to calculate manually. + container.scrollIntoView( isReverse ); + placeCaretAtVerticalEdge( container, isReverse, rect, false ); + return; + } + + placeCaretAtHorizontalEdge( container, isReverse ); + return; + } + + const selection = defaultView.getSelection(); + selection.removeAllRanges(); + selection.addRange( range ); + container.focus(); + // Editable was already focussed, it goes back to old range... + // This fixes it. + selection.removeAllRanges(); + selection.addRange( range ); +} diff --git a/packages/dom/src/dom/remove-invalid-html.js b/packages/dom/src/dom/remove-invalid-html.js new file mode 100644 index 00000000000000..909902c9dc6f90 --- /dev/null +++ b/packages/dom/src/dom/remove-invalid-html.js @@ -0,0 +1,23 @@ +/** + * Internal dependencies + */ +import cleanNodeList from './clean-node-list'; + +/** + * Given a schema, unwraps or removes nodes, attributes and classes on HTML. + * + * @param {string} HTML The HTML to clean up. + * @param {Object} schema Schema for the HTML. + * @param {Object} inline Whether to clean for inline mode. + * + * @return {string} The cleaned up HTML. + */ +export default function removeInvalidHTML( HTML, schema, inline ) { + const doc = document.implementation.createHTMLDocument( '' ); + + doc.body.innerHTML = HTML; + + cleanNodeList( doc.body.childNodes, doc, schema, inline ); + + return doc.body.innerHTML; +} diff --git a/packages/dom/src/dom/remove.js b/packages/dom/src/dom/remove.js new file mode 100644 index 00000000000000..2e22108a08045e --- /dev/null +++ b/packages/dom/src/dom/remove.js @@ -0,0 +1,9 @@ +/** + * Given a DOM node, removes it from the DOM. + * + * @param {Element} node Node to be removed. + * @return {void} + */ +export default function remove( node ) { + node.parentNode.removeChild( node ); +} diff --git a/packages/dom/src/dom/replace-tag.js b/packages/dom/src/dom/replace-tag.js new file mode 100644 index 00000000000000..dc70cf922c64a6 --- /dev/null +++ b/packages/dom/src/dom/replace-tag.js @@ -0,0 +1,19 @@ +/** + * Replaces the given node with a new node with the given tag name. + * + * @param {Element} node The node to replace + * @param {string} tagName The new tag name. + * + * @return {Element} The new node. + */ +export default function replaceTag( node, tagName ) { + const newNode = node.ownerDocument.createElement( tagName ); + + while ( node.firstChild ) { + newNode.appendChild( node.firstChild ); + } + + node.parentNode.replaceChild( newNode, node ); + + return newNode; +} diff --git a/packages/dom/src/dom/replace.js b/packages/dom/src/dom/replace.js new file mode 100644 index 00000000000000..ab9b05df3a08a8 --- /dev/null +++ b/packages/dom/src/dom/replace.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import insertAfter from './insert-after'; +import remove from './remove'; + +/** + * Given two DOM nodes, replaces the former with the latter in the DOM. + * + * @param {Element} processedNode Node to be removed. + * @param {Element} newNode Node to be inserted in its place. + * @return {void} + */ +export default function replace( processedNode, newNode ) { + insertAfter( newNode, processedNode.parentNode ); + remove( processedNode ); +} diff --git a/packages/dom/src/dom/strip-html.js b/packages/dom/src/dom/strip-html.js new file mode 100644 index 00000000000000..efd5e2d8784cc2 --- /dev/null +++ b/packages/dom/src/dom/strip-html.js @@ -0,0 +1,14 @@ +/** + * Removes any HTML tags from the provided string. + * + * @param {string} html The string containing html. + * + * @return {string} The text content with any html removed. + */ +export default function stripHTML( html ) { + const document = new window.DOMParser().parseFromString( + html, + 'text/html' + ); + return document.body.textContent || ''; +} diff --git a/packages/dom/src/dom/unwrap.js b/packages/dom/src/dom/unwrap.js new file mode 100644 index 00000000000000..6a93a40625477c --- /dev/null +++ b/packages/dom/src/dom/unwrap.js @@ -0,0 +1,16 @@ +/** + * Unwrap the given node. This means any child nodes are moved to the parent. + * + * @param {Node} node The node to unwrap. + * + * @return {void} + */ +export default function unwrap( node ) { + const parent = node.parentNode; + + while ( node.firstChild ) { + parent.insertBefore( node.firstChild, node ); + } + + parent.removeChild( node ); +} diff --git a/packages/dom/src/dom/wrap.js b/packages/dom/src/dom/wrap.js new file mode 100644 index 00000000000000..725a71f6c4b928 --- /dev/null +++ b/packages/dom/src/dom/wrap.js @@ -0,0 +1,10 @@ +/** + * Wraps the given node with a new node with the given tag name. + * + * @param {Element} newNode The node to insert. + * @param {Element} referenceNode The node to wrap. + */ +export default function wrap( newNode, referenceNode ) { + referenceNode.parentNode.insertBefore( newNode, referenceNode ); + newNode.appendChild( referenceNode ); +} diff --git a/packages/dom/src/focusable.js b/packages/dom/src/focusable.js index 35b1c9ac637bf5..80e9dcb4968318 100644 --- a/packages/dom/src/focusable.js +++ b/packages/dom/src/focusable.js @@ -35,7 +35,7 @@ const SELECTOR = [ * Returns true if the specified element is visible (i.e. neither display: none * nor visibility: hidden). * - * @param {Element} element DOM element to test. + * @param {HTMLElement} element DOM element to test. * * @return {boolean} Whether element is visible. */ @@ -67,16 +67,18 @@ function skipFocus( element ) { * false otherwise. Area is only focusable if within a map where a named map * referenced by an image somewhere in the document. * - * @param {Element} element DOM area element to test. + * @param {HTMLAreaElement} element DOM area element to test. * * @return {boolean} Whether area element is valid for focus. */ function isValidFocusableArea( element ) { + /** @type {HTMLMapElement | null} */ const map = element.closest( 'map[name]' ); if ( ! map ) { return false; } + /** @type {HTMLImageElement | null} */ const img = element.ownerDocument.querySelector( 'img[usemap="#' + map.name + '"]' ); @@ -91,6 +93,9 @@ function isValidFocusableArea( element ) { * @return {Element[]} Focusable elements. */ export function find( context ) { + /* eslint-disable jsdoc/no-undefined-types */ + /** @type {NodeListOf<HTMLElement>} */ + /* eslint-enable jsdoc/no-undefined-types */ const elements = context.querySelectorAll( SELECTOR ); return Array.from( elements ).filter( ( element ) => { @@ -100,7 +105,9 @@ export function find( context ) { const { nodeName } = element; if ( 'AREA' === nodeName ) { - return isValidFocusableArea( element ); + return isValidFocusableArea( + /** @type {HTMLAreaElement} */ ( element ) + ); } return true; diff --git a/packages/dom/src/tabbable.js b/packages/dom/src/tabbable.js index d5300794adfedd..80a7838b88a6bf 100644 --- a/packages/dom/src/tabbable.js +++ b/packages/dom/src/tabbable.js @@ -18,7 +18,7 @@ import { find as findFocusable } from './focusable'; * * @param {Element} element Element from which to retrieve. * - * @return {?number} Tab index of element (default 0). + * @return {number} Tab index of element (default 0). */ function getTabIndex( element ) { const tabIndex = element.getAttribute( 'tabindex' ); @@ -36,18 +36,24 @@ export function isTabbableIndex( element ) { return getTabIndex( element ) !== -1; } +/** @typedef {Element & { type?: string, checked?: boolean, name?: string }} MaybeHTMLInputElement */ + /** * Returns a stateful reducer function which constructs a filtered array of * tabbable elements, where at most one radio input is selected for a given * name, giving priority to checked input, falling back to the first * encountered. * - * @return {Function} Radio group collapse reducer. + * @return {(acc: MaybeHTMLInputElement[], el: MaybeHTMLInputElement) => MaybeHTMLInputElement[]} Radio group collapse reducer. */ function createStatefulCollapseRadioGroup() { + /** @type {Record<string, MaybeHTMLInputElement>} */ const CHOSEN_RADIO_BY_NAME = {}; - return function collapseRadioGroup( result, element ) { + return function collapseRadioGroup( + /** @type {MaybeHTMLInputElement[]} */ result, + /** @type {MaybeHTMLInputElement} */ element + ) { const { nodeName, type, checked, name } = element; // For all non-radio tabbables, construct to array by concatenating. @@ -86,7 +92,7 @@ function createStatefulCollapseRadioGroup() { * @param {Element} element Element. * @param {number} index Array index of element. * - * @return {Object} Mapped object with element, index. + * @return {{ element: Element, index: number }} Mapped object with element, index. */ function mapElementToObjectTabbable( element, index ) { return { element, index }; @@ -96,7 +102,7 @@ function mapElementToObjectTabbable( element, index ) { * An array map callback, returning an element of the given mapped object's * element value. * - * @param {Object} object Mapped object with index. + * @param {{ element: Element }} object Mapped object with element. * * @return {Element} Mapped object element. */ @@ -109,8 +115,8 @@ function mapObjectTabbableToElement( object ) { * * @see mapElementToObjectTabbable * - * @param {Object} a First object to compare. - * @param {Object} b Second object to compare. + * @param {{ element: Element, index: number }} a First object to compare. + * @param {{ element: Element, index: number }} b Second object to compare. * * @return {number} Comparator result. */ @@ -128,9 +134,9 @@ function compareObjectTabbables( a, b ) { /** * Givin focusable elements, filters out tabbable element. * - * @param {Array} focusables Focusable elements to filter. + * @param {Element[]} focusables Focusable elements to filter. * - * @return {Array} Tabbable elements. + * @return {Element[]} Tabbable elements. */ function filterTabbable( focusables ) { return focusables @@ -141,6 +147,10 @@ function filterTabbable( focusables ) { .reduce( createStatefulCollapseRadioGroup(), [] ); } +/** + * @param {Element} context + * @return {Element[]} Tabbable elements within the context. + */ export function find( context ) { return filterTabbable( findFocusable( context ) ); } diff --git a/packages/dom/tsconfig.json b/packages/dom/tsconfig.json index 8821a076a20e5b..3b280ed7f99446 100644 --- a/packages/dom/tsconfig.json +++ b/packages/dom/tsconfig.json @@ -4,5 +4,9 @@ "rootDir": "src", "declarationDir": "build-types" }, - "include": [ "src/data-transfer.js" ] -} \ No newline at end of file + "include": [ + "src/data-transfer.js", + "src/focusable.js", + "src/tabbable.js", + ] +} diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index ca5b65c5c8f5de..89e25d997e5c99 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -29,7 +29,7 @@ "main": "build/index.js", "module": "build-module/index.js", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/keycodes": "file:../keycodes", "@wordpress/url": "file:../url", "lodash": "^4.17.19", diff --git a/packages/e2e-test-utils/src/click-block-toolbar-button.js b/packages/e2e-test-utils/src/click-block-toolbar-button.js index 3aa431bfe29ff9..e73888f0681385 100644 --- a/packages/e2e-test-utils/src/click-block-toolbar-button.js +++ b/packages/e2e-test-utils/src/click-block-toolbar-button.js @@ -22,7 +22,7 @@ export async function clickBlockToolbarButton( label, type = 'ariaLabel' ) { if ( type === 'content' ) { button = await page.waitForXPath( - `//*[@class='${ BLOCK_TOOLBAR_SELECTOR }']//button[contains(text(), '${ label }')]` + `//*[contains(concat(' ', normalize-space(@class), ' '), ' ${ BLOCK_TOOLBAR_SELECTOR } ')]//button[contains(text(), '${ label }')]` ); } diff --git a/packages/e2e-tests/fixtures/blocks/core__group.html b/packages/e2e-tests/fixtures/blocks/core__group.html index e5df0f2fce926a..df7eef39e38981 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.html +++ b/packages/e2e-tests/fixtures/blocks/core__group.html @@ -1,12 +1,9 @@ <!-- wp:group {"align":"full","backgroundColor":"secondary"} --> -<div class="wp-block-group alignfull has-secondary-background-color has-background"> - <div class="wp-block-group__inner-container"> - <!-- wp:paragraph --> - <p>This is a group block.</p> - <!-- /wp:paragraph --> +<div class="wp-block-group alignfull has-secondary-background-color has-background"><!-- wp:paragraph --> +<p>This is a group block.</p> +<!-- /wp:paragraph --> - <!-- wp:paragraph --> - <p>Group block content.</p> - <!-- /wp:paragraph --></div> - </div> +<!-- wp:paragraph --> +<p>Group block content.</p> +<!-- /wp:paragraph --></div> <!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__group.json b/packages/e2e-tests/fixtures/blocks/core__group.json index 526a8d70fbcdfa..7fd0b5d60ba113 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.json +++ b/packages/e2e-tests/fixtures/blocks/core__group.json @@ -32,6 +32,6 @@ "originalContent": "<p>Group block content.</p>" } ], - "originalContent": "<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t\n\n\t\t</div>\n\t</div>" + "originalContent": "<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\n</div>" } ] diff --git a/packages/e2e-tests/fixtures/blocks/core__group.parsed.json b/packages/e2e-tests/fixtures/blocks/core__group.parsed.json index ba86ac5a02d6bd..a82a138ba47415 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.parsed.json +++ b/packages/e2e-tests/fixtures/blocks/core__group.parsed.json @@ -10,28 +10,28 @@ "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t<p>This is a group block.</p>\n\t\t", + "innerHTML": "\n<p>This is a group block.</p>\n", "innerContent": [ - "\n\t\t<p>This is a group block.</p>\n\t\t" + "\n<p>This is a group block.</p>\n" ] }, { "blockName": "core/paragraph", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\t\t<p>Group block content.</p>\n\t\t", + "innerHTML": "\n<p>Group block content.</p>\n", "innerContent": [ - "\n\t\t<p>Group block content.</p>\n\t\t" + "\n<p>Group block content.</p>\n" ] } ], - "innerHTML": "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t\n\n\t\t</div>\n\t</div>\n", + "innerHTML": "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\n</div>\n", "innerContent": [ - "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t", + "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">", null, - "\n\n\t\t", + "\n\n", null, - "</div>\n\t</div>\n" + "</div>\n" ] }, { diff --git a/packages/e2e-tests/fixtures/blocks/core__group.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group.serialized.html index 8ac236255f9f61..df7eef39e38981 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group.serialized.html +++ b/packages/e2e-tests/fixtures/blocks/core__group.serialized.html @@ -1,9 +1,9 @@ <!-- wp:group {"align":"full","backgroundColor":"secondary"} --> -<div class="wp-block-group alignfull has-secondary-background-color has-background"><div class="wp-block-group__inner-container"><!-- wp:paragraph --> +<div class="wp-block-group alignfull has-secondary-background-color has-background"><!-- wp:paragraph --> <p>This is a group block.</p> <!-- /wp:paragraph --> <!-- wp:paragraph --> <p>Group block content.</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-2.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-2.serialized.html index cf7ebe4f5a05ec..1e25d657b8106f 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-2.serialized.html +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-2.serialized.html @@ -1,5 +1,5 @@ <!-- wp:group {"textColor":"accent"} --> -<div class="wp-block-group has-accent-color has-text-color"><div class="wp-block-group__inner-container"><!-- wp:paragraph --> +<div class="wp-block-group has-accent-color has-text-color"><!-- wp:paragraph --> <p>My paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.html new file mode 100644 index 00000000000000..9702e29ed52d6c --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.html @@ -0,0 +1,13 @@ +<!-- wp:group {"align":"full","backgroundColor":"secondary"} --> +<div class="wp-block-group alignfull has-secondary-background-color has-background"> + <div class="wp-block-group__inner-container"> + <!-- wp:paragraph --> + <p>This is a group block.</p> + <!-- /wp:paragraph --> + + <!-- wp:paragraph --> + <p>Group block content.</p> + <!-- /wp:paragraph --> + </div> +</div> +<!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.json b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.json new file mode 100644 index 00000000000000..2ca0de07bbb39a --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.json @@ -0,0 +1,37 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/group", + "isValid": true, + "attributes": { + "tagName": "div", + "align": "full", + "backgroundColor": "secondary" + }, + "innerBlocks": [ + { + "clientId": "_clientId_0", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": "This is a group block.", + "dropCap": false + }, + "innerBlocks": [], + "originalContent": "<p>This is a group block.</p>" + }, + { + "clientId": "_clientId_1", + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": "Group block content.", + "dropCap": false + }, + "innerBlocks": [], + "originalContent": "<p>Group block content.</p>" + } + ], + "originalContent": "<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t\n\n\t\t\n\t</div>\n</div>" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.parsed.json b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.parsed.json new file mode 100644 index 00000000000000..9da531aefe611e --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.parsed.json @@ -0,0 +1,46 @@ +[ + { + "blockName": "core/group", + "attrs": { + "align": "full", + "backgroundColor": "secondary" + }, + "innerBlocks": [ + { + "blockName": "core/paragraph", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\t\t<p>This is a group block.</p>\n\t\t", + "innerContent": [ + "\n\t\t<p>This is a group block.</p>\n\t\t" + ] + }, + { + "blockName": "core/paragraph", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n\t\t<p>Group block content.</p>\n\t\t", + "innerContent": [ + "\n\t\t<p>Group block content.</p>\n\t\t" + ] + } + ], + "innerHTML": "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t\n\n\t\t\n\t</div>\n</div>\n", + "innerContent": [ + "\n<div class=\"wp-block-group alignfull has-secondary-background-color has-background\">\n\t<div class=\"wp-block-group__inner-container\">\n\t\t", + null, + "\n\n\t\t", + null, + "\n\t</div>\n</div>\n" + ] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.serialized.html new file mode 100644 index 00000000000000..df7eef39e38981 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated-inner-container.serialized.html @@ -0,0 +1,9 @@ +<!-- wp:group {"align":"full","backgroundColor":"secondary"} --> +<div class="wp-block-group alignfull has-secondary-background-color has-background"><!-- wp:paragraph --> +<p>This is a group block.</p> +<!-- /wp:paragraph --> + +<!-- wp:paragraph --> +<p>Group block content.</p> +<!-- /wp:paragraph --></div> +<!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.json b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.json index 7f1f51c3644e40..c67c67fc51cd8e 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.json +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.json @@ -4,10 +4,10 @@ "name": "core/group", "isValid": true, "attributes": { - "backgroundColor": "lighter-blue", + "tagName": "div", "align": "full", "anchor": "test-id", - "tagName": "div" + "backgroundColor": "lighter-blue" }, "innerBlocks": [ { diff --git a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html index b9f1dc3ba37e12..b36427bb867648 100644 --- a/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html +++ b/packages/e2e-tests/fixtures/blocks/core__group__deprecated.serialized.html @@ -1,5 +1,5 @@ <!-- wp:group {"align":"full","backgroundColor":"lighter-blue"} --> -<div class="wp-block-group alignfull has-lighter-blue-background-color has-background" id="test-id"><div class="wp-block-group__inner-container"><!-- wp:paragraph --> +<div class="wp-block-group alignfull has-lighter-blue-background-color has-background" id="test-id"><!-- wp:paragraph --> <p>test</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group --> diff --git a/packages/e2e-tests/fixtures/blocks/core__loginout.html b/packages/e2e-tests/fixtures/blocks/core__loginout.html new file mode 100644 index 00000000000000..dab3964df07c55 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__loginout.html @@ -0,0 +1 @@ +<!-- wp:loginout /--> diff --git a/packages/e2e-tests/fixtures/blocks/core__loginout.json b/packages/e2e-tests/fixtures/blocks/core__loginout.json new file mode 100644 index 00000000000000..67eaab753bff39 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__loginout.json @@ -0,0 +1,13 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/loginout", + "isValid": true, + "attributes": { + "displayLoginAsForm": false, + "redirectToCurrent": true + }, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__loginout.parsed.json b/packages/e2e-tests/fixtures/blocks/core__loginout.parsed.json new file mode 100644 index 00000000000000..96c310c7e14561 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__loginout.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/loginout", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__loginout.serialized.html b/packages/e2e-tests/fixtures/blocks/core__loginout.serialized.html new file mode 100644 index 00000000000000..dab3964df07c55 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__loginout.serialized.html @@ -0,0 +1 @@ +<!-- wp:loginout /--> diff --git a/packages/e2e-tests/fixtures/blocks/core__query-title.html b/packages/e2e-tests/fixtures/blocks/core__query-title.html new file mode 100644 index 00000000000000..2fa05648f259fd --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__query-title.html @@ -0,0 +1 @@ +<!-- wp:query-title /--> diff --git a/packages/e2e-tests/fixtures/blocks/core__query-title.json b/packages/e2e-tests/fixtures/blocks/core__query-title.json new file mode 100644 index 00000000000000..082dfaeb845e5f --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__query-title.json @@ -0,0 +1,12 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/query-title", + "isValid": true, + "attributes": { + "level": 1 + }, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__query-title.parsed.json b/packages/e2e-tests/fixtures/blocks/core__query-title.parsed.json new file mode 100644 index 00000000000000..8cea12344830a9 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__query-title.parsed.json @@ -0,0 +1,18 @@ +[ + { + "blockName": "core/query-title", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__query-title.serialized.html b/packages/e2e-tests/fixtures/blocks/core__query-title.serialized.html new file mode 100644 index 00000000000000..2fa05648f259fd --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__query-title.serialized.html @@ -0,0 +1 @@ +<!-- wp:query-title /--> diff --git a/packages/e2e-tests/fixtures/blocks/core__term-description.html b/packages/e2e-tests/fixtures/blocks/core__term-description.html new file mode 100644 index 00000000000000..63bb38e7fbe3b4 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__term-description.html @@ -0,0 +1 @@ +<!-- wp:term-description {"align":"full"} /--> diff --git a/packages/e2e-tests/fixtures/blocks/core__term-description.json b/packages/e2e-tests/fixtures/blocks/core__term-description.json new file mode 100644 index 00000000000000..4148e19d752424 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__term-description.json @@ -0,0 +1,12 @@ +[ + { + "clientId": "_clientId_0", + "name": "core/term-description", + "isValid": true, + "attributes": { + "align": "full" + }, + "innerBlocks": [], + "originalContent": "" + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__term-description.parsed.json b/packages/e2e-tests/fixtures/blocks/core__term-description.parsed.json new file mode 100644 index 00000000000000..30e6c5778a52d4 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__term-description.parsed.json @@ -0,0 +1,20 @@ +[ + { + "blockName": "core/term-description", + "attrs": { + "align": "full" + }, + "innerBlocks": [], + "innerHTML": "", + "innerContent": [] + }, + { + "blockName": null, + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n", + "innerContent": [ + "\n" + ] + } +] diff --git a/packages/e2e-tests/fixtures/blocks/core__term-description.serialized.html b/packages/e2e-tests/fixtures/blocks/core__term-description.serialized.html new file mode 100644 index 00000000000000..63bb38e7fbe3b4 --- /dev/null +++ b/packages/e2e-tests/fixtures/blocks/core__term-description.serialized.html @@ -0,0 +1 @@ +<!-- wp:term-description {"align":"full"} /--> diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index e7f28f8137126a..b6a1878c624eea 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "2.1.1", + "version": "2.1.0", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/plugins/disable-animations.php b/packages/e2e-tests/plugins/disable-animations.php index 35e3a9c60c8889..811ab8ec670cf2 100644 --- a/packages/e2e-tests/plugins/disable-animations.php +++ b/packages/e2e-tests/plugins/disable-animations.php @@ -11,7 +11,7 @@ * Enqueue CSS stylesheet disabling animations. */ function enqueue_disable_animations_stylesheet() { - $custom_css = '* { animation-duration: 0ms !important; transition-duration: 0s !important; }'; + $custom_css = '* { animation-duration: 0ms !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }'; wp_add_inline_style( 'wp-components', $custom_css ); } diff --git a/packages/e2e-tests/plugins/inner-blocks-locking-all-embed.php b/packages/e2e-tests/plugins/inner-blocks-locking-all-embed.php index c9489d68f357d7..39ae15d6d25072 100644 --- a/packages/e2e-tests/plugins/inner-blocks-locking-all-embed.php +++ b/packages/e2e-tests/plugins/inner-blocks-locking-all-embed.php @@ -10,7 +10,7 @@ /** * Registers a custom script for the plugin. */ -function enqueue_container_without_paragraph_plugin_script() { +function enqueue_inner_blocks_locking_all_embed_plugin_script() { wp_enqueue_script( 'gutenberg-test-inner-blocks-locking-all-embed', plugins_url( 'inner-blocks-locking-all-embed/index.js', __FILE__ ), @@ -25,4 +25,4 @@ function enqueue_container_without_paragraph_plugin_script() { ); } -add_action( 'init', 'enqueue_container_without_paragraph_plugin_script' ); +add_action( 'init', 'enqueue_inner_blocks_locking_all_embed_plugin_script' ); diff --git a/packages/e2e-tests/plugins/inner-blocks-templates.php b/packages/e2e-tests/plugins/inner-blocks-templates.php index 212992e3619623..6762ab59f7cf74 100644 --- a/packages/e2e-tests/plugins/inner-blocks-templates.php +++ b/packages/e2e-tests/plugins/inner-blocks-templates.php @@ -10,7 +10,7 @@ /** * Registers a custom script for the plugin. */ -function enqueue_container_without_paragraph_plugin_script() { +function enqueue_inner_blocks_templates_plugin_script() { wp_enqueue_script( 'gutenberg-test-inner-blocks-templates', plugins_url( 'inner-blocks-templates/index.js', __FILE__ ), @@ -27,4 +27,4 @@ function enqueue_container_without_paragraph_plugin_script() { ); } -add_action( 'init', 'enqueue_container_without_paragraph_plugin_script' ); +add_action( 'init', 'enqueue_inner_blocks_templates_plugin_script' ); diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/group.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/group.test.js.snap index 49ddf4c41946de..df7b3f6419f155 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/group.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/group.test.js.snap @@ -2,20 +2,20 @@ exports[`Group can be created using the block inserter 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"></div></div> +<div class=\\"wp-block-group\\"></div> <!-- /wp:group -->" `; exports[`Group can be created using the slash inserter 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"></div></div> +<div class=\\"wp-block-group\\"></div> <!-- /wp:group -->" `; exports[`Group can have other blocks appended to it using the button appender 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph --> <p>Group Block with a Paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; diff --git a/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap b/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap index 18b856a57a4303..feefe814ca13da 100644 --- a/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap +++ b/packages/e2e-tests/specs/editor/blocks/__snapshots__/image.test.js.snap @@ -17,3 +17,9 @@ exports[`Image should drag and drop files into media placeholder 1`] = ` <figure class=\\"wp-block-image\\"><img alt=\\"\\"/></figure> <!-- /wp:image -->" `; + +exports[`Image should undo without broken temporary state 1`] = ` +"<!-- wp:image --> +<figure class=\\"wp-block-image\\"><img alt=\\"\\"/></figure> +<!-- /wp:image -->" +`; diff --git a/packages/e2e-tests/specs/editor/blocks/image.test.js b/packages/e2e-tests/specs/editor/blocks/image.test.js index b254413a141df9..1acf1248f7182c 100644 --- a/packages/e2e-tests/specs/editor/blocks/image.test.js +++ b/packages/e2e-tests/specs/editor/blocks/image.test.js @@ -137,6 +137,24 @@ describe( 'Image', () => { ).toBe( '1<br data-rich-text-line-break="true">2' ); } ); + it( 'should have keyboard navigable toolbar for caption', async () => { + await insertBlock( 'Image' ); + const fileName = await upload( '.wp-block-image input[type="file"]' ); + await waitForImage( fileName ); + // Navigate to More, Link, Italic and finally Bold. + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await page.keyboard.press( 'Space' ); + await page.keyboard.press( 'a' ); + await page.keyboard.press( 'ArrowRight' ); + + expect( + await page.evaluate( () => document.activeElement.innerHTML ) + ).toBe( '<strong>a</strong>' ); + } ); + it( 'should drag and drop files into media placeholder', async () => { await page.keyboard.press( 'Enter' ); await insertBlock( 'Image' ); @@ -288,4 +306,59 @@ describe( 'Image', () => { expect( initialImageDataURL ).not.toEqual( updatedImageDataURL ); expect( updatedImageDataURL ).toMatchSnapshot(); } ); + + it( 'Should reset dimensions on change URL', async () => { + const imageUrl = '/wp-includes/images/w-logo-blue.png'; + + await insertBlock( 'Image' ); + + // Upload an initial image. + const filename = await upload( '.wp-block-image input[type="file"]' ); + + // Resize the Uploaded Image. + await openDocumentSettingsSidebar(); + await page.click( + '[aria-label="Image size presets"] button:first-child' + ); + + const regexBefore = new RegExp( + `<!-- wp:image {"id":\\d+,"width":3,"height":3,"sizeSlug":"large","linkDestination":"none"} -->\\s*<figure class="wp-block-image size-large is-resized"><img src="[^"]+\\/${ filename }\\.png" alt="" class="wp-image-\\d+" width="3" height="3"\\/><\\/figure>\\s*<!-- /wp:image -->` + ); + + // Check if dimensions are changed. + expect( await getEditedPostContent() ).toMatch( regexBefore ); + + // Replace uploaded image with an URL. + await clickButton( 'Replace' ); + await clickButton( 'Edit' ); + + await page.waitForSelector( '.block-editor-url-input__input' ); + await page.evaluate( + () => + ( document.querySelector( + '.block-editor-url-input__input' + ).value = '' ) + ); + + await page.focus( '.block-editor-url-input__input' ); + await page.keyboard.type( imageUrl ); + await page.click( '.block-editor-link-control__search-submit' ); + + const regexAfter = new RegExp( + `<!-- wp:image {"sizeSlug":"large","linkDestination":"none"} -->\\s*<figure class="wp-block-image size-large"><img src="${ imageUrl }" alt=""/></figure>\\s*<!-- /wp:image -->` + ); + + // Check if dimensions are reset. + expect( await getEditedPostContent() ).toMatch( regexAfter ); + } ); + + it( 'should undo without broken temporary state', async () => { + await insertBlock( 'Image' ); + const fileName = await upload( '.wp-block-image input[type="file"]' ); + await waitForImage( fileName ); + await pressKeyWithModifier( 'primary', 'z' ); + // Expect an empty image block (placeholder) rather than one with a + // broken temporary URL. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); diff --git a/packages/e2e-tests/specs/editor/blocks/paragraph.test.js b/packages/e2e-tests/specs/editor/blocks/paragraph.test.js index d998ae32387c39..74f3f7f8cb98ff 100644 --- a/packages/e2e-tests/specs/editor/blocks/paragraph.test.js +++ b/packages/e2e-tests/specs/editor/blocks/paragraph.test.js @@ -13,8 +13,9 @@ describe( 'Paragraph', () => { await page.keyboard.type( '1' ); const firstBlockTagName = await page.evaluate( () => { - return document.querySelector( '.block-editor-block-list__layout' ) - .firstChild.tagName; + return document.querySelector( + '.block-editor-block-list__layout .wp-block' + ).tagName; } ); // The outer element should be a paragraph. Blocks should never have any diff --git a/packages/e2e-tests/specs/editor/blocks/quote.test.js b/packages/e2e-tests/specs/editor/blocks/quote.test.js index e185c9e3c7cd70..da4efefb38de13 100644 --- a/packages/e2e-tests/specs/editor/blocks/quote.test.js +++ b/packages/e2e-tests/specs/editor/blocks/quote.test.js @@ -87,7 +87,7 @@ describe( 'Quote', () => { await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); await transformBlockTo( 'Paragraph' ); @@ -96,7 +96,7 @@ describe( 'Quote', () => { it( 'and renders only one paragraph for the cite, if the quote is void', async () => { await insertBlock( 'Quote' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); await transformBlockTo( 'Paragraph' ); @@ -146,7 +146,7 @@ describe( 'Quote', () => { it( 'is transformed to a heading and a quote if the quote contains a citation', async () => { await insertBlock( 'Quote' ); await page.keyboard.type( 'one' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); await transformBlockTo( 'Heading' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -157,7 +157,7 @@ describe( 'Quote', () => { await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); await transformBlockTo( 'Heading' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -174,7 +174,7 @@ describe( 'Quote', () => { await page.keyboard.type( 'one' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( 'two' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'cite' ); await transformBlockTo( 'Pullquote' ); expect( await getEditedPostContent() ).toMatchSnapshot(); @@ -214,7 +214,7 @@ describe( 'Quote', () => { await page.keyboard.type( '1' ); await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'c' ); await page.keyboard.press( 'ArrowUp' ); await page.keyboard.press( 'ArrowUp' ); @@ -237,6 +237,7 @@ describe( 'Quote', () => { await page.keyboard.press( 'ArrowLeft' ); await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'ArrowDown' ); await page.keyboard.press( 'Backspace' ); expect( await getEditedPostContent() ).toMatchSnapshot(); diff --git a/packages/e2e-tests/specs/editor/blocks/table.test.js b/packages/e2e-tests/specs/editor/blocks/table.test.js index 1b8273159b2ba7..f3888d70f062ce 100644 --- a/packages/e2e-tests/specs/editor/blocks/table.test.js +++ b/packages/e2e-tests/specs/editor/blocks/table.test.js @@ -77,16 +77,16 @@ describe( 'Table', () => { await page.click( 'td' ); await page.keyboard.type( 'This' ); - // Tab to the next cell and add some text. - await page.keyboard.press( 'Tab' ); + // Navigate to the next cell and add some text. + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'is' ); - // Tab to the next cell and add some text. - await page.keyboard.press( 'Tab' ); + // Navigate to the next cell and add some text. + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'table' ); - // Tab to the next cell and add some text. - await page.keyboard.press( 'Tab' ); + // Navigate to the next cell and add some text. + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.type( 'block' ); // Expect the post to have the correct written content inside the table. diff --git a/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap b/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap index bec98aa0820550..a7d34618db0de4 100644 --- a/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap +++ b/packages/e2e-tests/specs/editor/plugins/__snapshots__/cpt-locking.test.js.snap @@ -38,21 +38,21 @@ exports[`cpt locking template_lock all should not error when deleting the cotent exports[`cpt locking template_lock all unlocked group should allow blocks to be moved 1`] = ` "<!-- wp:group {\\"templateLock\\":false} --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph {\\"placeholder\\":\\"Add a description\\"} --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph {\\"placeholder\\":\\"Add a description\\"} --> <p>p1</p> <!-- /wp:paragraph --> <!-- wp:quote --> <blockquote class=\\"wp-block-quote\\"><p></p></blockquote> -<!-- /wp:quote --></div></div> +<!-- /wp:quote --></div> <!-- /wp:group -->" `; exports[`cpt locking template_lock all unlocked group should allow blocks to be removed 1`] = ` "<!-- wp:group {\\"templateLock\\":false} --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:quote --> +<div class=\\"wp-block-group\\"><!-- wp:quote --> <blockquote class=\\"wp-block-quote\\"><p></p></blockquote> -<!-- /wp:quote --></div></div> +<!-- /wp:quote --></div> <!-- /wp:group -->" `; diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap index 4b786e99fdb23d..f3c24c200b971a 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/block-grouping.test.js.snap @@ -2,7 +2,7 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:heading --> +<div class=\\"wp-block-group\\"><!-- wp:heading --> <h2>Group Heading</h2> <!-- /wp:heading --> @@ -12,13 +12,13 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of d <!-- wp:paragraph --> <p>Some paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via block transforms 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph --> <p>First Paragraph</p> <!-- /wp:paragraph --> @@ -28,13 +28,13 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of t <!-- wp:paragraph --> <p>Third Paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via options toolbar 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph --> <p>First Paragraph</p> <!-- /wp:paragraph --> @@ -44,13 +44,13 @@ exports[`Block Grouping Group creation creates a group from multiple blocks of t <!-- wp:paragraph --> <p>Third Paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:heading --> +<div class=\\"wp-block-group\\"><!-- wp:heading --> <h2>Group Heading</h2> <!-- /wp:heading --> @@ -60,7 +60,7 @@ exports[`Block Grouping Group creation groups and ungroups multiple blocks of di <!-- wp:paragraph --> <p>Some paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; @@ -80,7 +80,7 @@ exports[`Block Grouping Group creation groups and ungroups multiple blocks of di exports[`Block Grouping Preserving selected blocks attributes preserves width alignment settings of selected blocks 1`] = ` "<!-- wp:group {\\"align\\":\\"full\\"} --> -<div class=\\"wp-block-group alignfull\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:heading --> +<div class=\\"wp-block-group alignfull\\"><!-- wp:heading --> <h2>Group Heading</h2> <!-- /wp:heading --> @@ -94,7 +94,7 @@ exports[`Block Grouping Preserving selected blocks attributes preserves width al <!-- wp:paragraph --> <p>Some paragraph</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group -->" `; diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/block-hierarchy-navigation.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/block-hierarchy-navigation.test.js.snap index 7904000d8aad2c..406e1be99232c7 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/block-hierarchy-navigation.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/block-hierarchy-navigation.test.js.snap @@ -52,12 +52,12 @@ exports[`Navigating the block hierarchy should navigate using the block hierarch exports[`Navigating the block hierarchy should select the wrapper div for a group 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph --> <p>just a paragraph</p> <!-- /wp:paragraph --> <!-- wp:separator --> <hr class=\\"wp-block-separator\\"/> -<!-- /wp:separator --></div></div> +<!-- /wp:separator --></div> <!-- /wp:group -->" `; diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap index f033b781c3dd2c..b66bee14aae4ee 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/embedding.test.js.snap @@ -7,7 +7,7 @@ exports[`Embedding content should allow the user to convert unembeddable URLs to `; exports[`Embedding content should allow the user to try embedding a failed URL again 1`] = ` -"<!-- wp:embed {\\"url\\":\\"https://twitter.com/wooyaygutenberg123454312\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"className\\":\\"\\"} --> +"<!-- wp:embed {\\"url\\":\\"https://twitter.com/wooyaygutenberg123454312\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} --> <figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\"> https://twitter.com/wooyaygutenberg123454312 </div></figure> @@ -15,13 +15,13 @@ https://twitter.com/wooyaygutenberg123454312 `; exports[`Embedding content should render embeds in the correct state 1`] = ` -"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true,\\"className\\":\\"\\"} --> +"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"twitter\\",\\"responsive\\":true} --> <figure class=\\"wp-block-embed is-type-rich is-provider-twitter wp-block-embed-twitter\\"><div class=\\"wp-block-embed__wrapper\\"> https://twitter.com/notnownikki </div></figure> <!-- /wp:embed --> -<!-- wp:embed {\\"url\\":\\"https://twitter.com/wooyaygutenberg123454312\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"embed-handler\\",\\"responsive\\":true,\\"className\\":\\"\\"} --> +<!-- wp:embed {\\"url\\":\\"https://twitter.com/wooyaygutenberg123454312\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"embed-handler\\",\\"responsive\\":true} --> <figure class=\\"wp-block-embed is-type-rich is-provider-embed-handler wp-block-embed-embed-handler\\"><div class=\\"wp-block-embed__wrapper\\"> https://twitter.com/wooyaygutenberg123454312 </div></figure> @@ -39,7 +39,7 @@ https://twitter.com/thatbunty </div></figure> <!-- /wp:embed --> -<!-- wp:embed {\\"url\\":\\"https://wordpress.org/gutenberg/handbook/block-api/attributes/\\",\\"type\\":\\"wp-embed\\",\\"providerNameSlug\\":\\"wordpress\\",\\"className\\":\\"\\"} --> +<!-- wp:embed {\\"url\\":\\"https://wordpress.org/gutenberg/handbook/block-api/attributes/\\",\\"type\\":\\"wp-embed\\",\\"providerNameSlug\\":\\"wordpress\\"} --> <figure class=\\"wp-block-embed is-type-wp-embed is-provider-wordpress wp-block-embed-wordpress\\"><div class=\\"wp-block-embed__wrapper\\"> https://wordpress.org/gutenberg/handbook/block-api/attributes/ </div></figure> @@ -59,7 +59,7 @@ https://cloudup.com/cQFlxqtY4ob `; exports[`Embedding content should retry embeds that could not be embedded with trailing slashes, without the trailing slashes 1`] = ` -"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki/\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"embed-handler\\",\\"responsive\\":true,\\"className\\":\\"\\"} --> +"<!-- wp:embed {\\"url\\":\\"https://twitter.com/notnownikki/\\",\\"type\\":\\"rich\\",\\"providerNameSlug\\":\\"embed-handler\\",\\"responsive\\":true} --> <figure class=\\"wp-block-embed is-type-rich is-provider-embed-handler wp-block-embed-embed-handler\\"><div class=\\"wp-block-embed__wrapper\\"> https://twitter.com/notnownikki/ </div></figure> diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/adding-blocks.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap similarity index 69% rename from packages/e2e-tests/specs/editor/various/__snapshots__/adding-blocks.test.js.snap rename to packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap index 9870b5be306fa7..ec465386aaf05d 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/adding-blocks.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/inserting-blocks.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`adding blocks Should insert content using the placeholder and the regular inserter 1`] = ` +exports[`Inserting blocks Should insert content using the placeholder and the regular inserter 1`] = ` "<!-- wp:paragraph --> <p>Paragraph block</p> <!-- /wp:paragraph --> @@ -18,7 +18,7 @@ exports[`adding blocks Should insert content using the placeholder and the regul <!-- /wp:paragraph -->" `; -exports[`adding blocks Should insert content using the placeholder and the regular inserter 2`] = ` +exports[`Inserting blocks Should insert content using the placeholder and the regular inserter 2`] = ` "<!-- wp:paragraph --> <p>Paragraph block</p> <!-- /wp:paragraph --> @@ -28,7 +28,7 @@ exports[`adding blocks Should insert content using the placeholder and the regul <!-- /wp:quote -->" `; -exports[`adding blocks Should insert content using the placeholder and the regular inserter 3`] = ` +exports[`Inserting blocks Should insert content using the placeholder and the regular inserter 3`] = ` "<!-- wp:paragraph --> <p>Paragraph block</p> <!-- /wp:paragraph --> @@ -53,11 +53,11 @@ lines preserved[/myshortcode] <!-- /wp:shortcode -->" `; -exports[`adding blocks inserts a block in proper place after having clicked \`Browse All\` from block appender 1`] = ` +exports[`Inserting blocks inserts a block in proper place after having clicked \`Browse All\` from block appender 1`] = ` "<!-- wp:group --> -<div class=\\"wp-block-group\\"><div class=\\"wp-block-group__inner-container\\"><!-- wp:paragraph --> +<div class=\\"wp-block-group\\"><!-- wp:paragraph --> <p>Paragraph inside group</p> -<!-- /wp:paragraph --></div></div> +<!-- /wp:paragraph --></div> <!-- /wp:group --> <!-- wp:paragraph --> @@ -65,7 +65,7 @@ exports[`adding blocks inserts a block in proper place after having clicked \`Br <!-- /wp:paragraph -->" `; -exports[`adding blocks inserts a block in proper place after having clicked \`Browse All\` from inline inserter 1`] = ` +exports[`Inserting blocks inserts a block in proper place after having clicked \`Browse All\` from inline inserter 1`] = ` "<!-- wp:paragraph --> <p>First paragraph</p> <!-- /wp:paragraph --> @@ -83,7 +83,7 @@ exports[`adding blocks inserts a block in proper place after having clicked \`Br <!-- /wp:paragraph -->" `; -exports[`adding blocks inserts a block in proper place after having clicked \`Browse All\` from inline inserter 2`] = ` +exports[`Inserting blocks inserts a block in proper place after having clicked \`Browse All\` from inline inserter 2`] = ` "<!-- wp:paragraph --> <p>First paragraph</p> <!-- /wp:paragraph --> @@ -105,7 +105,7 @@ exports[`adding blocks inserts a block in proper place after having clicked \`Br <!-- /wp:paragraph -->" `; -exports[`adding blocks inserts blocks at root level when using the root appender while selection is in an inner block 1`] = ` +exports[`Inserting blocks inserts blocks at root level when using the root appender while selection is in an inner block 1`] = ` "<!-- wp:buttons --> <div class=\\"wp-block-buttons\\"><!-- wp:button --> <div class=\\"wp-block-button\\"><a class=\\"wp-block-button__link\\">1.1</a></div> diff --git a/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap b/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap index 26f4b365f85da3..1b19d10c91b3ae 100644 --- a/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap +++ b/packages/e2e-tests/specs/editor/various/__snapshots__/writing-flow.test.js.snap @@ -297,3 +297,9 @@ exports[`Writing Flow should remember initial vertical position 1`] = ` <p><br>2</p> <!-- /wp:paragraph -->" `; + +exports[`Writing Flow should only consider the content as one tab stop 1`] = ` +"<!-- wp:table --> +<figure class=\\"wp-block-table\\"><table><tbody><tr><td></td><td>2</td></tr><tr><td></td><td></td></tr></tbody></table></figure> +<!-- /wp:table -->" +`; diff --git a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js index df09b44f4b23e2..f781cc556e13c5 100644 --- a/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js +++ b/packages/e2e-tests/specs/editor/various/block-hierarchy-navigation.test.js @@ -42,7 +42,7 @@ describe( 'Navigating the block hierarchy', () => { await page.click( '[aria-label="Two columns; equal split"]' ); // Add a paragraph in the first column. - await page.keyboard.press( 'Tab' ); // Tab to inserter. + await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter. await page.keyboard.press( 'Enter' ); // Activate inserter. await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result. @@ -78,7 +78,7 @@ describe( 'Navigating the block hierarchy', () => { await lastColumnsBlockMenuItem.click(); // Insert text in the last column block. - await page.keyboard.press( 'Tab' ); // Tab to inserter. + await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter. await page.keyboard.press( 'Enter' ); // Activate inserter. await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result. @@ -94,7 +94,7 @@ describe( 'Navigating the block hierarchy', () => { await page.click( '[aria-label="Two columns; equal split"]' ); // Add a paragraph in the first column. - await page.keyboard.press( 'Tab' ); // Tab to inserter. + await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter. await page.keyboard.press( 'Enter' ); // Activate inserter. await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result. @@ -121,7 +121,7 @@ describe( 'Navigating the block hierarchy', () => { await page.waitForSelector( '.is-selected[data-type="core/column"]' ); // Insert text in the last column block - await page.keyboard.press( 'Tab' ); // Tab to inserter. + await page.keyboard.press( 'ArrowDown' ); // Navigate to inserter. await page.keyboard.press( 'Enter' ); // Activate inserter. await page.keyboard.type( 'Paragraph' ); await pressKeyTimes( 'Tab', 2 ); // Tab to paragraph result. diff --git a/packages/e2e-tests/specs/editor/various/inserter.test.js b/packages/e2e-tests/specs/editor/various/inserter.test.js deleted file mode 100644 index 2a115bda09d69b..00000000000000 --- a/packages/e2e-tests/specs/editor/various/inserter.test.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * WordPress dependencies - */ -import { - createNewPost, - openGlobalBlockInserter, - insertBlock, - setBrowserViewport, -} from '@wordpress/e2e-test-utils'; - -describe( 'Inserter', () => { - beforeEach( async () => { - await createNewPost(); - } ); - - it( 'shows block preview when hovering over block in inserter', async () => { - await openGlobalBlockInserter(); - await page.focus( '.editor-block-list-item-paragraph' ); - const preview = await page.waitForSelector( - '.block-editor-inserter__preview', - { - visible: true, - } - ); - const isPreviewVisible = await preview.isIntersectingViewport(); - expect( isPreviewVisible ).toBe( true ); - } ); - - it.each( [ 'large', 'small' ] )( - 'last-inserted block should be given and keep the focus (%s viewport)', - async ( viewport ) => { - await setBrowserViewport( viewport ); - - await page.type( - '.block-editor-default-block-appender__content', - 'Testing inserted block focus' - ); - - await insertBlock( 'Image' ); - - await page.waitForSelector( 'figure[data-type="core/image"]' ); - - const selectedBlock = await page.evaluate( () => { - return wp.data.select( 'core/block-editor' ).getSelectedBlock(); - } ); - - expect( selectedBlock.name ).toBe( 'core/image' ); - } - ); -} ); diff --git a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js b/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js similarity index 88% rename from packages/e2e-tests/specs/editor/various/adding-blocks.test.js rename to packages/e2e-tests/specs/editor/various/inserting-blocks.test.js index efe1a049a78207..498cc94cef17f5 100644 --- a/packages/e2e-tests/specs/editor/various/adding-blocks.test.js +++ b/packages/e2e-tests/specs/editor/various/inserting-blocks.test.js @@ -2,13 +2,14 @@ * WordPress dependencies */ import { + closeGlobalBlockInserter, createNewPost, - insertBlock, getEditedPostContent, + insertBlock, + openGlobalBlockInserter, pressKeyTimes, - setBrowserViewport, - closeGlobalBlockInserter, searchForBlock, + setBrowserViewport, showBlockToolbar, } from '@wordpress/e2e-test-utils'; @@ -37,7 +38,7 @@ async function waitForInserterPatternLoad() { } ); } -describe( 'adding blocks', () => { +describe( 'Inserting blocks', () => { beforeEach( async () => { await createNewPost(); } ); @@ -157,6 +158,19 @@ describe( 'adding blocks', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'should insert block with the slash inserter when using multiple words', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '/tag cloud' ); + await page.waitForXPath( + `//*[contains(@class, "components-autocomplete__result") and contains(@class, "is-selected") and contains(text(), 'Tag Cloud')]` + ); + await page.keyboard.press( 'Enter' ); + + expect( + await page.waitForSelector( '[data-type="core/tag-cloud"]' ) + ).not.toBeNull(); + } ); + // Check for regression of https://github.com/WordPress/gutenberg/issues/9583 it( 'should not allow transfer of focus outside of the block-insertion menu once open', async () => { // Enter the default block and click the inserter toggle button to the left of it. @@ -290,14 +304,14 @@ describe( 'adding blocks', () => { inserterMenuInputSelector ); inserterMenuSearchInput.type( 'cover' ); - // We need to wait a bit after typing otherwise we might an "early" result - // that is going to be "detached" when trying to click on it - // eslint-disable-next-line no-restricted-syntax - await page.waitForTimeout( 100 ); - const coverBlock = await page.waitForSelector( + await page.waitForSelector( '.block-editor-block-types-list .editor-block-list-item-cover' ); - await coverBlock.click(); + // clicking may be too quick and may select a detached node. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Enter' ); + expect( await getEditedPostContent() ).toMatchSnapshot(); } ); @@ -372,4 +386,39 @@ describe( 'adding blocks', () => { ); expect( isFocusInBlock ).toBe( true ); } ); + + it( 'shows block preview when hovering over block in inserter', async () => { + await openGlobalBlockInserter(); + await page.focus( '.editor-block-list-item-paragraph' ); + const preview = await page.waitForSelector( + '.block-editor-inserter__preview', + { + visible: true, + } + ); + const isPreviewVisible = await preview.isIntersectingViewport(); + expect( isPreviewVisible ).toBe( true ); + } ); + + it.each( [ 'large', 'small' ] )( + 'last-inserted block should be given and keep the focus (%s viewport)', + async ( viewport ) => { + await setBrowserViewport( viewport ); + + await page.type( + '.block-editor-default-block-appender__content', + 'Testing inserted block focus' + ); + + await insertBlock( 'Image' ); + + await page.waitForSelector( 'figure[data-type="core/image"]' ); + + const selectedBlock = await page.evaluate( () => { + return wp.data.select( 'core/block-editor' ).getSelectedBlock(); + } ); + + expect( selectedBlock.name ).toBe( 'core/image' ); + } + ); } ); diff --git a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js index 029a224eaab2f4..8a595a436d9b2b 100644 --- a/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js +++ b/packages/e2e-tests/specs/editor/various/multi-block-selection.test.js @@ -16,17 +16,17 @@ async function getSelectedFlatIndices() { const indices = []; let single; - Array.from( document.querySelectorAll( '.wp-block' ) ).forEach( - ( node, index ) => { - if ( node.classList.contains( 'is-selected' ) ) { - single = index; - } - - if ( node.classList.contains( 'is-multi-selected' ) ) { - indices.push( index ); - } + Array.from( + document.querySelectorAll( '.wp-block:not(.editor-post-title)' ) + ).forEach( ( node, index ) => { + if ( node.classList.contains( 'is-selected' ) ) { + single = index + 1; } - ); + + if ( node.classList.contains( 'is-multi-selected' ) ) { + indices.push( index + 1 ); + } + } ); return single !== undefined ? single : indices; } ); @@ -489,7 +489,6 @@ describe( 'Multi-block selection', () => { await page.keyboard.press( 'Enter' ); await page.keyboard.type( '2' ); await pressKeyWithModifier( 'shift', 'ArrowUp' ); - await testNativeSelection(); expect( await getSelectedFlatIndices() ).toEqual( [ 1, 2 ] ); diff --git a/packages/e2e-tests/specs/editor/various/toolbar-roving-tabindex.test.js b/packages/e2e-tests/specs/editor/various/toolbar-roving-tabindex.test.js index ce30594e23b041..367c61eec96fb1 100644 --- a/packages/e2e-tests/specs/editor/various/toolbar-roving-tabindex.test.js +++ b/packages/e2e-tests/specs/editor/various/toolbar-roving-tabindex.test.js @@ -46,7 +46,7 @@ async function testGroupKeyboardNavigation( currentBlockTitle ) { await expectLabelToHaveFocus( 'Block: Group' ); - await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'ArrowRight' ); await expectLabelToHaveFocus( currentBlockLabel ); await pressKeyWithModifier( 'shift', 'Tab' ); await expectLabelToHaveFocus( 'Select Group' ); @@ -102,17 +102,15 @@ describe( 'Toolbar roving tabindex', () => { await page.keyboard.press( 'Home' ); await expectLabelToHaveFocus( 'Table' ); await page.click( '.blocks-table__placeholder-button' ); - await testBlockToolbarKeyboardNavigation( 'Block: Table', 'Table' ); + await page.keyboard.press( 'Tab' ); + await testBlockToolbarKeyboardNavigation( 'Body cell text', 'Table' ); await wrapCurrentBlockWithGroup( 'Table' ); await testGroupKeyboardNavigation( 'Block: Table', 'Table' ); } ); it( 'ensures custom html block toolbar uses roving tabindex', async () => { await insertBlock( 'Custom HTML' ); - await testBlockToolbarKeyboardNavigation( - 'Block: Custom HTML', - 'Custom HTML' - ); + await testBlockToolbarKeyboardNavigation( 'HTML', 'Custom HTML' ); await wrapCurrentBlockWithGroup( 'Custom HTML' ); await testGroupKeyboardNavigation( 'Block: Custom HTML', diff --git a/packages/e2e-tests/specs/editor/various/writing-flow.test.js b/packages/e2e-tests/specs/editor/various/writing-flow.test.js index af2fb84006fe43..3b8e6746ac866a 100644 --- a/packages/e2e-tests/specs/editor/various/writing-flow.test.js +++ b/packages/e2e-tests/specs/editor/various/writing-flow.test.js @@ -559,9 +559,6 @@ describe( 'Writing Flow', () => { await clickBlockToolbarButton( 'Align' ); await clickButton( 'Wide width' ); - // Focus the block. - await page.keyboard.press( 'Tab' ); - // Select the previous block. await page.keyboard.press( 'ArrowUp' ); @@ -594,4 +591,38 @@ describe( 'Writing Flow', () => { expect( type ).toBe( 'core/image' ); } ); + + it( 'should only consider the content as one tab stop', async () => { + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '/table' ); + await page.keyboard.press( 'Enter' ); + // Move into the placeholder UI. + await page.keyboard.press( 'ArrowDown' ); + // Tab to the "Create table" button. + await page.keyboard.press( 'Tab' ); + await page.keyboard.press( 'Tab' ); + // Create the table. + await page.keyboard.press( 'Space' ); + // Return focus after focus loss. This should be fixed. + await page.keyboard.press( 'Tab' ); + // Navigate to the second cell. + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.type( '2' ); + // Confirm correct setup. + expect( await getEditedPostContent() ).toMatchSnapshot(); + // The content should only have one tab stop. + await page.keyboard.press( 'Tab' ); + expect( + await page.evaluate( () => + document.activeElement.getAttribute( 'aria-label' ) + ) + ).toBe( 'Post' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + await pressKeyWithModifier( 'shift', 'Tab' ); + expect( + await page.evaluate( () => + document.activeElement.getAttribute( 'aria-label' ) + ) + ).toBe( 'Table' ); + } ); } ); diff --git a/packages/e2e-tests/specs/experiments/document-settings.test.js b/packages/e2e-tests/specs/experiments/document-settings.test.js index eb59dc2a3f82c2..8d7092d18c6592 100644 --- a/packages/e2e-tests/specs/experiments/document-settings.test.js +++ b/packages/e2e-tests/specs/experiments/document-settings.test.js @@ -1,11 +1,7 @@ /** * WordPress dependencies */ -import { - canvas, - trashAllPosts, - activateTheme, -} from '@wordpress/e2e-test-utils'; +import { trashAllPosts, activateTheme } from '@wordpress/e2e-test-utils'; /** * Internal dependencies @@ -58,9 +54,15 @@ describe( 'Document Settings', () => { describe( 'and a template part is clicked in the template', () => { it( "should display the selected template part's name in the document header", async () => { - // Click on a template part in the template - const header = await canvas().$( '.site-header' ); - await header.click(); + // Select the header template part via list view. + await page.click( 'button[aria-label="List View"]' ); + const headerTemplatePartListViewButton = await page.waitForXPath( + '//button[contains(@class, "block-editor-block-navigation-block-select-button")][contains(., "Header")]' + ); + headerTemplatePartListViewButton.click(); + await page.click( + 'button[aria-label="Close list view sidebar"]' + ); // Evaluate the document settings secondary title const actual = await getDocumentSettingsSecondaryTitle(); diff --git a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js index 57b99eb68d5d93..9081f8bebff889 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-editing.test.js @@ -36,8 +36,8 @@ const createTemplatePart = async ( await createNewButton.click(); await page.waitForSelector( isNested - ? '.wp-block-template-part .wp-block-template-part .block-editor-block-list__layout' - : '.wp-block-template-part .block-editor-block-list__layout' + ? '.wp-block-template-part .wp-block-template-part.block-editor-block-list__layout' + : '.wp-block-template-part.block-editor-block-list__layout' ); await openDocumentSettingsSidebar(); @@ -204,7 +204,7 @@ describe( 'Multi-entity editor states', () => { // Wait for site editor to load. await canvas().waitForSelector( - '.wp-block-template-part .block-editor-block-list__layout' + '.wp-block-template-part.block-editor-block-list__layout' ); // Our custom template shows up in the "Templates > General" menu; let's use it. diff --git a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js index c0c035ccd0894d..0380184033a113 100644 --- a/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js +++ b/packages/e2e-tests/specs/experiments/multi-entity-saving.test.js @@ -7,7 +7,6 @@ import { publishPost, trashAllPosts, activateTheme, - canvas, } from '@wordpress/e2e-test-utils'; /** @@ -21,7 +20,7 @@ describe( 'Multi-entity save flow', () => { const checkboxInputSelector = '.components-checkbox-control__input'; const entitiesSaveSelector = '.editor-entities-saved-states__save-button'; const templatePartSelector = '*[data-type="core/template-part"]'; - const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-block-list__layout`; + const activatedTemplatePartSelector = `${ templatePartSelector }.block-editor-block-list__layout`; const savePanelSelector = '.entities-saved-states__panel'; const closePanelButtonSelector = '.editor-post-publish-panel__header-cancel-button button'; @@ -187,11 +186,14 @@ describe( 'Multi-entity save flow', () => { await navigationPanel.backToRoot(); await navigationPanel.navigate( 'Templates' ); await navigationPanel.clickItemByText( 'Index' ); - await navigationPanel.close(); - // Click the first block so that the template part inserts in the right place. - const firstBlock = await canvas().$( '.wp-block' ); - await firstBlock.click(); + // Select the header template part via list view. + await page.click( 'button[aria-label="List View"]' ); + const headerTemplatePartListViewButton = await page.waitForXPath( + '//button[contains(@class, "block-editor-block-navigation-block-select-button")][contains(., "Header")]' + ); + headerTemplatePartListViewButton.click(); + await page.click( 'button[aria-label="Close list view sidebar"]' ); // Insert something to dirty the editor. await insertBlock( 'Paragraph' ); diff --git a/packages/e2e-tests/specs/experiments/navigation-editor.test.js b/packages/e2e-tests/specs/experiments/navigation-editor.test.js index 22edc8474c9ccc..e2b5e42edb2875 100644 --- a/packages/e2e-tests/specs/experiments/navigation-editor.test.js +++ b/packages/e2e-tests/specs/experiments/navigation-editor.test.js @@ -3,6 +3,7 @@ */ import { createJSONResponse, + pressKeyWithModifier, setUpResponseMocking, visitAdminPage, } from '@wordpress/e2e-test-utils'; @@ -14,6 +15,13 @@ import { addQueryArgs } from '@wordpress/url'; import { useExperimentalFeatures } from '../../experimental-features'; import menuItemsFixture from './fixtures/menu-items-response-fixture.json'; +const TYPE_NAMES = { + post: 'post', + page: 'page', + post_tag: 'tag', + category: 'category', +}; + const menusFixture = [ { name: 'Test Menu 1', @@ -29,6 +37,35 @@ const menusFixture = [ }, ]; +const searchFixture = [ + { + id: 300, + title: 'Home', + url: 'https://example.com/home', + type: 'post', + subtype: 'page', + }, + { + id: 301, + title: 'About', + url: 'https://example.com/about', + type: 'post', + subtype: 'page', + }, + { + id: 302, + title: 'Boats', + url: 'https://example.com/?cat=123', + type: 'category', + }, + { + id: 303, + title: 'Faves', + url: 'https://example.com/?tag=456', + type: 'post_tag', + }, +]; + // Matching against variations of the same URL encoded and non-encoded // produces the most reliable mocking. const REST_MENUS_ROUTES = [ @@ -40,9 +77,14 @@ const REST_MENU_ITEMS_ROUTES = [ `rest_route=${ encodeURIComponent( '/__experimental/menu-items' ) }`, ]; +const REST_SEARCH_ROUTES = [ + '/wp/v2/search', + `rest_route=${ encodeURIComponent( '/wp/v2/search' ) }`, +]; + /** * Determines if a given URL matches any of a given collection of - * routes (extressed as substrings). + * routes (expressed as substrings). * * @param {string} reqUrl the full URL to be tested for matches. * @param {Array} routes array of strings to match against the URL. @@ -88,6 +130,10 @@ function getMenuItemMocks( responsesByMethod ) { return getEndpointMocks( REST_MENU_ITEMS_ROUTES, responsesByMethod ); } +function getSearchMocks( responsesByMethod ) { + return getEndpointMocks( REST_SEARCH_ROUTES, responsesByMethod ); +} + async function visitNavigationEditor() { const query = addQueryArgs( '', { page: 'gutenberg-navigation', @@ -272,7 +318,7 @@ describe( 'Navigation editor', () => { expect( submenuLinkVisible ).toBeDefined(); // click in the top left corner of the canvas. - const canvas = await page.$( '.edit-navigation-layout__canvas' ); + const canvas = await page.$( '.edit-navigation-layout__content-area' ); const boundingBox = await canvas.boundingBox(); await page.mouse.click( boundingBox.x + 5, boundingBox.y + 5 ); @@ -282,4 +328,59 @@ describe( 'Navigation editor', () => { } ); expect( submenuLinkHidden ).toBeDefined(); } ); + + it( 'displays suggestions when adding a link', async () => { + await setUpResponseMocking( [ + ...getMenuMocks( { GET: assignMockMenuIds( menusFixture ) } ), + ...getSearchMocks( { GET: searchFixture } ), + ] ); + + await visitNavigationEditor(); + + // Wait for the block to be present and start an empty block. + const navBlock = await page.waitForSelector( + 'div[aria-label="Block: Navigation"]' + ); + await navBlock.click(); + const startEmptyButton = await page.waitForXPath( + '//button[.="Start empty"]' + ); + await startEmptyButton.click(); + + const appender = await page.waitForSelector( + 'button[aria-label="Add block"]' + ); + await appender.click(); + + // Must be an exact match to the word 'Link' as other + // variations also contain the word 'Link'. + const linkInserterItem = await page.waitForXPath( + '//button[@role="option"]//span[.="Link"]' + ); + await linkInserterItem.click(); + + await page.waitForSelector( 'input[aria-label="URL"]' ); + + // The link suggestions should be searchable. + for ( let i = 0; i < searchFixture.length; i++ ) { + const { title, type, subtype, url } = searchFixture[ i ]; + const expectedURL = url.replace( 'https://', '' ); + const expectedType = TYPE_NAMES[ subtype || type ]; + + await page.keyboard.type( title ); + const suggestionTitle = await page.waitForXPath( + `//button[@role="option"]//span[.="${ title }"]` + ); + const suggestionType = await page.waitForXPath( + `//button[@role="option"]//span[.="${ expectedType }"]` + ); + const suggestionURL = await page.waitForXPath( + `//button[@role="option"]//span[.="${ expectedURL }"]` + ); + expect( suggestionTitle ).toBeTruthy(); + expect( suggestionType ).toBeTruthy(); + expect( suggestionURL ).toBeTruthy(); + await pressKeyWithModifier( 'primary', 'A' ); + } + } ); } ); diff --git a/packages/e2e-tests/specs/experiments/settings-sidebar.test.js b/packages/e2e-tests/specs/experiments/settings-sidebar.test.js index 5cf6b9e7e3d5c1..43c95626525801 100644 --- a/packages/e2e-tests/specs/experiments/settings-sidebar.test.js +++ b/packages/e2e-tests/specs/experiments/settings-sidebar.test.js @@ -71,7 +71,6 @@ describe( 'Settings sidebar', () => { await navigationPanel.backToRoot(); await navigationPanel.navigate( 'Templates' ); await navigationPanel.clickItemByText( '404' ); - await navigationPanel.close(); const templateCardAfterNavigation = await getTemplateCard(); expect( templateCardBeforeNavigation ).toMatchObject( { diff --git a/packages/e2e-tests/specs/experiments/template-part.test.js b/packages/e2e-tests/specs/experiments/template-part.test.js index 618140057caf9b..d384c385f2350d 100644 --- a/packages/e2e-tests/specs/experiments/template-part.test.js +++ b/packages/e2e-tests/specs/experiments/template-part.test.js @@ -66,7 +66,6 @@ describe( 'Template Part', () => { await navigationPanel.backToRoot(); await navigationPanel.navigate( 'Templates' ); await navigationPanel.clickItemByText( 'Index' ); - await navigationPanel.close(); } async function triggerEllipsisMenuItem( textPrompt ) { @@ -166,7 +165,9 @@ describe( 'Template Part', () => { } ); it( 'Should convert selected block to template part', async () => { - await canvas().waitForSelector( '.wp-block-template-part' ); + await canvas().waitForSelector( + '.wp-block-template-part.block-editor-block-list__layout' + ); const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -204,7 +205,9 @@ describe( 'Template Part', () => { } ); it( 'Should convert multiple selected blocks to template part', async () => { - await canvas().waitForSelector( '.wp-block-template-part' ); + await canvas().waitForSelector( + '.wp-block-template-part.block-editor-block-list__layout' + ); const initialTemplateParts = await canvas().$$( '.wp-block-template-part' ); @@ -263,7 +266,7 @@ describe( 'Template Part', () => { '.editor-entities-saved-states__save-button'; const savePostSelector = '.editor-post-publish-button__button'; const templatePartSelector = '*[data-type="core/template-part"]'; - const activatedTemplatePartSelector = `${ templatePartSelector } .block-editor-block-list__layout`; + const activatedTemplatePartSelector = `${ templatePartSelector }.block-editor-block-list__layout`; const testContentSelector = `//p[contains(., "${ testContent }")]`; const createNewButtonSelector = '//button[contains(text(), "New template part")]'; diff --git a/packages/edit-navigation/package.json b/packages/edit-navigation/package.json index 35b0fc594b0b79..329c46d793fc07 100644 --- a/packages/edit-navigation/package.json +++ b/packages/edit-navigation/package.json @@ -24,7 +24,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", "@wordpress/block-library": "file:../block-library", @@ -40,6 +40,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/interface": "file:../interface", "@wordpress/keyboard-shortcuts": "file:../keyboard-shortcuts", "@wordpress/media-utils": "file:../media-utils", "@wordpress/notices": "file:../notices", diff --git a/packages/edit-navigation/src/components/editor/style.scss b/packages/edit-navigation/src/components/editor/style.scss index 108f836fb68d9a..353421d9d18018 100644 --- a/packages/edit-navigation/src/components/editor/style.scss +++ b/packages/edit-navigation/src/components/editor/style.scss @@ -5,23 +5,39 @@ max-width: $navigation-editor-width; margin: auto; + .editor-styles-wrapper { + padding: 0; + } + .components-spinner { display: block; margin: $grid-unit-15 auto; } - // Adapt the layout of the Navigation and Link blocks + // Adapt the layout of the Navigation and Link blocks. // to work better in the context of the Navigation Screen. .wp-block-navigation { margin: 0; font-size: 15px; padding: $grid-unit-15; + + // This is the default font that is going to be used in the content of the areas (blocks). + font-family: $default-font; } - .wp-block-navigation-link { + // Increase specificity. + .wp-block-navigation .wp-block-navigation-link { display: block; - // Fix focus outlines + // Show submenus on click. + > .wp-block-navigation-link__container { + // This unsets some styles inherited from the block, meant to only show submenus on click, not hover, when inside the editor. + opacity: 1; + visibility: visible; + display: none; + } + + // Fix focus outlines. &.is-selected > .wp-block-navigation-link__content, &.is-selected:hover > .wp-block-navigation-link__content { box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); @@ -33,7 +49,6 @@ } .wp-block-navigation-link__content { - padding: 0; margin-bottom: 6px; border-radius: $radius-block-ui; @@ -42,10 +57,13 @@ } } - .wp-block-navigation-link__label { - padding: $grid-unit-15; - padding-left: $grid-unit-30; + .wp-block-navigation-link__label, + .wp-block-navigation-link__placeholder-text { + padding: $grid-unit-05; + padding-left: $grid-unit-10; + } + .wp-block-navigation-link__label { // Without this Links with submenus display a pointer. cursor: text; } @@ -75,14 +93,16 @@ } // Override Nav block styling for deeply nested submenus. - .has-child .wp-block-navigation__container .wp-block-navigation__container { + .has-child .wp-block-navigation__container .wp-block-navigation__container, + .has-child .wp-block-navigation__container .wp-block-navigation-link__container { left: auto; } // When editing a link with children, highlight the parent // and adjust the spacing and submenu icon. .wp-block-navigation-link.has-child.is-editing { - > .wp-block-navigation__container { + > .wp-block-navigation__container, + > .wp-block-navigation-link__container { opacity: 1; visibility: visible; position: relative; diff --git a/packages/edit-navigation/src/components/header/index.js b/packages/edit-navigation/src/components/header/index.js index 9fcba8bfb3885f..d8753de03502fb 100644 --- a/packages/edit-navigation/src/components/header/index.js +++ b/packages/edit-navigation/src/components/header/index.js @@ -6,14 +6,14 @@ import { find } from 'lodash'; /** * WordPress dependencies */ +import { DropdownMenu } from '@wordpress/components'; +import { PinnedItems } from '@wordpress/interface'; import { __, sprintf } from '@wordpress/i18n'; -import { Button, Dropdown, DropdownMenu, Popover } from '@wordpress/components'; /** * Internal dependencies */ import SaveButton from './save-button'; -import ManageLocations from './manage-locations'; import MenuSwitcher from '../menu-switcher'; export default function Header( { @@ -69,7 +69,7 @@ export default function Header( { popoverProps={ { className: 'edit-navigation-header__menu-switcher-dropdown', - position: 'bottom left', + position: 'bottom center', } } > { ( { onClose } ) => ( @@ -84,24 +84,8 @@ export default function Header( { ) } </DropdownMenu> - <Dropdown - contentClassName="edit-navigation-header__manage-locations" - position="bottom left" - renderToggle={ ( { isOpen, onToggle } ) => ( - <Button - isTertiary - aria-expanded={ isOpen } - onClick={ onToggle } - > - { __( 'Manage locations' ) } - </Button> - ) } - renderContent={ () => <ManageLocations /> } - /> - <SaveButton navigationPost={ navigationPost } /> - - <Popover.Slot name="block-toolbar" /> + <PinnedItems.Slot scope="core/edit-navigation" /> </div> ) } </div> diff --git a/packages/edit-navigation/src/components/header/manage-locations.js b/packages/edit-navigation/src/components/header/manage-locations.js deleted file mode 100644 index f9d2e31f5b0804..00000000000000 --- a/packages/edit-navigation/src/components/header/manage-locations.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { Spinner, SelectControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import useMenuLocations from '../../hooks/use-menu-locations'; - -export default function ManageLocations() { - const menus = useSelect( ( select ) => select( 'core' ).getMenus(), [] ); - - const [ menuLocations, assignMenuToLocation ] = useMenuLocations(); - - if ( ! menus || ! menuLocations ) { - return <Spinner />; - } - - if ( ! menus.length ) { - return <p>{ __( 'There are no available menus.' ) }</p>; - } - - if ( ! menuLocations.length ) { - return <p>{ __( 'There are no available menu locations.' ) }</p>; - } - - return menuLocations.map( ( menuLocation ) => ( - <SelectControl - key={ menuLocation.name } - label={ menuLocation.description } - labelPosition="top" - value={ menuLocation.menu } - options={ [ - { value: 0, label: __( '-' ) }, - ...menus.map( ( menu ) => ( { - value: menu.id, - label: menu.name, - } ) ), - ] } - onChange={ ( menuId ) => { - assignMenuToLocation( menuLocation.name, Number( menuId ) ); - } } - /> - ) ); -} diff --git a/packages/edit-navigation/src/components/header/style.scss b/packages/edit-navigation/src/components/header/style.scss index f280e58298819b..3e4ea409e897b1 100644 --- a/packages/edit-navigation/src/components/header/style.scss +++ b/packages/edit-navigation/src/components/header/style.scss @@ -1,18 +1,19 @@ .edit-navigation-header { display: flex; align-items: center; - padding: $grid-unit-15; + padding: $grid-unit-15 $grid-unit-30 $grid-unit-15 20px; } .edit-navigation-header__title-subtitle { flex-grow: 1; - padding-left: $grid-unit-10; } .edit-navigation-header__title { - font-size: $default-font-size; - line-height: $default-line-height; + font-size: 23px; + font-weight: 400; margin: 0; + padding: 7px 0 4px 0; + line-height: 1.3; } .edit-navigation-header__subtitle { @@ -23,9 +24,25 @@ .edit-navigation-header__actions { display: flex; + + > .components-dropdown, + > .components-button, + > .interface-pinned-items .components-button { + &:not(:last-child) { + margin-right: $grid-unit-15; + } + } } .edit-navigation-header__menu-switcher-dropdown { // Appear below the modal overlay. z-index: z-index(".components-popover.edit-navigation-header__menu-switcher-dropdown"); } + +// Hide notices. +.gutenberg_page_gutenberg-navigation { + .notice, + #wpfooter { + display: none; + } +} diff --git a/packages/edit-navigation/src/components/inspector-additions/auto-add-pages-panel.js b/packages/edit-navigation/src/components/inspector-additions/auto-add-pages.js similarity index 54% rename from packages/edit-navigation/src/components/inspector-additions/auto-add-pages-panel.js rename to packages/edit-navigation/src/components/inspector-additions/auto-add-pages.js index 62ca9ffb2db989..9e6d53853534f3 100644 --- a/packages/edit-navigation/src/components/inspector-additions/auto-add-pages-panel.js +++ b/packages/edit-navigation/src/components/inspector-additions/auto-add-pages.js @@ -3,10 +3,10 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect } from '@wordpress/element'; -import { PanelBody, CheckboxControl } from '@wordpress/components'; +import { ToggleControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -export default function AutoAddPagesPanel( { menuId } ) { +export default function AutoAddPages( { menuId } ) { const menu = useSelect( ( select ) => select( 'core' ).getMenu( menuId ), [ menuId, ] ); @@ -22,18 +22,19 @@ export default function AutoAddPagesPanel( { menuId } ) { const { saveMenu } = useDispatch( 'core' ); return ( - <PanelBody> - <CheckboxControl - label={ __( 'Automatically add new top-level pages' ) } - checked={ autoAddPages ?? false } - onChange={ ( newAutoAddPages ) => { - setAutoAddPages( newAutoAddPages ); - saveMenu( { - ...menu, - auto_add: newAutoAddPages, - } ); - } } - /> - </PanelBody> + <ToggleControl + label={ __( 'Add new pages' ) } + help={ __( + 'Automatically add published top-level pages to this menu.' + ) } + checked={ autoAddPages ?? false } + onChange={ ( newAutoAddPages ) => { + setAutoAddPages( newAutoAddPages ); + saveMenu( { + ...menu, + auto_add: newAutoAddPages, + } ); + } } + /> ); } diff --git a/packages/edit-navigation/src/components/inspector-additions/delete-menu-panel.js b/packages/edit-navigation/src/components/inspector-additions/delete-menu-panel.js deleted file mode 100644 index c112840b621e20..00000000000000 --- a/packages/edit-navigation/src/components/inspector-additions/delete-menu-panel.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelBody, Button } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -export default function DeleteMenuPanel( { onDeleteMenu } ) { - return ( - <PanelBody className="edit-navigation-inspector-additions__delete-menu-panel"> - <Button - isLink - isDestructive - onClick={ () => { - if ( - // eslint-disable-next-line no-alert - window.confirm( - __( - 'Are you sure you want to delete this navigation?' - ) - ) - ) { - onDeleteMenu(); - } - } } - > - { __( 'Delete menu' ) } - </Button> - </PanelBody> - ); -} diff --git a/packages/edit-navigation/src/components/inspector-additions/delete-menu.js b/packages/edit-navigation/src/components/inspector-additions/delete-menu.js new file mode 100644 index 00000000000000..940e19d8bc9384 --- /dev/null +++ b/packages/edit-navigation/src/components/inspector-additions/delete-menu.js @@ -0,0 +1,27 @@ +/** + * WordPress dependencies + */ +import { Button } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +export default function DeleteMenu( { onDeleteMenu } ) { + return ( + <Button + className="edit-navigation-inspector-additions__delete-menu-button" + isTertiary + isDestructive + onClick={ () => { + if ( + // eslint-disable-next-line no-alert + window.confirm( + __( 'Are you sure you want to delete this navigation?' ) + ) + ) { + onDeleteMenu(); + } + } } + > + { __( 'Delete menu' ) } + </Button> + ); +} diff --git a/packages/edit-navigation/src/components/inspector-additions/index.js b/packages/edit-navigation/src/components/inspector-additions/index.js index 50fddb1987785c..9bfbf3a45638e3 100644 --- a/packages/edit-navigation/src/components/inspector-additions/index.js +++ b/packages/edit-navigation/src/components/inspector-additions/index.js @@ -7,13 +7,22 @@ import { InspectorControls } from '@wordpress/block-editor'; /** * Internal dependencies */ -import AutoAddPagesPanel from './auto-add-pages-panel'; -import DeleteMenuPanel from './delete-menu-panel'; +import AutoAddPages from './auto-add-pages'; +import DeleteMenu from './delete-menu'; +import ManageLocations from './manage-locations'; import { NameEditor } from '../name-editor'; import { PanelBody } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -export default function InspectorAdditions( { menuId, onDeleteMenu } ) { +export default function InspectorAdditions( { + menuId, + menus, + onDeleteMenu, + onSelectMenu, + isManageLocationsModalOpen, + closeManageLocationsModal, + openManageLocationsModal, +} ) { const selectedBlock = useSelect( ( select ) => select( 'core/block-editor' ).getSelectedBlock(), [] @@ -25,10 +34,20 @@ export default function InspectorAdditions( { menuId, onDeleteMenu } ) { return ( <InspectorControls> - <PanelBody title={ __( 'Menu Settings' ) }> + <PanelBody title={ __( 'Menu settings' ) }> <NameEditor /> - <AutoAddPagesPanel menuId={ menuId } /> - <DeleteMenuPanel onDeleteMenu={ onDeleteMenu } /> + <AutoAddPages menuId={ menuId } /> + <DeleteMenu onDeleteMenu={ onDeleteMenu } /> + </PanelBody> + <PanelBody title={ __( 'Theme locations' ) }> + <ManageLocations + menus={ menus } + selectedMenuId={ menuId } + onSelectMenu={ onSelectMenu } + isModalOpen={ isManageLocationsModalOpen } + closeModal={ closeManageLocationsModal } + openModal={ openManageLocationsModal } + /> </PanelBody> </InspectorControls> ); diff --git a/packages/edit-navigation/src/components/inspector-additions/manage-locations.js b/packages/edit-navigation/src/components/inspector-additions/manage-locations.js new file mode 100644 index 00000000000000..a9b81cdd4f4486 --- /dev/null +++ b/packages/edit-navigation/src/components/inspector-additions/manage-locations.js @@ -0,0 +1,154 @@ +/** + * WordPress dependencies + */ +import { + Spinner, + SelectControl, + CheckboxControl, + Button, + Modal, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useMenuLocations } from '../../hooks'; + +export default function ManageLocations( { + onSelectMenu, + isModalOpen, + openModal, + closeModal, + menus, + selectedMenuId, +} ) { + const { + menuLocations, + assignMenuToLocation, + toggleMenuLocationAssignment, + } = useMenuLocations(); + + if ( ! menuLocations ) { + return <Spinner />; + } + + if ( ! menuLocations.length ) { + return <p>{ __( 'There are no available menu locations.' ) }</p>; + } + + const themeLocationCountTextMain = sprintf( + // translators: Number of available theme locations. + __( + 'Your current theme provides %d different locations to place menu.' + ), + menuLocations.length + ); + + const themeLocationCountTextModal = sprintf( + // translators: Number of available theme locations. + __( + 'Your current theme supports %d different locations. Select which menu appears in each location.' + ), + menuLocations.length + ); + + const menusWithSelection = menuLocations.map( ( { name, menu } ) => { + const menuOnLocation = menus + .filter( ( { id } ) => ! [ 0, selectedMenuId ].includes( id ) ) + .find( ( { id } ) => id === menu ); + + return ( + <li + key={ name } + className="edit-navigation-manage-locations__checklist-item" + > + <CheckboxControl + className="edit-navigation-manage-locations__menu-location-checkbox" + checked={ menu === selectedMenuId } + onChange={ () => + toggleMenuLocationAssignment( name, selectedMenuId ) + } + label={ name } + help={ + menuOnLocation && + sprintf( + // translators: menu name. + __( 'Currently using %s' ), + menuOnLocation.name + ) + } + /> + </li> + ); + } ); + + const menuLocationCard = menuLocations.map( ( menuLocation ) => ( + <div + key={ menuLocation.name } + className="edit-navigation-manage-locations__menu-entry" + > + <SelectControl + key={ menuLocation.name } + className="edit-navigation-manage-locations__select-menu" + label={ menuLocation.description } + labelPosition="top" + value={ menuLocation.menu } + options={ [ + { value: 0, label: __( '-' ), key: 0 }, + ...menus.map( ( { id, name } ) => ( { + key: id, + value: id, + label: name, + } ) ), + ] } + onChange={ ( menuId ) => { + assignMenuToLocation( menuLocation.name, Number( menuId ) ); + } } + /> + <Button + isSecondary + style={ { + visibility: !! menuLocation.menu ? 'visible' : 'hidden', + } } + className="edit-navigation-manage-locations__edit-button" + onClick={ () => ( + closeModal(), onSelectMenu( menuLocation.menu ) + ) } + > + { __( 'Edit' ) } + </Button> + </div> + ) ); + + return ( + <> + <div className="edit-navigation-manage-locations__theme-location-text-main"> + { themeLocationCountTextMain } + </div> + <ul className="edit-navigation-manage-locations__checklist"> + { menusWithSelection } + </ul> + <Button + isSecondary + className="edit-navigation-manage-locations__open-menu-locations-modal-button" + aria-expanded={ isModalOpen } + onClick={ openModal } + > + { __( 'Manage locations' ) } + </Button> + { isModalOpen && ( + <Modal + className="edit-navigation-manage-locations__modal" + title={ __( 'Manage locations' ) } + onRequestClose={ closeModal } + > + <div className="edit-navigation-manage-locations__theme-location-text-modal"> + { themeLocationCountTextModal } + </div> + { menuLocationCard } + </Modal> + ) } + </> + ); +} diff --git a/packages/edit-navigation/src/components/inspector-additions/style.scss b/packages/edit-navigation/src/components/inspector-additions/style.scss index 4c2c5bdf8c3eb8..8feb6f6293d6d9 100644 --- a/packages/edit-navigation/src/components/inspector-additions/style.scss +++ b/packages/edit-navigation/src/components/inspector-additions/style.scss @@ -1,16 +1,47 @@ -.block-editor-block-inspector__no-blocks, -.block-editor-block-inspector { - width: $sidebar-width; - background: $white; - border-left: 1px solid $gray-300; - position: fixed; - top: $grid-unit-40; - right: 0; - bottom: 0; - z-index: 1; - overflow: auto; -} - -.edit-navigation-inspector-additions__delete-menu-panel { +.edit-navigation-inspector-additions__delete-menu-button { + width: 100%; + justify-content: center; +} + +.edit-navigation-manage-locations__modal { + max-width: 360px; +} + +.edit-navigation-manage-locations__delete-menu-panel { text-align: center; } + +.edit-navigation-manage-locations__menu-entry .components-base-control { + width: 100%; +} + +.edit-navigation-manage-locations__edit-button { + flex: 1; +} + +.edit-navigation-manage-locations__menu-entry { + display: flex; + margin-bottom: $grid-unit-15; + margin-top: $grid-unit-15; + .components-custom-select-control, + .components-custom-select-control__button { + width: 100%; + } + button { + height: 100%; + } +} + +.edit-navigation-manage-locations__select-menu { + padding-right: $grid-unit-10; +} + +.edit-navigation-manage-locations__open-menu-locations-modal-button { + width: 100%; + justify-content: center; +} + +.edit-navigation-manage-locations__theme-location-text-main, +.edit-navigation-manage-locations__theme-location-text-modal { + margin-bottom: $grid-unit-30; +} diff --git a/packages/edit-navigation/src/components/layout/block-toolbar.js b/packages/edit-navigation/src/components/layout/block-toolbar.js new file mode 100644 index 00000000000000..082aec025fed89 --- /dev/null +++ b/packages/edit-navigation/src/components/layout/block-toolbar.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { + BlockToolbar, + store as blockEditorStore, +} from '@wordpress/block-editor'; +import { Popover } from '@wordpress/components'; +import { useSelect } from '@wordpress/data'; + +export default function NavigationEditorBlockToolbar( { isFixed } ) { + const isNavigationMode = useSelect( + ( select ) => select( blockEditorStore ).isNavigationMode(), + [] + ); + return ( + <> + <Popover.Slot name="block-toolbar" /> + { isFixed && ( + <div className="edit-navigation-layout__block-toolbar"> + { ! isNavigationMode && <BlockToolbar hideDragHandle /> } + </div> + ) } + </> + ); +} diff --git a/packages/edit-navigation/src/components/layout/index.js b/packages/edit-navigation/src/components/layout/index.js index be6c0cd4f1a1c1..8971dab0b50dd9 100644 --- a/packages/edit-navigation/src/components/layout/index.js +++ b/packages/edit-navigation/src/components/layout/index.js @@ -6,20 +6,26 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { + BlockEditorKeyboardShortcuts, + BlockEditorProvider, + __unstableUseBlockSelectionClearer as useBlockSelectionClearer, +} from '@wordpress/block-editor'; import { DropZoneProvider, Popover, SlotFillProvider, Spinner, } from '@wordpress/components'; -import { useDispatch } from '@wordpress/data'; -import { - BlockEditorKeyboardShortcuts, - BlockEditorProvider, - BlockInspector, - __unstableUseBlockSelectionClearer as useBlockSelectionClearer, -} from '@wordpress/block-editor'; +import { useViewportMatch } from '@wordpress/compose'; +import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo, useState } from '@wordpress/element'; +import { + InterfaceSkeleton, + ComplementaryArea, + store as interfaceStore, +} from '@wordpress/interface'; +import { __ } from '@wordpress/i18n'; /** * Internal dependencies @@ -34,14 +40,26 @@ import { } from '../../hooks'; import ErrorBoundary from '../error-boundary'; import NavigationEditorShortcuts from './shortcuts'; +import Sidebar from './sidebar'; import Header from '../header'; import Notices from '../notices'; +import BlockToolbar from './block-toolbar'; import Editor from '../editor'; import InspectorAdditions from '../inspector-additions'; import { store as editNavigationStore } from '../../store'; +const interfaceLabels = { + /* translators: accessibility text for the navigation screen top bar landmark region. */ + header: __( 'Navigation top bar' ), + /* translators: accessibility text for the navigation screen content landmark region. */ + body: __( 'Navigation menu blocks' ), + /* translators: accessibility text for the widgets screen settings landmark region. */ + sidebar: __( 'Navigation settings' ), +}; + export default function Layout( { blockEditorSettings } ) { - const canvasRef = useBlockSelectionClearer(); + const contentAreaRef = useBlockSelectionClearer(); + const isLargeViewport = useViewportMatch( 'medium' ); const [ isMenuNameControlFocused, setIsMenuNameControlFocused ] = useState( false ); @@ -56,16 +74,29 @@ export default function Layout( { blockEditorSettings } ) { navigationPost, selectMenu, deleteMenu, + openManageLocationsModal, + closeManageLocationsModal, + isManageLocationsModalOpen, } = useNavigationEditor(); const [ blocks, onInput, onChange ] = useNavigationBlockEditor( navigationPost ); + const { hasSidebarEnabled } = useSelect( + ( select ) => ( { + hasSidebarEnabled: !! select( + interfaceStore + ).getActiveComplementaryArea( 'core/edit-navigation' ), + } ), + [] + ); + useMenuNotifications( selectedMenuId ); const hasMenus = !! menus?.length; const isBlockEditorReady = !! ( hasMenus && navigationPost ); + const hasPermanentSidebar = isLargeViewport && hasMenus; return ( <ErrorBoundary> @@ -73,13 +104,17 @@ export default function Layout( { blockEditorSettings } ) { <DropZoneProvider> <BlockEditorKeyboardShortcuts.Register /> <NavigationEditorShortcuts.Register /> - + <NavigationEditorShortcuts saveBlocks={ savePost } /> <Notices /> - - <div - className={ classnames( 'edit-navigation-layout', { - 'has-block-inspector': isBlockEditorReady, - } ) } + <BlockEditorProvider + value={ blocks } + onInput={ onInput } + onChange={ onChange } + settings={ { + ...blockEditorSettings, + templateLock: 'all', + } } + useSubRegistry={ false } > <MenuIdContext.Provider value={ selectedMenuId }> <IsMenuNameControlFocusedContext.Provider @@ -91,57 +126,88 @@ export default function Layout( { blockEditorSettings } ) { [ isMenuNameControlFocused ] ) } > - <Header - isPending={ ! hasLoadedMenus } - menus={ menus } - selectedMenuId={ selectedMenuId } - onSelectMenu={ selectMenu } - navigationPost={ navigationPost } - /> - - { ! hasFinishedInitialLoad && <Spinner /> } + <InterfaceSkeleton + className={ classnames( + 'edit-navigation-layout', + { + 'has-permanent-sidebar': hasPermanentSidebar, + } + ) } + labels={ interfaceLabels } + header={ + <Header + isPending={ ! hasLoadedMenus } + menus={ menus } + selectedMenuId={ selectedMenuId } + onSelectMenu={ selectMenu } + navigationPost={ navigationPost } + /> + } + content={ + <> + { ! hasFinishedInitialLoad && ( + <Spinner /> + ) } - { hasFinishedInitialLoad && ! hasMenus && ( - <EmptyState /> - ) } + { hasFinishedInitialLoad && + ! hasMenus && <EmptyState /> } - { isBlockEditorReady && ( - <BlockEditorProvider - value={ blocks } - onInput={ onInput } - onChange={ onChange } - settings={ { - ...blockEditorSettings, - templateLock: 'all', - } } - useSubRegistry={ false } - > - <BlockEditorKeyboardShortcuts /> - <NavigationEditorShortcuts - saveBlocks={ savePost } - /> - <div - className="edit-navigation-layout__canvas" - ref={ canvasRef } - > - <Editor - isPending={ ! hasLoadedMenus } - blocks={ blocks } - /> - </div> - <InspectorAdditions - menuId={ selectedMenuId } - onDeleteMenu={ deleteMenu } - /> - <BlockInspector - bubblesVirtually={ false } - /> - </BlockEditorProvider> - ) } + { isBlockEditorReady && ( + <> + <BlockToolbar + isFixed={ + ! isLargeViewport + } + /> + <div + className="edit-navigation-layout__content-area" + ref={ contentAreaRef } + > + <Editor + isPending={ + ! hasLoadedMenus + } + blocks={ blocks } + /> + <InspectorAdditions + isManageLocationsModalOpen={ + isManageLocationsModalOpen + } + openManageLocationsModal={ + openManageLocationsModal + } + closeManageLocationsModal={ + closeManageLocationsModal + } + onSelectMenu={ + selectMenu + } + menus={ menus } + menuId={ + selectedMenuId + } + onDeleteMenu={ + deleteMenu + } + /> + </div> + </> + ) } + </> + } + sidebar={ + ( hasPermanentSidebar || + hasSidebarEnabled ) && ( + <ComplementaryArea.Slot scope="core/edit-navigation" /> + ) + } + /> + <Sidebar + hasPermanentSidebar={ hasPermanentSidebar } + /> </IsMenuNameControlFocusedContext.Provider> </MenuIdContext.Provider> - </div> - + </BlockEditorProvider> <Popover.Slot /> </DropZoneProvider> </SlotFillProvider> diff --git a/packages/edit-navigation/src/components/layout/sidebar.js b/packages/edit-navigation/src/components/layout/sidebar.js new file mode 100644 index 00000000000000..8dc3edf44e54ad --- /dev/null +++ b/packages/edit-navigation/src/components/layout/sidebar.js @@ -0,0 +1,65 @@ +/** + * WordPress dependencies + */ +import { BlockInspector } from '@wordpress/block-editor'; +import { useDispatch } from '@wordpress/data'; +import { useEffect } from '@wordpress/element'; +import { + ComplementaryArea, + store as interfaceStore, +} from '@wordpress/interface'; +import { __ } from '@wordpress/i18n'; +import { cog } from '@wordpress/icons'; + +/** + * Internal dependencies + */ +import { + COMPLEMENTARY_AREA_SCOPE, + COMPLEMENTARY_AREA_ID, +} from '../../constants'; + +function useTogglePermanentSidebar( hasPermanentSidebar ) { + const { enableComplementaryArea, disableComplementaryArea } = useDispatch( + interfaceStore + ); + + useEffect( () => { + if ( hasPermanentSidebar ) { + enableComplementaryArea( + COMPLEMENTARY_AREA_SCOPE, + COMPLEMENTARY_AREA_ID + ); + } else { + disableComplementaryArea( + COMPLEMENTARY_AREA_SCOPE, + COMPLEMENTARY_AREA_ID + ); + } + }, [ + hasPermanentSidebar, + enableComplementaryArea, + disableComplementaryArea, + ] ); +} + +export default function Sidebar( { hasPermanentSidebar } ) { + useTogglePermanentSidebar( hasPermanentSidebar ); + + return ( + <ComplementaryArea + className="edit-navigation-sidebar" + /* translators: button label text should, if possible, be under 16 characters. */ + title={ __( 'Settings' ) } + closeLabel={ __( 'Close settings' ) } + scope={ COMPLEMENTARY_AREA_SCOPE } + identifier={ COMPLEMENTARY_AREA_ID } + icon={ cog } + isActiveByDefault={ hasPermanentSidebar } + header={ <></> } + isPinnable={ ! hasPermanentSidebar } + > + <BlockInspector /> + </ComplementaryArea> + ); +} diff --git a/packages/edit-navigation/src/components/layout/style.scss b/packages/edit-navigation/src/components/layout/style.scss index 1138a64ef15e75..c86e2c6ca8da13 100644 --- a/packages/edit-navigation/src/components/layout/style.scss +++ b/packages/edit-navigation/src/components/layout/style.scss @@ -22,7 +22,7 @@ } .edit-navigation-layout { - &.has-block-inspector { + &.has-permanent-sidebar { margin-right: $sidebar-width; } @@ -30,20 +30,74 @@ margin: $navigation-editor-spacing-top auto 0; } - .edit-navigation-layout__canvas { - // Provide space for the floating block toolbar. - padding-top: $navigation-editor-spacing-top; + .edit-navigation-layout__content-area { + // Add some margin above the fixed mobile toolbar. + padding-top: $grid-unit-15; + @include break-medium() { + // Provide space for the floating block toolbar. + padding-top: $navigation-editor-spacing-top; + } // Ensure the entire layout is full-height, the background // of the editing canvas needs to be full-height for block // deselection to work. flex-grow: 1; } + + .interface-interface-skeleton__header { + border-bottom-color: transparent; + } + + // Force the sidebar to the right side of the screen on larger + // breakpoints. + &.has-permanent-sidebar .interface-interface-skeleton__sidebar { + position: fixed !important; + top: $grid-unit-40; + right: 0; + bottom: 0; + left: auto; + + // Hide the toggle as the sidebar should be permanently open. + .interface-complementary-area-header { + display: none; + } + } +} + +.edit-navigation-layout__block-toolbar { + // Make the fixed toolbar appear in a similar position to the floating toolbar. + // Take the spacing for the floating toolbar, then subtract the toolbar height and + // the gap between the content area and the toolbar. + margin-top: $navigation-editor-spacing-top - $block-toolbar-height - $grid-unit-15; + + .block-editor-block-toolbar { + background: $white; + border: $border-width solid $gray-900; + border-radius: $radius-block-ui; + max-width: $navigation-editor-width; + margin: auto; + overflow-y: hidden; + } + + .components-toolbar, + .components-toolbar-group { + border-right-color: $gray-900; + + .components-toolbar, + .components-toolbar-group { + border-width: 0; + } + } + + height: $block-toolbar-height; } .edit-navigation-empty-state { - width: $navigation-editor-width; - margin-top: $navigation-editor-spacing-top; + max-width: $navigation-editor-width; margin-left: auto; margin-right: auto; + @include break-medium() { + // Match the padding top of the editor canvas. + margin-top: $navigation-editor-spacing-top; + } } diff --git a/packages/edit-navigation/src/utils/constants.js b/packages/edit-navigation/src/constants/index.js similarity index 58% rename from packages/edit-navigation/src/utils/constants.js rename to packages/edit-navigation/src/constants/index.js index 9074d2f5e0a59e..04587b60671d99 100644 --- a/packages/edit-navigation/src/utils/constants.js +++ b/packages/edit-navigation/src/constants/index.js @@ -25,3 +25,17 @@ export const NAVIGATION_POST_KIND = 'root'; * @type {string} */ export const NAVIGATION_POST_POST_TYPE = 'postType'; + +/** + * The scope name of the editor's complementary area. + * + * @type {string} + */ +export const COMPLEMENTARY_AREA_SCOPE = 'core/edit-navigation'; + +/** + * The identifier of the editor's complementary area. + * + * @type {string} + */ +export const COMPLEMENTARY_AREA_ID = 'edit-navigation/block-inspector'; diff --git a/packages/edit-navigation/src/filters/add-menu-name-editor.js b/packages/edit-navigation/src/filters/add-menu-name-editor.js index e7c0e4210aeb62..030fd7da961b6e 100644 --- a/packages/edit-navigation/src/filters/add-menu-name-editor.js +++ b/packages/edit-navigation/src/filters/add-menu-name-editor.js @@ -15,8 +15,8 @@ const addMenuNameEditor = createHigherOrderComponent( } return ( <> - <BlockEdit { ...props } /> <NameDisplay /> + <BlockEdit { ...props } /> </> ); }, diff --git a/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js b/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js index a2074e2216501c..5752f642f52299 100644 --- a/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js +++ b/packages/edit-navigation/src/filters/remove-edit-unsupported-features.js @@ -15,7 +15,6 @@ const removeNavigationBlockEditUnsupportedFeatures = createHigherOrderComponent( { ...props } hasSubmenuIndicatorSetting={ false } hasItemJustificationControls={ false } - hasListViewModal={ false } /> ); }, diff --git a/packages/edit-navigation/src/hooks/use-menu-entity.js b/packages/edit-navigation/src/hooks/use-menu-entity.js index 0b092f62a5d837..b0347b115aaa65 100644 --- a/packages/edit-navigation/src/hooks/use-menu-entity.js +++ b/packages/edit-navigation/src/hooks/use-menu-entity.js @@ -5,7 +5,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies */ -import { MENU_KIND, MENU_POST_TYPE } from '../utils/constants'; +import { MENU_KIND, MENU_POST_TYPE } from '../constants'; import { untitledMenu } from './index'; diff --git a/packages/edit-navigation/src/hooks/use-menu-locations.js b/packages/edit-navigation/src/hooks/use-menu-locations.js index bfd10225904069..548a34fc0138b1 100644 --- a/packages/edit-navigation/src/hooks/use-menu-locations.js +++ b/packages/edit-navigation/src/hooks/use-menu-locations.js @@ -5,6 +5,21 @@ import { useState, useEffect, useMemo, useCallback } from '@wordpress/element'; import apiFetch from '@wordpress/api-fetch'; import { useDispatch } from '@wordpress/data'; +/** + * External dependencies + */ +import { merge } from 'lodash'; + +const locationsForMenuId = ( menuLocationsByName, id ) => + Object.values( menuLocationsByName ) + .filter( ( { menu } ) => menu === id ) + .map( ( { name } ) => name ); + +const withLocations = ( menuLocationsByName ) => ( id ) => ( { + id, + locations: locationsForMenuId( menuLocationsByName, id ), +} ); + export default function useMenuLocations() { const [ menuLocationsByName, setMenuLocationsByName ] = useState( null ); @@ -14,7 +29,7 @@ export default function useMenuLocations() { const fetchMenuLocationsByName = async () => { const newMenuLocationsByName = await apiFetch( { method: 'GET', - path: '/__experimental/menu-locations', + path: '/__experimental/menu-locations/', } ); if ( isMounted ) { @@ -23,7 +38,6 @@ export default function useMenuLocations() { }; fetchMenuLocationsByName(); - return () => ( isMounted = false ); }, [] ); @@ -33,50 +47,36 @@ export default function useMenuLocations() { async ( locationName, newMenuId ) => { const oldMenuId = menuLocationsByName[ locationName ].menu; - const newMenuLocationsByName = { - ...menuLocationsByName, - [ locationName ]: { - ...menuLocationsByName[ locationName ], - menu: newMenuId, - }, - }; + const newMenuLocationsByName = merge( menuLocationsByName, { + [ locationName ]: { menu: newMenuId }, + } ); setMenuLocationsByName( newMenuLocationsByName ); - const promises = []; - - if ( oldMenuId ) { - promises.push( - saveMenu( { - id: oldMenuId, - locations: Object.values( newMenuLocationsByName ) - .filter( ( { menu } ) => menu === oldMenuId ) - .map( ( { name } ) => name ), - } ) - ); - } - - if ( newMenuId ) { - promises.push( - saveMenu( { - id: newMenuId, - locations: Object.values( newMenuLocationsByName ) - .filter( ( { menu } ) => menu === newMenuId ) - .map( ( { name } ) => name ), - } ) - ); - } - - await Promise.all( promises ); + [ oldMenuId, newMenuId ] + .filter( Boolean ) + .map( withLocations( newMenuLocationsByName ) ) + .forEach( saveMenu ); }, [ menuLocationsByName ] ); + const toggleMenuLocationAssignment = ( locationName, newMenuId ) => { + const idToSet = + menuLocationsByName[ locationName ].menu === newMenuId + ? 0 + : newMenuId; + assignMenuToLocation( locationName, idToSet ); + }; + const menuLocations = useMemo( - () => - menuLocationsByName ? Object.values( menuLocationsByName ) : null, + () => Object.values( menuLocationsByName || {} ), [ menuLocationsByName ] ); - return [ menuLocations, assignMenuToLocation ]; + return { + menuLocations, + assignMenuToLocation, + toggleMenuLocationAssignment, + }; } diff --git a/packages/edit-navigation/src/hooks/use-menu-notifications.js b/packages/edit-navigation/src/hooks/use-menu-notifications.js index e85f33f164fdd8..bedcbd8cb7901e 100644 --- a/packages/edit-navigation/src/hooks/use-menu-notifications.js +++ b/packages/edit-navigation/src/hooks/use-menu-notifications.js @@ -7,7 +7,7 @@ import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies */ -import { MENU_POST_TYPE, MENU_KIND } from '../utils/constants'; +import { MENU_POST_TYPE, MENU_KIND } from '../constants'; export default function useMenuNotifications( menuId ) { const { lastSaveError, lastDeleteError } = useSelect( diff --git a/packages/edit-navigation/src/hooks/use-navigation-block-editor.js b/packages/edit-navigation/src/hooks/use-navigation-block-editor.js index 855bd92b758705..24f179d04bd75f 100644 --- a/packages/edit-navigation/src/hooks/use-navigation-block-editor.js +++ b/packages/edit-navigation/src/hooks/use-navigation-block-editor.js @@ -8,10 +8,7 @@ import { useEntityBlockEditor } from '@wordpress/core-data'; /** * Internal dependencies */ -import { - NAVIGATION_POST_KIND, - NAVIGATION_POST_POST_TYPE, -} from '../utils/constants'; +import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE } from '../constants'; import { store as editNavigationStore } from '../store'; export default function useNavigationBlockEditor( post ) { diff --git a/packages/edit-navigation/src/hooks/use-navigation-editor.js b/packages/edit-navigation/src/hooks/use-navigation-editor.js index 3af9a2b1003860..ab6aeb0638c1eb 100644 --- a/packages/edit-navigation/src/hooks/use-navigation-editor.js +++ b/packages/edit-navigation/src/hooks/use-navigation-editor.js @@ -19,6 +19,14 @@ const getMenusData = ( select ) => { }; }; export default function useNavigationEditor() { + const [ + isManageLocationsModalOpen, + setIsManageLocationsModalOpen, + ] = useState( false ); + const [ openManageLocationsModal, closeManageLocationsModal ] = [ + true, + false, + ].map( ( bool ) => () => setIsManageLocationsModalOpen( bool ) ); const { deleteMenu: _deleteMenu } = useDispatch( 'core' ); const [ selectedMenuId, setSelectedMenuId ] = useState( null ); const [ hasFinishedInitialLoad, setHasFinishedInitialLoad ] = useState( @@ -67,5 +75,8 @@ export default function useNavigationEditor() { deleteMenu, hasFinishedInitialLoad, hasLoadedMenus, + openManageLocationsModal, + closeManageLocationsModal, + isManageLocationsModalOpen, }; } diff --git a/packages/edit-navigation/src/index.js b/packages/edit-navigation/src/index.js index 4744f8b29d6f5e..8421751159f90f 100644 --- a/packages/edit-navigation/src/index.js +++ b/packages/edit-navigation/src/index.js @@ -26,8 +26,10 @@ export function initialize( id, settings ) { __experimentalRegisterExperimentalCoreBlocks(); } - settings.__experimentalFetchLinkSuggestions = () => - fetchLinkSuggestions( settings ); + settings.__experimentalFetchLinkSuggestions = ( + searchText, + searchOptions + ) => fetchLinkSuggestions( searchText, searchOptions, settings ); render( <Layout blockEditorSettings={ settings } />, diff --git a/packages/edit-navigation/src/store/actions.js b/packages/edit-navigation/src/store/actions.js index 90dcf5aa192270..de4ec3e1691c14 100644 --- a/packages/edit-navigation/src/store/actions.js +++ b/packages/edit-navigation/src/store/actions.js @@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; /** @@ -17,6 +17,7 @@ import { getMenuItemToClientIdMapping, resolveMenuItems, dispatch, + select, apiFetch, } from './controls'; import { @@ -89,7 +90,7 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) { try { // Save edits to the menu, like the menu name. - const menuResponse = yield dispatch( + yield dispatch( 'core', 'saveEditedEntityRecord', 'root', @@ -97,6 +98,18 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) { menuId ); + const error = yield select( + 'core', + 'getLastEntitySaveError', + 'root', + 'menu', + menuId + ); + + if ( error ) { + throw new Error( error.message ); + } + // Save blocks as menu items. const batchSaveResponse = yield* batchSave( menuId, @@ -104,8 +117,8 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) { post.blocks[ 0 ] ); - if ( ! batchSaveResponse.success || ! menuResponse ) { - throw new Error(); + if ( ! batchSaveResponse.success ) { + throw new Error( batchSaveResponse.data.message ); } yield dispatch( @@ -116,15 +129,17 @@ export const saveNavigationPost = serializeProcessing( function* ( post ) { type: 'snackbar', } ); - } catch ( e ) { - yield dispatch( - noticesStore, - 'createErrorNotice', - __( 'There was an error.' ), - { - type: 'snackbar', - } - ); + } catch ( saveError ) { + const errorMessage = saveError + ? sprintf( + /* translators: %s: The text of an error message (potentially untranslated). */ + __( "Unable to save: '%s'" ), + saveError.message + ) + : __( 'Unable to save: An error ocurred.' ); + yield dispatch( noticesStore, 'createErrorNotice', errorMessage, { + type: 'snackbar', + } ); } } ); diff --git a/packages/edit-navigation/src/store/resolvers.js b/packages/edit-navigation/src/store/resolvers.js index aaecaec940c1e3..a81b19011a0ba9 100644 --- a/packages/edit-navigation/src/store/resolvers.js +++ b/packages/edit-navigation/src/store/resolvers.js @@ -11,10 +11,7 @@ import { parse, createBlock } from '@wordpress/blocks'; /** * Internal dependencies */ -import { - NAVIGATION_POST_KIND, - NAVIGATION_POST_POST_TYPE, -} from '../utils/constants'; +import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE } from '../constants'; import { resolveMenuItems, dispatch } from './controls'; import { buildNavigationPostId } from './utils'; diff --git a/packages/edit-navigation/src/store/selectors.js b/packages/edit-navigation/src/store/selectors.js index 48261838028161..13c496c7b028ed 100644 --- a/packages/edit-navigation/src/store/selectors.js +++ b/packages/edit-navigation/src/store/selectors.js @@ -11,10 +11,7 @@ import { createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ -import { - NAVIGATION_POST_KIND, - NAVIGATION_POST_POST_TYPE, -} from '../utils/constants'; +import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE } from '../constants'; import { buildNavigationPostId } from './utils'; diff --git a/packages/edit-navigation/src/store/test/actions.js b/packages/edit-navigation/src/store/test/actions.js index 9f24671057feaf..d3e1bbf4a8ab5b 100644 --- a/packages/edit-navigation/src/store/test/actions.js +++ b/packages/edit-navigation/src/store/test/actions.js @@ -12,6 +12,7 @@ import { resolveMenuItems, getMenuItemToClientIdMapping, dispatch, + select, apiFetch, } from '../controls'; import { menuItemsQuery, computeCustomizedAttribute } from '../utils'; @@ -24,7 +25,7 @@ jest.mock( '../utils', () => { } ); describe( 'createMissingMenuItems', () => { - it( 'create missing menu for navigation block', () => { + it( 'creates a missing menu for navigation block', () => { const post = { id: 'navigation-post-1', slug: 'navigation-post-1', @@ -100,7 +101,7 @@ describe( 'createMissingMenuItems', () => { expect( action.next( [] ).done ).toBe( true ); } ); - it( 'create missing menu for navigation link block', () => { + it( 'creates a missing menu for navigation link block', () => { const post = { id: 'navigation-post-1', slug: 'navigation-post-1', @@ -227,7 +228,7 @@ describe( 'createMissingMenuItems', () => { } ); describe( 'saveNavigationPost', () => { - it( 'should convert all the blocks into menu items and batch save them at once', () => { + it( 'converts all the blocks into menu items and batch save them at once', () => { const post = { id: 'navigation-post-1', slug: 'navigation-post-1', @@ -326,8 +327,11 @@ describe( 'saveNavigationPost', () => { expect( action.next( mapping ).value ).toEqual( dispatch( 'core', 'saveEditedEntityRecord', 'root', 'menu', 1 ) ); - expect( action.next( { id: 1 } ).value ).toEqual( + select( 'core', 'getLastEntitySaveError', 'root', 'menu', 1 ) + ); + + expect( action.next().value ).toEqual( apiFetch( { path: '/__experimental/customizer-nonces/get-save-nonce', } ) @@ -361,7 +365,7 @@ describe( 'saveNavigationPost', () => { ); } ); - it( 'should handle error and show error notifications', () => { + it( 'handles an error from the batch API and show error notifications', () => { const post = { id: 'navigation-post-1', slug: 'navigation-post-1', @@ -461,6 +465,10 @@ describe( 'saveNavigationPost', () => { dispatch( 'core', 'saveEditedEntityRecord', 'root', 'menu', 1 ) ); + expect( action.next( { id: 1 } ).value ).toEqual( + select( 'core', 'getLastEntitySaveError', 'root', 'menu', 1 ) + ); + expect( action.next().value ).toEqual( apiFetch( { path: '/__experimental/customizer-nonces/get-save-nonce', @@ -483,11 +491,130 @@ describe( 'saveNavigationPost', () => { ) ); - expect( action.next( { success: false } ).value ).toEqual( + expect( + action.next( { success: false, data: { message: 'Test Message' } } ) + .value + ).toEqual( + dispatch( + noticesStore, + 'createErrorNotice', + __( "Unable to save: 'Test Message'" ), + { + type: 'snackbar', + } + ) + ); + } ); + + it( 'handles an error from the entity and show error notifications', () => { + const post = { + id: 'navigation-post-1', + slug: 'navigation-post-1', + type: 'page', + meta: { + menuId: 1, + }, + blocks: [ + { + attributes: { showSubmenuIcon: true }, + clientId: 'navigation-block-client-id', + innerBlocks: [ + { + attributes: { + label: 'wp.org', + opensInNewTab: false, + url: 'http://wp.org', + className: '', + rel: '', + description: '', + title: '', + }, + clientId: 'navigation-link-block-client-id-1', + innerBlocks: [], + isValid: true, + name: 'core/navigation-link', + }, + { + attributes: { + label: 'wp.com', + opensInNewTab: false, + url: 'http://wp.com', + className: '', + rel: '', + description: '', + title: '', + }, + clientId: 'navigation-link-block-client-id-2', + innerBlocks: [], + isValid: true, + name: 'core/navigation-link', + }, + ], + isValid: true, + name: 'core/navigation', + }, + ], + }; + + const menuItems = [ + { + id: 100, + title: { + raw: 'wp.com', + rendered: 'wp.com', + }, + url: 'http://wp.com', + menu_order: 1, + menus: [ 1 ], + classes: [], + xfn: [], + description: '', + attr_title: '', + }, + { + id: 101, + title: { + raw: 'wp.org', + rendered: 'wp.org', + }, + url: 'http://wp.org', + menu_order: 2, + menus: [ 1 ], + classes: [], + xfn: [], + description: '', + attr_title: '', + }, + ]; + + const mapping = { + 100: 'navigation-link-block-client-id-1', + 101: 'navigation-link-block-client-id-2', + }; + + const action = saveNavigationPost( post ); + + expect( action.next().value ).toEqual( + resolveMenuItems( post.meta.menuId ) + ); + + expect( action.next( menuItems ).value ).toEqual( + getMenuItemToClientIdMapping( post.id ) + ); + + expect( action.next( mapping ).value ).toEqual( + dispatch( 'core', 'saveEditedEntityRecord', 'root', 'menu', 1 ) + ); + + expect( action.next().value ).toEqual( + select( 'core', 'getLastEntitySaveError', 'root', 'menu', 1 ) + ); + + expect( action.next( { message: 'Test Message 2' } ).value ).toEqual( dispatch( noticesStore, 'createErrorNotice', - __( 'There was an error.' ), + __( "Unable to save: 'Test Message 2'" ), { type: 'snackbar', } diff --git a/packages/edit-navigation/src/store/test/resolvers.js b/packages/edit-navigation/src/store/test/resolvers.js index af3e0ef2b6cd1c..d5da62efa4a7e9 100644 --- a/packages/edit-navigation/src/store/test/resolvers.js +++ b/packages/edit-navigation/src/store/test/resolvers.js @@ -6,7 +6,7 @@ import { resolveMenuItems, dispatch } from '../controls'; import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE, -} from '../../utils/constants'; +} from '../../constants'; import { buildNavigationPostId } from '../utils'; diff --git a/packages/edit-navigation/src/store/test/selectors.js b/packages/edit-navigation/src/store/test/selectors.js index 67b7a25f932cb8..40abbb893a8a17 100644 --- a/packages/edit-navigation/src/store/test/selectors.js +++ b/packages/edit-navigation/src/store/test/selectors.js @@ -9,7 +9,7 @@ import { import { NAVIGATION_POST_KIND, NAVIGATION_POST_POST_TYPE, -} from '../../utils/constants'; +} from '../../constants'; import { buildNavigationPostId } from '../utils'; diff --git a/packages/edit-navigation/src/style.scss b/packages/edit-navigation/src/style.scss index db7580a1233833..5667e52290c76f 100644 --- a/packages/edit-navigation/src/style.scss +++ b/packages/edit-navigation/src/style.scss @@ -1,5 +1,5 @@ $navigation-editor-width: 420px; -$navigation-editor-spacing-top: $grid-unit-40 * 2; +$navigation-editor-spacing-top: $grid-unit-50 * 2; *, *::before, @@ -8,6 +8,7 @@ $navigation-editor-spacing-top: $grid-unit-40 * 2; } @import "./components/add-menu/style.scss"; +@import "../../interface/src/style.scss"; @import "./components/editor/style.scss"; @import "./components/error-boundary/style.scss"; @import "./components/header/style.scss"; diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 3fcf5c7cb74a34..2d5f7af3c145d9 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -23,7 +23,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss index 8f74bb8031a2d4..41e97f4d402bcf 100644 --- a/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss +++ b/packages/edit-post/src/components/header/fullscreen-mode-close/style.scss @@ -1,3 +1,6 @@ +// Developer notes: these rules are duplicated for the site editor. +// They need to be updated in both places. + .edit-post-fullscreen-mode-close.has-icon { // Do not show the toolbar icon on small screens, // when Fullscreen Mode is not an option in the "More" menu. @@ -13,21 +16,43 @@ border-radius: 0; height: $header-height; width: $header-height; - - &:hover { - background: #32373d; // WP-admin light-gray. - } + position: relative; &:active { color: $white; } &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 ($border-width-focus + 1px) $white; + box-shadow: none; + } + + &::before { + transition: box-shadow 0.1s ease; + @include reduce-motion("transition"); + content: ""; + display: block; + position: absolute; + top: 9px; + right: 9px; + bottom: 9px; + left: 9px; + border-radius: $radius-block-ui + $border-width + $border-width; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) #23282e; + } + + // Hover color. + &:hover::before { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $gray-700; + } + + // Lightened spot color focus. + &:focus::before { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) rgba($white, 0.1), inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } } .edit-post-fullscreen-mode-close_site-icon { - width: 36px; + width: $button-size; + border-radius: $radius-block-ui; } diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index f2acd3ac387350..0c9da0ae76a82e 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -94,7 +94,9 @@ function Layout( { styles } ) { showIconLabels, hasReducedUI, showBlockBreadcrumbs, + supportsLayout, } = useSelect( ( select ) => { + const editorSettings = select( 'core/editor' ).getEditorSettings(); return { hasFixedToolbar: select( editPostStore ).isFeatureActive( 'fixedToolbar' @@ -112,8 +114,8 @@ function Layout( { styles } ) { ), isInserterOpened: select( editPostStore ).isInserterOpened(), mode: select( editPostStore ).getEditorMode(), - isRichEditingEnabled: select( 'core/editor' ).getEditorSettings() - .richEditingEnabled, + isRichEditingEnabled: editorSettings.richEditingEnabled, + supportsLayout: editorSettings.supportsLayout, hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), previousShortcut: select( keyboardShortcutsStore @@ -139,6 +141,7 @@ function Layout( { styles } ) { 'has-fixed-toolbar': hasFixedToolbar, 'has-metaboxes': hasActiveMetaboxes, 'show-icon-labels': showIconLabels, + 'supports-layout': supportsLayout, } ); const openSidebarPanel = () => openGeneralSidebar( diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index e2c3a144674b5d..9a9c033b55b0c2 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -115,3 +115,112 @@ height: 100%; } } + +// Depreacted style needed for the block widths and alignments. +// for themes that don't support the new layout (theme.json) +.edit-post-layout:not(.supports-layout) { + .wp-block { + max-width: $content-width; + margin-left: auto; + margin-right: auto; + + &[data-align="wide"] { + max-width: $wide-content-width; + } + + &[data-align="full"] { + max-width: none; + } + + // Alignments. + &[data-align="left"], + &[data-align="right"] { + width: 100%; + + // When images are floated, the block itself should collapse to zero height. + height: 0; + + &::before { + content: none; + } + } + + // Left. + &[data-align="left"] > * { + /*!rtl:begin:ignore*/ + float: left; + margin-right: 2em; + /*!rtl:end:ignore*/ + } + + // Right. + &[data-align="right"] > * { + /*!rtl:begin:ignore*/ + float: right; + margin-left: 2em; + /*!rtl:end:ignore*/ + } + + // Wide and full-wide. + &[data-align="full"], + &[data-align="wide"] { + clear: both; + } + + } + + // Full Width Blocks + // specificity required to only target immediate child Blocks of a Group + .wp-block-group > [data-align="full"] { + margin-left: auto; + margin-right: auto; + } + + // Full Width Blocks with a background (ie: has padding) + .wp-block-group.has-background > [data-align="full"] { + // note: using position `left` causes hoz scrollbars so + // we opt to use margin instead + // the 30px matches the hoz padding applied in `theme.scss` + // added when the Block has a background set + margin-left: -30px; + + // 60px here is x2 the hoz padding from `theme.scss` added when + // the Block has a background set + // note: also duplicated below for full width Groups + width: calc(100% + 60px); + } + + /** + * Group: Full Width Alignment + */ + [data-align="full"] .wp-block-group { + + // Non-full Width Blocks + // specificity required to only target immediate child Blocks of Group + > .wp-block { + padding-left: $block-padding; + padding-right: $block-padding; + + @include break-small() { + padding-left: 0; + padding-right: 0; + } + } + + // Full Width Blocks + // specificity required to only target immediate child Blocks of Group + > [data-align="full"] { + padding-right: 0; + padding-left: 0; + left: 0; + width: 100%; + max-width: none; + } + + // Full Width Blocks with a background (ie: has padding) + // note: also duplicated above for all Group widths + &.has-background > [data-align="full"] { + width: calc(100% + 60px); + } + } +} diff --git a/packages/edit-post/src/components/manage-blocks-modal/style.scss b/packages/edit-post/src/components/manage-blocks-modal/style.scss index 3f722714a5333d..a63bbdf26c127e 100644 --- a/packages/edit-post/src/components/manage-blocks-modal/style.scss +++ b/packages/edit-post/src/components/manage-blocks-modal/style.scss @@ -121,9 +121,9 @@ .edit-post-manage-blocks-modal__results { height: 100%; overflow: auto; - margin-left: -$grid-unit-30; - margin-right: -$grid-unit-30; - padding-left: $grid-unit-30; - padding-right: $grid-unit-30; + margin-left: -$grid-unit-40; + margin-right: -$grid-unit-40; + padding-left: $grid-unit-40; + padding-right: $grid-unit-40; border-top: $border-width solid $gray-300; } diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 76891cd8449b92..e452498fbd98d8 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -8,6 +8,7 @@ import { import { WritingFlow, BlockList, + store as blockEditorStore, __unstableUseBlockSelectionClearer as useBlockSelectionClearer, __unstableUseTypewriter as useTypewriter, __unstableUseClipboardHandler as useClipboardHandler, @@ -16,6 +17,8 @@ import { __experimentalUseResizeCanvas as useResizeCanvas, __unstableUseCanvasClickRedirect as useCanvasClickRedirect, __unstableEditorStyles as EditorStyles, + __experimentalUseEditorFeature as useEditorFeature, + __experimentalLayoutStyle as LayoutStyle, } from '@wordpress/block-editor'; import { Popover } from '@wordpress/components'; import { useRef } from '@wordpress/element'; @@ -44,6 +47,10 @@ export default function VisualEditor( { styles } ) { ( select ) => select( editPostStore ).hasMetaBoxes(), [] ); + const themeSupportsLayout = useSelect( ( select ) => { + const { getSettings } = select( blockEditorStore ); + return getSettings().supportsLayout; + }, [] ); const desktopCanvasStyles = { height: '100%', // Add a constant padding for the typewritter effect. When typing at the @@ -51,6 +58,12 @@ export default function VisualEditor( { styles } ) { paddingBottom: hasMetaBoxes ? null : '40vh', }; const resizedCanvasStyles = useResizeCanvas( deviceType ); + const defaultLayout = useEditorFeature( 'layout' ); + const { contentSize, wideSize } = defaultLayout || {}; + const alignments = + contentSize || wideSize + ? [ 'wide', 'full' ] + : [ 'left', 'center', 'right' ]; const mergedRefs = useMergeRefs( [ ref, @@ -63,6 +76,12 @@ export default function VisualEditor( { styles } ) { return ( <div className="edit-post-visual-editor"> + { themeSupportsLayout && ( + <LayoutStyle + selector=".edit-post-visual-editor__post-title-wrapper, .block-editor-block-list__layout.is-root-container" + layout={ defaultLayout } + /> + ) } <EditorStyles styles={ styles } /> <VisualEditorGlobalKeyboardShortcuts /> <Popover.Slot name="block-toolbar" /> @@ -77,7 +96,19 @@ export default function VisualEditor( { styles } ) { <PostTitle /> </div> ) } - <BlockList /> + <BlockList + __experimentalLayout={ + themeSupportsLayout + ? { + type: 'default', + // Find a way to inject this in the support flag code (hooks). + alignments: themeSupportsLayout + ? alignments + : undefined, + } + : undefined + } + /> </WritingFlow> </div> <__experimentalBlockSettingsMenuFirstItem> diff --git a/packages/edit-post/src/components/welcome-guide/style.scss b/packages/edit-post/src/components/welcome-guide/style.scss index f76ae8bd9536c3..1697a694b6291e 100644 --- a/packages/edit-post/src/components/welcome-guide/style.scss +++ b/packages/edit-post/src/components/welcome-guide/style.scss @@ -6,6 +6,7 @@ &__image { background: #00a0d2; height: 240px; + margin: 0 0 $grid-unit-20; &__prm-r { display: none; @@ -26,7 +27,7 @@ font-family: $default-font; font-size: 24px; line-height: 1.4; - margin: 0 0 $grid-unit-20 0; + margin: $grid-unit-20 0 $grid-unit-20 0; padding: 0 $grid-unit-40; } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 11a82a1e899d34..3f0fa4d1105c7a 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -82,10 +82,6 @@ function Editor( { const isFSETheme = getEditorSettings().isFSETheme; const isViewable = getPostType( postType )?.viewable ?? false; - // Prefetch and parse patterns. This ensures patterns are loaded and parsed when - // the editor is loaded rather than degrading the performance of the inserter. - select( 'core/block-editor' ).__experimentalGetAllowedPatterns(); - return { hasFixedToolbar: isFeatureActive( 'fixedToolbar' ) || @@ -120,7 +116,7 @@ function Editor( { const editorSettings = useMemo( () => { const result = { - ...omit( settings, [ 'defaultEditorStyles', 'styles' ] ), + ...omit( settings, [ 'styles' ] ), __experimentalPreferredStyleVariations: { value: preferredStyleVariations, onChange: updatePreferredStyleVariations, @@ -167,7 +163,7 @@ function Editor( { ] ); const styles = useMemo( () => { - return hasThemeStyles ? settings.styles : settings.defaultEditorStyles; + return hasThemeStyles ? settings.styles : []; }, [ settings, hasThemeStyles ] ); if ( ! post ) { diff --git a/packages/edit-post/src/plugins/copy-content-menu-item/index.js b/packages/edit-post/src/plugins/copy-content-menu-item/index.js index 01198d4bd57709..11e1da532adc10 100644 --- a/packages/edit-post/src/plugins/copy-content-menu-item/index.js +++ b/packages/edit-post/src/plugins/copy-content-menu-item/index.js @@ -2,46 +2,28 @@ * WordPress dependencies */ import { MenuItem } from '@wordpress/components'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { useCopyOnClick, compose, ifCondition } from '@wordpress/compose'; -import { useRef, useEffect } from '@wordpress/element'; +import { useCopyToClipboard } from '@wordpress/compose'; import { store as noticesStore } from '@wordpress/notices'; +import { store as editorStore } from '@wordpress/editor'; -function CopyContentMenuItem( { createNotice, editedPostContent } ) { - const ref = useRef(); - const hasCopied = useCopyOnClick( ref, editedPostContent ); - - useEffect( () => { - if ( ! hasCopied ) { - return; - } +export default function CopyContentMenuItem() { + const { createNotice } = useDispatch( noticesStore ); + const getText = useSelect( + ( select ) => () => + select( editorStore ).getEditedPostAttribute( 'content' ), + [] + ); + function onSuccess() { createNotice( 'info', __( 'All content copied.' ), { isDismissible: true, type: 'snackbar', } ); - }, [ hasCopied ] ); - - return ( - <MenuItem ref={ ref }> - { hasCopied ? __( 'Copied!' ) : __( 'Copy all content' ) } - </MenuItem> - ); -} + } -export default compose( - withSelect( ( select ) => ( { - editedPostContent: select( 'core/editor' ).getEditedPostAttribute( - 'content' - ), - } ) ), - withDispatch( ( dispatch ) => { - const { createNotice } = dispatch( noticesStore ); + const ref = useCopyToClipboard( getText, onSuccess ); - return { - createNotice, - }; - } ), - ifCondition( ( { editedPostContent } ) => editedPostContent.length > 0 ) -)( CopyContentMenuItem ); + return <MenuItem ref={ ref }>{ __( 'Copy all content' ) }</MenuItem>; +} diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 6c13a6a8d4ad24..22ed97d362d86c 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -100,5 +100,4 @@ body.block-editor-page { } } -@include default-block-widths(); @include wordpress-admin-schemes(); diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 624db48a92c904..c24367cff3fc56 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -23,7 +23,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", diff --git a/packages/edit-site/src/components/block-editor/index.js b/packages/edit-site/src/components/block-editor/index.js index fa07ae90b70faf..a119fc490f8769 100644 --- a/packages/edit-site/src/components/block-editor/index.js +++ b/packages/edit-site/src/components/block-editor/index.js @@ -103,7 +103,14 @@ export default function BlockEditor( { setIsInserterOpen } ) { > <DropZoneProvider> <WritingFlow> - <BlockList className="edit-site-block-editor__block-list" /> + <BlockList + className="edit-site-block-editor__block-list" + __experimentalLayout={ { + type: 'default', + // At the root level of the site editor, no alignments should be allowed. + alignments: [], + } } + /> </WritingFlow> </DropZoneProvider> </Iframe> diff --git a/packages/edit-site/src/components/editor/global-styles-provider.js b/packages/edit-site/src/components/editor/global-styles-provider.js index 9480b849e821ef..bba4af1ea54b8e 100644 --- a/packages/edit-site/src/components/editor/global-styles-provider.js +++ b/packages/edit-site/src/components/editor/global-styles-provider.js @@ -143,7 +143,17 @@ export default function GlobalStylesProvider( { children, baseStyles } ) { const contexts = useMemo( () => getContexts( blockTypes ), [ blockTypes ] ); const { userStyles, mergedStyles } = useMemo( () => { - let newUserStyles = content ? JSON.parse( content ) : EMPTY_CONTENT; + let newUserStyles; + try { + newUserStyles = content ? JSON.parse( content ) : EMPTY_CONTENT; + } catch ( e ) { + /* eslint-disable no-console */ + console.error( 'User data is not JSON' ); + console.error( e ); + /* eslint-enable no-console */ + newUserStyles = EMPTY_CONTENT; + } + // It is very important to verify if the flag isGlobalStylesUserThemeJSON is true. // If it is not true the content was not escaped and is not safe. if ( ! newUserStyles.isGlobalStylesUserThemeJSON ) { diff --git a/packages/edit-site/src/components/editor/global-styles-renderer.js b/packages/edit-site/src/components/editor/global-styles-renderer.js index 5e7b68d998d671..b8bbc57cfe1ed1 100644 --- a/packages/edit-site/src/components/editor/global-styles-renderer.js +++ b/packages/edit-site/src/components/editor/global-styles-renderer.js @@ -99,19 +99,14 @@ function flattenTree( input = {}, prefix, token ) { /** * Transform given style tree into a set of style declarations. * - * @param {Object} blockSupports What styles the block supports. * @param {Object} blockStyles Block styles. * * @return {Array} An array of style declarations. */ -function getBlockStylesDeclarations( blockSupports, blockStyles = {} ) { +function getBlockStylesDeclarations( blockStyles = {} ) { return reduce( STYLE_PROPERTY, ( declarations, { value, properties }, key ) => { - if ( ! blockSupports.includes( key ) ) { - return declarations; - } - if ( !! properties ) { properties.forEach( ( prop ) => { if ( ! get( blockStyles, [ ...value, prop ], false ) ) { @@ -171,7 +166,6 @@ export default ( blockData, tree, type = 'all' ) => { } if ( type === 'all' || type === 'blockStyles' ) { const blockStyleDeclarations = getBlockStylesDeclarations( - blockData[ context ].supports, tree?.styles?.[ context ] ); diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 9b5d8a0962d4c2..75fa489bec2992 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useEffect, useState, useMemo, useCallback } from '@wordpress/element'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { AsyncModeProvider, useSelect, useDispatch } from '@wordpress/data'; import { SlotFillProvider, DropZoneProvider, @@ -43,7 +43,6 @@ const interfaceLabels = { function Editor( { initialSettings } ) { const { - isFullscreenActive, isInserterOpen, isListViewOpen, sidebarIsOpened, @@ -55,7 +54,6 @@ function Editor( { initialSettings } ) { isNavigationOpen, } = useSelect( ( select ) => { const { - isFeatureActive, isInserterOpened, isListViewOpened, getSettings, @@ -67,15 +65,10 @@ function Editor( { initialSettings } ) { const postType = getEditedPostType(); const postId = getEditedPostId(); - // Prefetch and parse patterns. This ensures patterns are loaded and parsed when - // the editor is loaded rather than degrading the performance of the inserter. - select( 'core/block-editor' ).__experimentalGetAllowedPatterns(); - // The currently selected entity to display. Typically template or template part. return { isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), - isFullscreenActive: isFeatureActive( 'fullscreenMode' ), sidebarIsOpened: !! select( interfaceStore ).getActiveComplementaryArea( editSiteStore.name ), @@ -161,7 +154,11 @@ function Editor( { initialSettings } ) { return <InserterSidebar />; } if ( isListViewOpen ) { - return <ListViewSidebar />; + return ( + <AsyncModeProvider value="true"> + <ListViewSidebar /> + </AsyncModeProvider> + ); } return null; }; @@ -169,7 +166,7 @@ function Editor( { initialSettings } ) { return ( <> <URLQueryController /> - <FullscreenMode isActive={ isFullscreenActive } /> + <FullscreenMode isActive /> <UnsavedChangesWarning /> <SlotFillProvider> <DropZoneProvider> diff --git a/packages/edit-site/src/components/header/more-menu/index.js b/packages/edit-site/src/components/header/more-menu/index.js index 7a2d6b4bbcc30c..b06193fd853ae2 100644 --- a/packages/edit-site/src/components/header/more-menu/index.js +++ b/packages/edit-site/src/components/header/more-menu/index.js @@ -49,15 +49,6 @@ const MoreMenu = () => ( 'Spotlight mode deactivated' ) } /> - <FeatureToggle - feature="fullscreenMode" - label={ __( 'Fullscreen mode' ) } - info={ __( 'Work without distraction' ) } - messageActivated={ __( 'Fullscreen mode activated' ) } - messageDeactivated={ __( - 'Fullscreen mode deactivated' - ) } - /> <ActionItem.Slot name="core/edit-site/plugin-more-menu" label={ __( 'Plugins' ) } diff --git a/packages/edit-site/src/components/header/style.scss b/packages/edit-site/src/components/header/style.scss index 322fd25f86fe00..859c26d8c1e456 100644 --- a/packages/edit-site/src/components/header/style.scss +++ b/packages/edit-site/src/components/header/style.scss @@ -9,14 +9,11 @@ $header-toolbar-min-width: 335px; width: 100%; justify-content: space-between; - @include break-medium() { - body.is-fullscreen-mode & { - padding-left: 60px; - transition: padding-left 20ms linear; - transition-delay: 80ms; - @include reduce-motion("transition"); - } - + body.is-fullscreen-mode & { + padding-left: 60px; + transition: padding-left 20ms linear; + transition-delay: 80ms; + @include reduce-motion("transition"); } .edit-site-header_start, @@ -46,6 +43,7 @@ body.is-navigation-sidebar-open { padding-left: 0; transition: padding-left 20ms linear; transition-delay: 0ms; + @include reduce-motion("transition"); } } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js index ea6c799bed779c..9c09c0aebf48d6 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/constants.js @@ -51,6 +51,12 @@ export const TEMPLATES_NEW_OPTIONS = [ 'index', ]; +export const TEMPLATE_OVERRIDES = { + singular: [ 'single', 'page' ], + index: [ 'archive', '404', 'search', 'singular', 'home' ], + home: [ 'front-page' ], +}; + export const MENU_ROOT = 'root'; export const MENU_CONTENT_CATEGORIES = 'content-categories'; export const MENU_CONTENT_PAGES = 'content-pages'; diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js index 0e6759932053fb..749660921ba5f1 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/content-navigation-item.js @@ -33,7 +33,9 @@ export default function ContentNavigationItem( { item } ) { }, [ isPreviewVisible ] ); - const { setPage } = useDispatch( editSiteStore ); + const { setPage, setIsNavigationPanelOpened } = useDispatch( + editSiteStore + ); const onActivateItem = useCallback( () => { const { type, slug, link, id } = item; @@ -46,6 +48,7 @@ export default function ContentNavigationItem( { item } ) { postId: id, }, } ); + setIsNavigationPanelOpened( false ); }, [ setPage, item ] ); if ( ! item ) { diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js index 88f470ca25e879..4ff4c225a23970 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/index.js @@ -6,9 +6,11 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { usePrevious } from '@wordpress/compose'; +import { store as coreDataStore } from '@wordpress/core-data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useState, useEffect, useRef } from '@wordpress/element'; import { ESCAPE } from '@wordpress/keycodes'; -import { useSelect, useDispatch } from '@wordpress/data'; /** * Internal dependencies @@ -22,7 +24,7 @@ const NavigationPanel = ( { isOpen } ) => { const [ contentActiveMenu, setContentActiveMenu ] = useState( MENU_ROOT ); const { templatesActiveMenu, siteTitle } = useSelect( ( select ) => { const { getNavigationPanelActiveMenu } = select( editSiteStore ); - const { getEntityRecord } = select( 'core' ); + const { getEntityRecord } = select( coreDataStore ); const siteData = getEntityRecord( 'root', '__unstableBase', undefined ) || {}; @@ -42,6 +44,15 @@ const NavigationPanel = ( { isOpen } ) => { } }, [ templatesActiveMenu ] ); + // Resets the content menu to its root whenever the navigation opens to avoid + // having it stuck on a sub-menu, interfering with the normal navigation behavior. + const prevIsOpen = usePrevious( isOpen ); + useEffect( () => { + if ( contentActiveMenu !== MENU_ROOT && isOpen && ! prevIsOpen ) { + setContentActiveMenu( MENU_ROOT ); + } + }, [ contentActiveMenu, isOpen ] ); + const { setIsNavigationPanelOpened } = useDispatch( editSiteStore ); const closeOnEscape = ( event ) => { @@ -69,12 +80,10 @@ const NavigationPanel = ( { isOpen } ) => { </div> <div className="edit-site-navigation-panel__scroll-container"> - { ( contentActiveMenu === MENU_ROOT || - templatesActiveMenu !== MENU_ROOT ) && ( + { contentActiveMenu === MENU_ROOT && ( <TemplatesNavigation /> ) } - { ( templatesActiveMenu === MENU_ROOT || - contentActiveMenu !== MENU_ROOT ) && ( + { templatesActiveMenu === MENU_ROOT && ( <ContentNavigation onActivateMenu={ setContentActiveMenu } /> diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js index 23f81bb710d9a1..bd9faf04df668a 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/content-posts.js @@ -61,7 +61,6 @@ export default function ContentPostsMenu() { type: 'page', path: '/', context: { - query: { categoryIds: [] }, queryContext: { page: 1 }, }, } ); diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js index 8cd861e8c6e035..1119cc7aa1eccc 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/menus/templates.js @@ -25,64 +25,15 @@ import { MENU_TEMPLATES_PAGES, MENU_TEMPLATES_POSTS, MENU_TEMPLATES_UNUSED, - TEMPLATES_GENERAL, - TEMPLATES_PAGES_PREFIXES, - TEMPLATES_POSTS_PREFIXES, - TEMPLATES_TOP_LEVEL, } from '../constants'; import NewTemplateDropdown from '../new-template-dropdown'; import TemplateNavigationItem from '../template-navigation-item'; import SearchResults from '../search-results'; import TemplatesSubMenu from './templates-sub'; -import { isTemplateSuperseded } from '../template-hierarchy'; - -function getTemplateLocation( template ) { - const { slug } = template; - - const isTopLevelTemplate = TEMPLATES_TOP_LEVEL.includes( slug ); - if ( isTopLevelTemplate ) { - return MENU_TEMPLATES; - } - - const isGeneralTemplate = TEMPLATES_GENERAL.includes( slug ); - if ( isGeneralTemplate ) { - return MENU_TEMPLATES_GENERAL; - } - - const isPostsTemplate = TEMPLATES_POSTS_PREFIXES.some( ( prefix ) => - slug.startsWith( prefix ) - ); - if ( isPostsTemplate ) { - return MENU_TEMPLATES_POSTS; - } - - const isPagesTemplate = TEMPLATES_PAGES_PREFIXES.some( ( prefix ) => - slug.startsWith( prefix ) - ); - if ( isPagesTemplate ) { - return MENU_TEMPLATES_PAGES; - } - - return MENU_TEMPLATES_GENERAL; -} - -function getUnusedTemplates( templates, showOnFront ) { - const unusedTemplates = []; - - const templateSlugs = map( templates, 'slug' ); - const supersededTemplates = templates.filter( ( { slug } ) => - isTemplateSuperseded( slug, templateSlugs, showOnFront ) - ); - - return [ ...supersededTemplates, ...unusedTemplates ]; -} - -function getTemplatesLocationMap( templates ) { - return templates.reduce( ( obj, template ) => { - obj[ template.slug ] = getTemplateLocation( template ); - return obj; - }, {} ); -} +import { + getTemplatesLocationMap, + getUnusedTemplates, +} from '../template-hierarchy'; export default function TemplatesMenu() { const [ search, setSearch ] = useState( '' ); diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss index dd58548e8b6933..eec2ca5c784a93 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/style.scss @@ -13,15 +13,25 @@ } } -.edit-site-navigation-panel.is-open { - width: $nav-sidebar-width; -} - .edit-site-navigation-panel__inner { position: relative; width: $nav-sidebar-width; height: 100%; overflow: hidden; + // Inner container is hidden to remove menu from the accessibility tree when not visible. + // Setting visibility here (rather than on the parent container) preserves the slide transition. + visibility: hidden; + // Transition settings should match parent container. + transition: visibility 100ms linear; + @include reduce-motion("transition"); +} + +.edit-site-navigation-panel.is-open { + width: $nav-sidebar-width; + + .edit-site-navigation-panel__inner { + visibility: visible; + } } .edit-site-navigation-panel__site-title-container { @@ -52,7 +62,6 @@ } .edit-site-navigation-panel__back-to-dashboard.components-button.is-tertiary { - border-radius: 0; height: $button-size; margin-top: $grid-unit-30; padding: $grid-unit $grid-unit-20 $grid-unit $grid-unit; diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-hierarchy.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-hierarchy.js index 29dba28b9b5aad..2f8d511ae10eae 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-hierarchy.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-hierarchy.js @@ -1,8 +1,22 @@ -const TEMPLATE_OVERRIDES = { - singular: [ 'single', 'page' ], - index: [ 'archive', '404', 'search', 'singular', 'home' ], - home: [ 'front-page' ], -}; +/** + * External dependencies + */ +import { map } from 'lodash'; + +/** + * Internal dependencies + */ +import { + MENU_TEMPLATES, + MENU_TEMPLATES_GENERAL, + MENU_TEMPLATES_PAGES, + MENU_TEMPLATES_POSTS, + TEMPLATE_OVERRIDES, + TEMPLATES_GENERAL, + TEMPLATES_PAGES_PREFIXES, + TEMPLATES_POSTS_PREFIXES, + TEMPLATES_TOP_LEVEL, +} from './constants'; export function isTemplateSuperseded( slug, existingSlugs, showOnFront ) { if ( ! TEMPLATE_OVERRIDES[ slug ] ) { @@ -21,3 +35,47 @@ export function isTemplateSuperseded( slug, existingSlugs, showOnFront ) { isTemplateSuperseded( overrideSlug, existingSlugs, showOnFront ) ); } + +export function getTemplateLocation( slug ) { + const isTopLevelTemplate = TEMPLATES_TOP_LEVEL.includes( slug ); + if ( isTopLevelTemplate ) { + return MENU_TEMPLATES; + } + + const isGeneralTemplate = TEMPLATES_GENERAL.includes( slug ); + if ( isGeneralTemplate ) { + return MENU_TEMPLATES_GENERAL; + } + + const isPostsTemplate = TEMPLATES_POSTS_PREFIXES.some( ( prefix ) => + slug.startsWith( prefix ) + ); + if ( isPostsTemplate ) { + return MENU_TEMPLATES_POSTS; + } + + const isPagesTemplate = TEMPLATES_PAGES_PREFIXES.some( ( prefix ) => + slug.startsWith( prefix ) + ); + if ( isPagesTemplate ) { + return MENU_TEMPLATES_PAGES; + } + + return MENU_TEMPLATES_GENERAL; +} + +export function getUnusedTemplates( templates, showOnFront ) { + const templateSlugs = map( templates, 'slug' ); + const supersededTemplates = templates.filter( ( { slug } ) => + isTemplateSuperseded( slug, templateSlugs, showOnFront ) + ); + + return supersededTemplates; +} + +export function getTemplatesLocationMap( templates ) { + return templates.reduce( ( obj, template ) => { + obj[ template.slug ] = getTemplateLocation( template.slug ); + return obj; + }, {} ); +} diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-navigation-item.js b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-navigation-item.js index 369a54efcf5139..7521ceb15bb89c 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-navigation-item.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-panel/template-navigation-item.js @@ -27,17 +27,25 @@ export default function TemplateNavigationItem( { item } ) { }, [] ); - const { setTemplate, setTemplatePart } = useDispatch( editSiteStore ); + const { + setTemplate, + setTemplatePart, + setIsNavigationPanelOpened, + } = useDispatch( editSiteStore ); const [ isPreviewVisible, setIsPreviewVisible ] = useState( false ); if ( ! item ) { return null; } - const onActivateItem = () => - 'wp_template' === item.type - ? setTemplate( item.id, item.slug ) - : setTemplatePart( item.id ); + const onActivateItem = () => { + if ( 'wp_template' === item.type ) { + setTemplate( item.id, item.slug ); + } else { + setTemplatePart( item.id ); + } + setIsNavigationPanelOpened( false ); + }; return ( <NavigationItem diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/index.js b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/index.js index 4b53da55fc023f..6134af8565f838 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/index.js @@ -5,6 +5,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { Button, Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { wordpress } from '@wordpress/icons'; +import { store as coreDataStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -12,32 +13,41 @@ import { wordpress } from '@wordpress/icons'; import { store as editSiteStore } from '../../../store'; function NavigationToggle( { icon, isOpen } ) { - const { isActive, isRequestingSiteIcon, siteIconUrl } = useSelect( - ( select ) => { - const { isFeatureActive } = select( editSiteStore ); - const { getEntityRecord } = select( 'core' ); - const { isResolving } = select( 'core/data' ); - const siteData = - getEntityRecord( 'root', '__unstableBase', undefined ) || {}; + const { + isRequestingSiteIcon, + navigationPanelMenu, + siteIconUrl, + } = useSelect( ( select ) => { + const { getCurrentTemplateNavigationPanelSubMenu } = select( + editSiteStore + ); + const { getEntityRecord, isResolving } = select( coreDataStore ); + const siteData = + getEntityRecord( 'root', '__unstableBase', undefined ) || {}; - return { - isActive: isFeatureActive( 'fullscreenMode' ), - isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [ - 'root', - '__unstableBase', - undefined, - ] ), - siteIconUrl: siteData.site_icon_url, - }; - }, - [] - ); + return { + isRequestingSiteIcon: isResolving( 'core', 'getEntityRecord', [ + 'root', + '__unstableBase', + undefined, + ] ), + navigationPanelMenu: getCurrentTemplateNavigationPanelSubMenu(), + siteIconUrl: siteData.site_icon_url, + }; + }, [] ); - const { setIsNavigationPanelOpened } = useDispatch( editSiteStore ); + const { + openNavigationPanelToMenu, + setIsNavigationPanelOpened, + } = useDispatch( editSiteStore ); - if ( ! isActive ) { - return null; - } + const toggleNavigationPanel = () => { + if ( isOpen ) { + setIsNavigationPanelOpened( false ); + return; + } + openNavigationPanelToMenu( navigationPanelMenu ); + }; let buttonIcon = <Icon size="36px" icon={ wordpress } />; @@ -64,7 +74,7 @@ function NavigationToggle( { icon, isOpen } ) { <Button className="edit-site-navigation-toggle__button has-icon" label={ __( 'Toggle navigation' ) } - onClick={ () => setIsNavigationPanelOpened( ! isOpen ) } + onClick={ toggleNavigationPanel } showTooltip > { buttonIcon } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss index e6110e9e9de608..d3ab0f7ac3d5b1 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/style.scss @@ -1,56 +1,65 @@ +// Developer notes: these rules are duplicated for the post editor. +// They need to be updated in both places. + .edit-site-navigation-toggle { align-items: center; background: $gray-900; border-radius: 0; - display: none; + display: flex; position: absolute; z-index: z-index(".edit-site-navigation-toggle"); - height: $header-height + $border-width; // Cover header border + height: $header-height; width: $header-height; - - @include break-medium() { - display: flex; - } - - body.is-navigation-sidebar-open & { - display: flex; - - .edit-site-navigation-toggle__button { - border-bottom: 1px solid transparent; - border-radius: 0; - } - } } .edit-site-navigation-toggle__button { align-items: center; background: $gray-900; + border-radius: 0; color: $white; - height: $header-height + $border-width; // Cover header border + height: $header-height; width: $header-height; z-index: 1; - // Adds a bottom border to match the header toolbar - border-bottom: 1px solid $gray-200; - // Prevents the bottom border from being clipped by border-bottom radius - border-radius: 2px 2px 0 0; - &.has-icon { min-width: $header-height; - &:hover { - background: #32373d; // WP-admin light-gray. - color: $white; - } + &:hover, &:active { color: $white; } + &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), inset 0 0 0 3px $white; + box-shadow: none; + } + + &::before { + transition: box-shadow 0.1s ease; + @include reduce-motion("transition"); + content: ""; + display: block; + position: absolute; + top: 9px; + right: 9px; + bottom: 9px; + left: 9px; + border-radius: $radius-block-ui + $border-width + $border-width; + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) #23282e; + } + + // Hover color. + &:hover::before { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) $gray-700; + } + + // Lightened spot color focus. + &:focus::before { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) rgba($white, 0.1), inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); } } } .edit-site-navigation-toggle__site-icon { - width: 36px; + width: $button-size; + border-radius: $radius-block-ui; } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js index 9043d97690ec62..a85afafc572461 100644 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js +++ b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/index.js @@ -29,11 +29,11 @@ describe( 'NavigationToggle', () => { it( 'should display a user uploaded site icon if it exists', () => { useSelect.mockImplementation( ( cb ) => { return cb( () => ( { - isResolving: () => false, - isFeatureActive: () => true, + getCurrentTemplateNavigationPanelSubMenu: () => 'root', getEntityRecord: () => ( { site_icon_url: 'https://fakeUrl.com', } ), + isResolving: () => false, } ) ); } ); @@ -48,11 +48,11 @@ describe( 'NavigationToggle', () => { it( 'should display a default site icon if no user uploaded site icon exists', () => { useSelect.mockImplementation( ( cb ) => { return cb( () => ( { - isResolving: () => false, - isFeatureActive: () => true, + getCurrentTemplateNavigationPanelSubMenu: () => 'root', getEntityRecord: () => ( { site_icon_url: '', } ), + isResolving: () => false, } ) ); } ); diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index 0deaffc1f64b4d..0dd3d956684e29 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -23,13 +23,14 @@ import { ESCAPE } from '@wordpress/keycodes'; import { store as editSiteStore } from '../../store'; export default function ListViewSidebar() { - const { rootBlocks, selectedBlockClientId } = useSelect( ( select ) => { - const { getSelectedBlockClientId, __unstableGetBlockTree } = select( - blockEditorStore - ); + const { clientIdsTree, selectedBlockClientIds } = useSelect( ( select ) => { + const { + __unstableGetClientIdsTree, + getSelectedBlockClientIds, + } = select( blockEditorStore ); return { - rootBlocks: __unstableGetBlockTree(), - selectedBlockClientId: getSelectedBlockClientId(), + clientIdsTree: __unstableGetClientIdsTree(), + selectedBlockClientIds: getSelectedBlockClientIds(), }; } ); const { setIsListViewOpened } = useDispatch( editSiteStore ); @@ -72,9 +73,9 @@ export default function ListViewSidebar() { ref={ useMergeRefs( [ focusReturnRef, focusOnMountRef ] ) } > <BlockNavigationTree - blocks={ rootBlocks } + blocks={ clientIdsTree } selectBlock={ selectEditorBlock } - selectedBlockClientId={ selectedBlockClientId } + selectedBlockClientIds={ selectedBlockClientIds } showNestedBlocks __experimentalPersistentListViewFeatures /> diff --git a/packages/edit-site/src/components/template-details/index.js b/packages/edit-site/src/components/template-details/index.js index 780277b21e0678..a8896c24ace79b 100644 --- a/packages/edit-site/src/components/template-details/index.js +++ b/packages/edit-site/src/components/template-details/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { Button, __experimentalText as Text } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -31,27 +31,14 @@ export default function TemplateDetails( { template, onClose } ) { return ( <> <div className="edit-site-template-details"> - <Text variant="sectionheading"> - { __( 'Template details' ) } - </Text> - - { title && ( - <Text variant="body"> - { sprintf( - /* translators: %s: Name of the template. */ - __( 'Name: %s' ), - title - ) } - </Text> - ) } + <Text variant="subtitle">{ title }</Text> { description && ( - <Text variant="body"> - { sprintf( - /* translators: %s: Description of the template. */ - __( 'Description: %s' ), - description - ) } + <Text + variant="body" + className="edit-site-template-details__description" + > + { description } </Text> ) } </div> diff --git a/packages/edit-site/src/components/template-details/style.scss b/packages/edit-site/src/components/template-details/style.scss index 873bf88c0d6d9b..e56595039a37a7 100644 --- a/packages/edit-site/src/components/template-details/style.scss +++ b/packages/edit-site/src/components/template-details/style.scss @@ -6,6 +6,10 @@ } } +.edit-site-template-details__description { + color: $gray-700; +} + .edit-site-template-details__show-all-button.components-button { display: block; background: $gray-900; diff --git a/packages/edit-site/src/store/defaults.js b/packages/edit-site/src/store/defaults.js index b7f95f64d6cfb8..d3a1116465d978 100644 --- a/packages/edit-site/src/store/defaults.js +++ b/packages/edit-site/src/store/defaults.js @@ -1,5 +1,3 @@ export const PREFERENCES_DEFAULTS = { - features: { - fullscreenMode: true, - }, + features: {}, }; diff --git a/packages/edit-site/src/store/index.js b/packages/edit-site/src/store/index.js index d183bcaca8e33b..593726c829f0c0 100644 --- a/packages/edit-site/src/store/index.js +++ b/packages/edit-site/src/store/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { createReduxStore, register } from '@wordpress/data'; +import { createReduxStore, registerStore } from '@wordpress/data'; import { controls } from '@wordpress/data-controls'; /** @@ -22,4 +22,6 @@ export const storeConfig = { export const store = createReduxStore( STORE_NAME, storeConfig ); -register( store ); +// Once we build a more generic persistence plugin that works across types of stores +// we'd be able to replace this with a register call. +registerStore( STORE_NAME, storeConfig ); diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index ab39225a364c8a..8543eb233fe035 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -1,15 +1,30 @@ /** * External dependencies */ -import { get } from 'lodash'; +import { get, map } from 'lodash'; import createSelector from 'rememo'; /** * WordPress dependencies */ +import { store as coreDataStore } from '@wordpress/core-data'; import { createRegistrySelector } from '@wordpress/data'; import { uploadMedia } from '@wordpress/media-utils'; +/** + * Internal dependencies + */ +import { + MENU_ROOT, + MENU_TEMPLATE_PARTS, + MENU_TEMPLATES_UNUSED, + TEMPLATE_PARTS_SUB_MENUS, +} from '../components/navigation-sidebar/navigation-panel/constants'; +import { + getTemplateLocation, + isTemplateSuperseded, +} from '../components/navigation-sidebar/navigation-panel/template-hierarchy'; + /** * Returns whether the given feature is enabled or not. * @@ -41,7 +56,7 @@ export function __experimentalGetPreviewDeviceType( state ) { * @return {Object} Whether the current user can create media or not. */ export const getCanUserCreateMedia = createRegistrySelector( ( select ) => () => - select( 'core' ).canUser( 'create', 'media' ) + select( coreDataStore ).canUser( 'create', 'media' ) ); /** @@ -139,6 +154,64 @@ export function getNavigationPanelActiveMenu( state ) { return state.navigationPanel.menu; } +/** + * Returns the current template or template part's corresponding + * navigation panel's sub menu, to be used with `openNavigationPanelToMenu`. + * + * @param {Object} state Global application state. + * + * @return {string} The current template or template part's sub menu. + */ +export const getCurrentTemplateNavigationPanelSubMenu = createRegistrySelector( + ( select ) => ( state ) => { + const templateType = getEditedPostType( state ); + const templateId = getEditedPostId( state ); + const template = templateId + ? select( coreDataStore ).getEntityRecord( + 'postType', + templateType, + templateId + ) + : null; + + if ( ! template ) { + return MENU_ROOT; + } + + if ( 'wp_template_part' === templateType ) { + return ( + TEMPLATE_PARTS_SUB_MENUS.find( + ( submenu ) => submenu.area === template?.area + )?.menu || MENU_TEMPLATE_PARTS + ); + } + + const templates = select( coreDataStore ).getEntityRecords( + 'postType', + 'wp_template', + { + per_page: -1, + } + ); + const showOnFront = select( coreDataStore ).getEditedEntityRecord( + 'root', + 'site' + ).show_on_front; + + if ( + isTemplateSuperseded( + template.slug, + map( templates, 'slug' ), + showOnFront + ) + ) { + return MENU_TEMPLATES_UNUSED; + } + + return getTemplateLocation( template.slug ); + } +); + /** * Returns the current opened/closed state of the navigation panel. * diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 12d922879e8854..7d05d853d54e83 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { store as coreDataStore } from '@wordpress/core-data'; + /** * Internal dependencies */ @@ -71,7 +76,7 @@ describe( 'selectors', () => { expect( getCanUserCreateMedia() ).toBe( true ); expect( getCanUserCreateMedia.registry.select - ).toHaveBeenCalledWith( 'core' ); + ).toHaveBeenCalledWith( coreDataStore ); expect( canUser ).toHaveBeenCalledWith( 'create', 'media' ); } ); } ); diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 9dff7eb78b9988..42894cd6e4ecf4 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -76,4 +76,3 @@ body.toplevel_page_gutenberg-edit-site { } @include wordpress-admin-schemes(); -@include default-block-widths(); diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 7db119d315afd2..6daa7221451ebf 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -23,7 +23,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/block-editor": "file:../block-editor", "@wordpress/block-library": "file:../block-library", diff --git a/packages/edit-widgets/src/blocks/widget-area/editor.scss b/packages/edit-widgets/src/blocks/widget-area/editor.scss index 8649629c9f8b67..94724b0097c3aa 100644 --- a/packages/edit-widgets/src/blocks/widget-area/editor.scss +++ b/packages/edit-widgets/src/blocks/widget-area/editor.scss @@ -1,5 +1,7 @@ .wp-block[data-type="core/widget-area"] { max-width: $widget-area-width; + margin-left: auto; + margin-right: auto; .components-panel__body > .components-panel__body-title { font-family: $default-font; diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss index ad27a373c9fb52..e9934f3ed53b78 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/style.scss @@ -1,6 +1,9 @@ .edit-widgets-block-editor { position: relative; + // This is the default font that is going to be used in the content of the areas (blocks). + font-family: $default-font; + // The button element easily inherits styles that are meant for the editor style. // These rules enhance the specificity to reduce that inheritance. // This is copied from edit-post/src/components/style.scss but without the padding. diff --git a/packages/editor/README.md b/packages/editor/README.md index ca608c4d579d1e..b8345adb19dd74 100644 --- a/packages/editor/README.md +++ b/packages/editor/README.md @@ -1,6 +1,6 @@ # Editor -Building blocks for WordPress editors. +This module utilizes components from the `@wordpress/block-editor` package. Having an awareness of the concept of a WordPress post, it associates the loading and saving mechanism of the value representing blocks to a post and its content. It also provides various components relevant for working with a post object in the context of an editor (e.g., a post title input component). This package can support editing posts of any post type and does not assume that rendering happens in any particular WordPress screen or layout arrangement. ## Installation @@ -22,7 +22,7 @@ The goal of the editor element is to let the user manipulate the content of thei Such a crucial step is handled by the grammar parsing which takes the serialized content of the post and infers an ordered block list using, preferably, syntax hints present in HTML comments. The editor is initialized with a state representation of the block nodes generated by the parsing of the raw content of a post element: `wp.blocks.parse( post.content.raw )`. -The *visual editor* is thus a component that contains and renders the list of block nodes from the internal state into the page. This removes any trace of imperative handling when it comes to finding a block and manipulating a block. As a matter of fact, the visual editor or the text editor are just two differentโ€”equally validโ€”views of the same representation of state. The internal representation of the post content is updated as blocks are updated and it is serialized back to be saved in `post_content`. +The _visual editor_ is thus a component that contains and renders the list of block nodes from the internal state into the page. This removes any trace of imperative handling when it comes to finding a block and manipulating a block. As a matter of fact, the visual editor or the text editor are just two differentโ€”equally validโ€”views of the same representation of state. The internal representation of the post content is updated as blocks are updated and it is serialized back to be saved in `post_content`. Individual blocks are handled by the `VisualBlock` component, which attaches event handlers and renders the `edit` function of a block definition to the document with the corresponding attributes and local state. The `edit` function is the markup shape of a component while in editing mode. @@ -37,7 +37,7 @@ When returned by your block's `edit` implementation, renders a toolbar of icon b Example: ```js -( function( editor, element ) { +( function ( editor, element ) { var el = element.createElement, BlockControls = editor.BlockControls, AlignmentToolbar = editor.AlignmentToolbar; @@ -45,35 +45,36 @@ Example: function edit( props ) { return [ // Controls: (only visible when block is selected) - el( BlockControls, { key: 'controls' }, + el( + BlockControls, + { key: 'controls' }, el( AlignmentToolbar, { value: props.align, - onChange: function( nextAlign ) { - props.setAttributes( { align: nextAlign } ) - } + onChange: function ( nextAlign ) { + props.setAttributes( { align: nextAlign } ); + }, } ) ), // Block content: (with alignment as attribute) - el( 'p', { key: 'text', style: { textAlign: props.align } }, + el( + 'p', + { key: 'text', style: { textAlign: props.align } }, 'Hello World!' ), ]; } -} )( - window.wp.editor, - window.wp.element -); +} )( window.wp.editor, window.wp.element ); ``` Note in this example that we render `AlignmentToolbar` as a child of the `BlockControls` element. This is another pre-configured component you can use to simplify block text alignment. Alternatively, you can create your own toolbar controls by passing an array of `controls` as a prop to the `BlockControls` component. Each control should be an object with the following properties: -- `icon: string` - Slug of the Dashicon to be shown in the control's toolbar button -- `title: string` - A human-readable localized text to be shown as the tooltip label of the control's button -- `subscript: ?string` - Optional text to be shown adjacent the button icon as subscript (for example, heading levels) -- `isActive: ?boolean` - Whether the control should be considered active / selected. Defaults to `false`. +- `icon: string` - Slug of the Dashicon to be shown in the control's toolbar button +- `title: string` - A human-readable localized text to be shown as the tooltip label of the control's button +- `subscript: ?string` - Optional text to be shown adjacent the button icon as subscript (for example, heading levels) +- `isActive: ?boolean` - Whether the control should be considered active / selected. Defaults to `false`. To create divisions between sets of controls within the same `BlockControls` element, passing `controls` instead as a nested array (array of arrays of objects). A divider will be shown between each set of controls. @@ -83,23 +84,23 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs The following properties (non-exhaustive list) are made available: -- `value: string` - Markup value of the field. Only valid markup is - allowed, as determined by `inline` value and available controls. -- `onChange: Function` - Callback handler when the value of the field changes, - passing the new value as its only argument. -- `placeholder: string` - A text hint to be shown to the user when the field - value is empty, similar to the - [`input` and `textarea` attribute of the same name](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/HTML5_updates#The_placeholder_attribute). -- `multiline: String` - A tag name to use for the tag that should be inserted - when Enter is pressed. For example: `li` in a list block, and `p` for a - block that can contain multiple paragraphs. The default is that only inline - elements are allowed to be used in inserted into the text, effectively - disabling the behavior of the "Enter" key. +- `value: string` - Markup value of the field. Only valid markup is + allowed, as determined by `inline` value and available controls. +- `onChange: Function` - Callback handler when the value of the field changes, + passing the new value as its only argument. +- `placeholder: string` - A text hint to be shown to the user when the field + value is empty, similar to the + [`input` and `textarea` attribute of the same name](https://developer.mozilla.org/en-US/docs/Learn/HTML/Forms/HTML5_updates#The_placeholder_attribute). +- `multiline: String` - A tag name to use for the tag that should be inserted + when Enter is pressed. For example: `li` in a list block, and `p` for a + block that can contain multiple paragraphs. The default is that only inline + elements are allowed to be used in inserted into the text, effectively + disabling the behavior of the "Enter" key. Example: ```js -( function( editor, element ) { +( function ( editor, element ) { var el = element.createElement, RichText = editor.RichText; @@ -110,15 +111,12 @@ Example: return el( RichText, { value: props.attributes.text, - onChange: onChange + onChange: onChange, } ); } // blocks.registerBlockType( ..., { edit: edit, ... } ); -} )( - window.wp.editor, - window.wp.element -); +} )( window.wp.editor, window.wp.element ); ``` <br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p> diff --git a/packages/editor/package.json b/packages/editor/package.json index 68465b509455e8..07de55c5868861 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,7 +1,7 @@ { "name": "@wordpress/editor", "version": "9.26.0", - "description": "Building blocks for WordPress editors.", + "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ @@ -27,7 +27,7 @@ "{src,build,build-module}/{index.js,store/index.js,hooks/**}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/autop": "file:../autop", "@wordpress/blob": "file:../blob", diff --git a/packages/editor/src/components/deprecated.js b/packages/editor/src/components/deprecated.js index 04962bf9dbe957..86f3f0b1d52901 100644 --- a/packages/editor/src/components/deprecated.js +++ b/packages/editor/src/components/deprecated.js @@ -65,6 +65,7 @@ export { default as ServerSideRender } from '@wordpress/server-side-render'; function deprecateComponent( name, Wrapped, staticsToHoist = [] ) { const Component = forwardRef( ( props, ref ) => { deprecated( 'wp.editor.' + name, { + since: '5.3', alternative: 'wp.blockEditor.' + name, } ); @@ -84,6 +85,7 @@ function deprecateComponent( name, Wrapped, staticsToHoist = [] ) { function deprecateFunction( name, func ) { return ( ...args ) => { deprecated( 'wp.editor.' + name, { + since: '5.3', alternative: 'wp.blockEditor.' + name, } ); diff --git a/packages/editor/src/components/error-boundary/index.js b/packages/editor/src/components/error-boundary/index.js index 6040e3b190d887..872f843ab7966f 100644 --- a/packages/editor/src/components/error-boundary/index.js +++ b/packages/editor/src/components/error-boundary/index.js @@ -3,9 +3,19 @@ */ import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { Button, ClipboardButton } from '@wordpress/components'; +import { Button } from '@wordpress/components'; import { select } from '@wordpress/data'; import { Warning } from '@wordpress/block-editor'; +import { useCopyToClipboard } from '@wordpress/compose'; + +function CopyButton( { text, children } ) { + const ref = useCopyToClipboard( text ); + return ( + <Button isSecondary ref={ ref }> + { children } + </Button> + ); +} class ErrorBoundary extends Component { constructor() { @@ -52,20 +62,12 @@ class ErrorBoundary extends Component { <Button key="recovery" onClick={ this.reboot } isSecondary> { __( 'Attempt Recovery' ) } </Button>, - <ClipboardButton - key="copy-post" - text={ this.getContent } - isSecondary - > + <CopyButton key="copy-post" text={ this.getContent }> { __( 'Copy Post Text' ) } - </ClipboardButton>, - <ClipboardButton - key="copy-error" - text={ error.stack } - isSecondary - > + </CopyButton>, + <CopyButton key="copy-error" text={ error.stack }> { __( 'Copy Error' ) } - </ClipboardButton>, + </CopyButton>, ] } > { __( 'The editor has encountered an unexpected error.' ) } diff --git a/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js b/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js index 67397065ab5e60..0b4e6ddbc6146d 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js +++ b/packages/editor/src/components/global-keyboard-shortcuts/visual-editor-shortcuts.js @@ -44,8 +44,8 @@ export default VisualEditorGlobalKeyboardShortcuts; export function EditorGlobalKeyboardShortcuts() { deprecated( 'EditorGlobalKeyboardShortcuts', { + since: '5.2', alternative: 'VisualEditorGlobalKeyboardShortcuts', - plugin: 'Gutenberg', } ); return <VisualEditorGlobalKeyboardShortcuts />; diff --git a/packages/editor/src/components/post-publish-panel/postpublish.js b/packages/editor/src/components/post-publish-panel/postpublish.js index c7aa69bddb20cd..3be45aefa59461 100644 --- a/packages/editor/src/components/post-publish-panel/postpublish.js +++ b/packages/editor/src/components/post-publish-panel/postpublish.js @@ -6,17 +6,13 @@ import { get } from 'lodash'; /** * WordPress dependencies */ -import { - PanelBody, - Button, - ClipboardButton, - TextControl, -} from '@wordpress/components'; +import { PanelBody, Button, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { safeDecodeURIComponent } from '@wordpress/url'; import { decodeEntities } from '@wordpress/html-entities'; +import { useCopyToClipboard } from '@wordpress/compose'; /** * Internal dependencies @@ -43,6 +39,15 @@ const getFuturePostUrl = ( post ) => { return post.permalink_template; }; +function CopyButton( { text, onCopy, children } ) { + const ref = useCopyToClipboard( text, onCopy ); + return ( + <Button isSecondary ref={ ref }> + { children } + </Button> + ); +} + class PostPublishPanelPostpublish extends Component { constructor() { super( ...arguments ); @@ -126,16 +131,11 @@ class PostPublishPanelPostpublish extends Component { { viewPostLabel } </Button> ) } - - <ClipboardButton - isSecondary - text={ link } - onCopy={ this.onCopy } - > + <CopyButton text={ link } onCopy={ this.onCopy }> { this.state.showCopyConfirmation ? __( 'Copied!' ) : __( 'Copy Link' ) } - </ClipboardButton> + </CopyButton> </div> </PanelBody> { children } 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 67546ff6710cd3..1bcc1558ef4d7f 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -175,6 +175,7 @@ function useBlockEditorSettings( settings, hasTemplate ) { 'gradients', 'hasFixedToolbar', 'hasReducedUI', + 'imageDefaultSize', 'imageDimensions', 'imageEditing', 'imageSizes', @@ -186,6 +187,7 @@ function useBlockEditorSettings( settings, hasTemplate ) { 'template', 'templateLock', 'titlePlaceholder', + 'supportsLayout', ] ), mediaUpload: hasUploadPermissions ? mediaUpload : undefined, __experimentalReusableBlocks: reusableBlocks, diff --git a/packages/editor/src/editor-styles.scss b/packages/editor/src/editor-styles.scss deleted file mode 100644 index 0292c8bbc77b74..00000000000000 --- a/packages/editor/src/editor-styles.scss +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Editor Normalization Styles - * - * These are only output in the editor, but styles here are prefixed .editor-styles-wrapper and affect the theming - * of the editor by themes. - */ - -body { - padding: 10px; -} - -// For full-wide blocks, we compensate for these 10px. -.block-editor-block-list__layout.is-root-container > .wp-block[data-align="full"] { - margin-left: -10px; - margin-right: -10px; -} - -.wp-align-wrapper { - max-width: $content-width; - - > .wp-block, - &.wp-align-full { - max-width: none; - } - - &.wp-align-wide { - max-width: $content-width; - } -} - -a { - // This inherits the blue link color set by wp-admin, which is unfortunate. - // However both inherit and unset properties set the color to black. - transition: none; -} - -code, -kbd { - padding: 0; - margin: 0; - background: inherit; - font-size: inherit; - font-family: monospace; -} - -/** - * The following styles revert to the browser defaults overriding the WPAdmin styles. - * This is only needed while the block editor is not being loaded in an iframe. - */ -:root { - font-family: serif; // unfortunately initial doesn't work for font-family. - font-size: initial; - line-height: initial; - color: initial; -} - -p { - font-size: revert; - line-height: revert; - margin: revert; -} - -ul, -ol { - margin: revert; - padding: revert; - - // Remove bottom margin from nested lists. - ul, - ol { - margin: revert; - } - - li { - margin: revert; - } -} - -ul { - list-style-type: revert; -} - -ol { - list-style-type: revert; -} - -ul ul, -ol ul { - list-style-type: revert; -} - -h1, -h2, -h3, -h4, -h5, -h6 { - font-size: revert; - margin: revert; - color: revert; - line-height: revert; - font-weight: revert; -} diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 4bfdf46e81c8c9..ec712f1a9ee9f4 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -114,8 +114,8 @@ export function resetPost( post ) { */ export function* resetAutosave( newAutosave ) { deprecated( 'resetAutosave action (`core/editor` store)', { + since: '5.3', alternative: 'receiveAutosaves action (`core` store)', - plugin: 'Gutenberg', } ); const postId = yield controls.select( STORE_NAME, 'getCurrentPostId' ); @@ -161,6 +161,7 @@ export function __experimentalRequestPostUpdateFinish( options = {} ) { */ export function updatePost() { deprecated( "wp.data.dispatch( 'core/editor' ).updatePost", { + since: '5.7', alternative: 'User the core entitires store instead', } ); return { @@ -649,6 +650,7 @@ export function updateEditorSettings( settings ) { const getBlockEditorAction = ( name ) => function* ( ...args ) { deprecated( "`wp.data.dispatch( 'core/editor' )." + name + '`', { + since: '5.3', alternative: "`wp.data.dispatch( 'core/block-editor' )." + name + '`', } ); diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 5399999001d42e..94125a58f1392f 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -20,6 +20,7 @@ export const PREFERENCES_DEFAULTS = { * disablePostFormats boolean Whether or not the post formats are disabled * allowedMimeTypes array? List of allowed mime types and file extensions * maxUploadFileSize number Maximum upload file size + * supportsLayout boolean Whether the editor supports layouts. */ export const EDITOR_SETTINGS_DEFAULTS = { ...SETTINGS_DEFAULTS, @@ -27,4 +28,5 @@ export const EDITOR_SETTINGS_DEFAULTS = { richEditingEnabled: true, codeEditingEnabled: true, enableCustomFields: false, + supportsLayout: true, }; diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 9c191e9219aa2c..75ca25a169a81d 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -306,6 +306,7 @@ export const getReferenceByDistinctEdits = createRegistrySelector( deprecated( "`wp.data.select( 'core/editor' ).getReferenceByDistinctEdits`", { + since: '5.4', alternative: "`wp.data.select( 'core' ).getReferenceByDistinctEdits`", } @@ -685,9 +686,9 @@ export const isEditedPostAutosaveable = createRegistrySelector( */ export const getAutosave = createRegistrySelector( ( select ) => ( state ) => { deprecated( "`wp.data.select( 'core/editor' ).getAutosave()`", { + since: '5.3', alternative: "`wp.data.select( 'core' ).getAutosave( postType, postId, userId )`", - plugin: 'Gutenberg', } ); const postType = getCurrentPostType( state ); @@ -713,9 +714,9 @@ export const getAutosave = createRegistrySelector( ( select ) => ( state ) => { */ export const hasAutosave = createRegistrySelector( ( select ) => ( state ) => { deprecated( "`wp.data.select( 'core/editor' ).hasAutosave()`", { + since: '5.3', alternative: "`!! wp.data.select( 'core' ).getAutosave( postType, postId, userId )`", - plugin: 'Gutenberg', } ); const postType = getCurrentPostType( state ); @@ -953,7 +954,7 @@ export function getSuggestedPostFormat( state ) { */ export function getBlocksForSerialization( state ) { deprecated( '`core/editor` getBlocksForSerialization selector', { - plugin: 'Gutenberg', + since: '5.3', alternative: 'getEditorBlocks', hint: 'Blocks serialization pre-processing occurs at save time', } ); @@ -1235,6 +1236,8 @@ export function getEditorBlocks( state ) { */ export function getEditorSelectionStart( state ) { deprecated( "select('core/editor').getEditorSelectionStart", { + since: '10.0', + plugin: 'Gutenberg', alternative: "select('core/editor').getEditorSelection", } ); return getEditedPostAttribute( state, 'selection' )?.selectionStart; @@ -1250,6 +1253,8 @@ export function getEditorSelectionStart( state ) { */ export function getEditorSelectionEnd( state ) { deprecated( "select('core/editor').getEditorSelectionStart", { + since: '10.0', + plugin: 'Gutenberg', alternative: "select('core/editor').getEditorSelection", } ); return getEditedPostAttribute( state, 'selection' )?.selectionEnd; @@ -1298,6 +1303,7 @@ export function getEditorSettings( state ) { */ export function getStateBeforeOptimisticTransaction() { deprecated( "select('core/editor').getStateBeforeOptimisticTransaction", { + since: '5.7', hint: 'No state history is kept on this store anymore', } ); @@ -1311,6 +1317,7 @@ export function getStateBeforeOptimisticTransaction() { */ export function inSomeHistory() { deprecated( "select('core/editor').inSomeHistory", { + since: '5.7', hint: 'No state history is kept on this store anymore', } ); return false; @@ -1319,6 +1326,7 @@ export function inSomeHistory() { function getBlockEditorSelector( name ) { return createRegistrySelector( ( select ) => ( state, ...args ) => { deprecated( "`wp.data.select( 'core/editor' )." + name + '`', { + since: '5.3', alternative: "`wp.data.select( 'core/block-editor' )." + name + '`', } ); diff --git a/packages/element/package.json b/packages/element/package.json index 9e48a3cd96ddc6..b072325d18283a 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -25,7 +25,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@types/react": "^16.9.0", "@types/react-dom": "^16.9.0", "@wordpress/escape-html": "file:../escape-html", diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index 880e8d312ba2de..cbbf3119984f67 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 2931efc71ccca2..71a7f94b028c24 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,8 +2,6 @@ ## Unreleased -## 9.0.1 (2021-03-19) - ### Bug Fix - Adds TypeScript as a peer dependency and makes it optional when not installed ([#29942](https://github.com/WordPress/gutenberg/pull/29942)). diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index f05aa17fae5b01..ebf8aff15da88f 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "9.0.1", + "version": "9.0.0", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/package.json b/packages/format-library/package.json index ba702ea8dc1c24..53a0f38e57a395 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/block-editor": "file:../block-editor", "@wordpress/components": "file:../components", diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 9402f769b04461..cc3eea4f6f8787 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -23,7 +23,7 @@ "react-native": "src/index", "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index f211c1b9e759e6..094f0ddfc81e67 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -24,7 +24,7 @@ "react-native": "src/index", "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/i18n/README.md b/packages/i18n/README.md index 6984e44e761a73..623db23eaf6d74 100644 --- a/packages/i18n/README.md +++ b/packages/i18n/README.md @@ -109,7 +109,7 @@ original format string is returned. _Related_ -- <http://www.diveintojavascript.com/projects/javascript-sprintf> +- <https://www.npmjs.com/package/sprintf-js> _Parameters_ diff --git a/packages/i18n/package.json b/packages/i18n/package.json index a48bf54a8f155f..72e169346cffd9 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -26,7 +26,7 @@ "pot-to-php": "./tools/pot-to-php.js" }, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/hooks": "file:../hooks", "gettext-parser": "^1.3.1", "lodash": "^4.17.19", diff --git a/packages/i18n/src/sprintf.js b/packages/i18n/src/sprintf.js index 90eca75beae885..42fa94d8aa3066 100644 --- a/packages/i18n/src/sprintf.js +++ b/packages/i18n/src/sprintf.js @@ -20,7 +20,7 @@ const logErrorOnce = memoize( console.error ); // eslint-disable-line no-console * @param {string} format The format of the string to generate. * @param {...*} args Arguments to apply to the format. * - * @see http://www.diveintojavascript.com/projects/javascript-sprintf + * @see https://www.npmjs.com/package/sprintf-js * * @return {string} The formatted string. */ diff --git a/packages/icons/package.json b/packages/icons/package.json index 2ade7c80ef51e5..423a4ecdf7630e 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -25,7 +25,7 @@ "react-native": "src/index", "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:../element", "@wordpress/primitives": "file:../primitives" }, diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 3a6457d6b9de63..0e927cad850288 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -5,6 +5,7 @@ export { default as alignJustify } from './library/align-justify'; export { default as alignLeft } from './library/align-left'; export { default as alignRight } from './library/align-right'; export { default as archive } from './library/archive'; +export { default as archiveTitle } from './library/archive-title'; export { default as arrowDown } from './library/arrow-down'; export { default as arrowLeft } from './library/arrow-left'; export { default as arrowRight } from './library/arrow-right'; @@ -107,6 +108,7 @@ export { default as linkOff } from './library/link-off'; export { default as list } from './library/list'; export { default as listView } from './library/list-view'; export { default as lock } from './library/lock'; +export { default as login } from './library/login'; export { default as loop } from './library/loop'; export { default as mapMarker } from './library/map-marker'; export { default as media } from './library/media'; @@ -139,6 +141,7 @@ export { default as plusCircleFilled } from './library/plus-circle-filled'; export { default as plusCircle } from './library/plus-circle'; export { default as plus } from './library/plus'; export { default as postCategories } from './library/post-categories'; +export { default as postContent } from './library/post-content'; export { default as postComments } from './library/post-comments'; export { default as postCommentsCount } from './library/post-comments-count'; export { default as postCommentsForm } from './library/post-comments-form'; @@ -189,6 +192,7 @@ export { default as tableRowBefore } from './library/table-row-before'; export { default as tableRowDelete } from './library/table-row-delete'; export { default as table } from './library/table'; export { default as tag } from './library/tag'; +export { default as termDescription } from './library/term-description'; export { default as footer } from './library/footer'; export { default as header } from './library/header'; export { default as sidebar } from './library/sidebar'; diff --git a/packages/icons/src/library/align-justify.js b/packages/icons/src/library/align-justify.js index f6f9a3a47e4a9d..9fee191c0a71e2 100644 --- a/packages/icons/src/library/align-justify.js +++ b/packages/icons/src/library/align-justify.js @@ -5,7 +5,7 @@ import { SVG, Path } from '@wordpress/primitives'; const alignJustify = ( <SVG xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"> - <Path d="M3 15h18v-2H3v2zm0 4h18v-2H3v2zm0-8h18V9H3v2zm0-6v2h18V5H3z" /> + <Path d="M4 12.8h16v-1.5H4v1.5zm0 7h12.4v-1.5H4v1.5zM4 4.3v1.5h16V4.3H4z" /> </SVG> ); diff --git a/packages/icons/src/library/archive-title.js b/packages/icons/src/library/archive-title.js new file mode 100644 index 00000000000000..68d62649889424 --- /dev/null +++ b/packages/icons/src/library/archive-title.js @@ -0,0 +1,16 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const archiveTitle = ( + <SVG viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <Path stroke="#1E1E1E" strokeWidth="1.5" d="M4 19.25h9M4 15.25h16" /> + <Path + d="M8.994 10.103H6.08L5.417 12H4l2.846-8h1.383l2.845 8H9.657l-.663-1.897zm-.457-1.28l-.994-2.857-1.006 2.857h2z" + fill="#1E1E1E" + /> + </SVG> +); + +export default archiveTitle; diff --git a/packages/icons/src/library/format-lowercase.js b/packages/icons/src/library/format-lowercase.js index 04278051c0bf0a..5ea05c2bc476fd 100644 --- a/packages/icons/src/library/format-lowercase.js +++ b/packages/icons/src/library/format-lowercase.js @@ -5,7 +5,7 @@ import { SVG, Path } from '@wordpress/primitives'; const formatLowercase = ( <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> - <Path d="M10.8 16.8c-.1-.1-.2-.3-.3-.5v-2.6c0-.9-.1-1.7-.3-2.2-.2-.5-.5-.9-.9-1.1-.4-.3-.9-.4-1.6-.4-.5 0-1 .1-1.5.2s-.9.3-1.2.6l.3 1.2c.4-.3.7-.4 1.1-.5.3-.1.7-.2 1-.2.6 0 1 .1 1.3.4.3.2.4.7.4 1.4-1.2 0-2.3.2-3.3.7s-1.4 1.1-1.4 2.1c0 .7.2 1.2.7 1.6.4.4 1 .6 1.8.6.9 0 1.7-.4 2.4-1.2.1.3.2.5.4.7.1.2.3.3.6.4.3.1.6.1 1.1.1h.1l.2-1.2h-.1c-.5.1-.7 0-.8-.1zM9.1 16c-.2.3-.5.6-.9.8-.4.1-.7.2-1.1.2-.4 0-.7-.1-.9-.3-.2-.2-.3-.5-.3-.9 0-.6.2-1 .7-1.3.5-.3 1.3-.4 2.5-.5v2zm10.5-3.9c-.3-.6-.7-1.1-1.2-1.5-.5-.4-1.2-.6-1.9-.6-.5 0-.9.1-1.4.3-.4.2-.8.5-1.1.8V6h-1.4v12h1.3l.2-1c.2.4.6.6 1 .8.4.2.9.3 1.4.3.7 0 1.2-.2 1.8-.5.5-.4 1-.9 1.3-1.5.3-.6.5-1.3.5-2.1 0-.6-.2-1.3-.5-1.9zm-1.6 4c-.4.5-.9.8-1.6.8s-1.2-.2-1.7-.7c-.5-.5-.7-1.2-.7-2.1 0-.9.2-1.6.7-2.1.4-.5 1-.7 1.7-.7s1.2.3 1.6.8c.4.5.6 1.2.6 2s-.2 1.4-.6 2z" /> + <Path d="M11 16.8c-.1-.1-.2-.3-.3-.5v-2.6c0-.9-.1-1.7-.3-2.2-.2-.5-.5-.9-.9-1.2-.4-.2-.9-.3-1.6-.3-.5 0-1 .1-1.5.2s-.9.3-1.2.6l.2 1.2c.4-.3.7-.4 1.1-.5.3-.1.7-.2 1-.2.6 0 1 .1 1.3.4.3.2.4.7.4 1.4-1.2 0-2.3.2-3.3.7s-1.4 1.1-1.4 2.1c0 .7.2 1.2.7 1.6.4.4 1 .6 1.8.6.9 0 1.7-.4 2.4-1.2.1.3.2.5.4.7.1.2.3.3.6.4.3.1.6.1 1.1.1h.1l.2-1.2h-.1c-.4.1-.6 0-.7-.1zM9.2 16c-.2.3-.5.6-.9.8-.3.1-.7.2-1.1.2-.4 0-.7-.1-.9-.3-.2-.2-.3-.5-.3-.9 0-.6.2-1 .7-1.3.5-.3 1.3-.4 2.5-.5v2zm10.6-3.9c-.3-.6-.7-1.1-1.2-1.5-.6-.4-1.2-.6-1.9-.6-.5 0-.9.1-1.4.3-.4.2-.8.5-1.1.8V6h-1.4v12h1.3l.2-1c.2.4.6.6 1 .8.4.2.9.3 1.4.3.7 0 1.2-.2 1.8-.5.5-.4 1-.9 1.3-1.5.3-.6.5-1.3.5-2.1-.1-.6-.2-1.3-.5-1.9zm-1.7 4c-.4.5-.9.8-1.6.8s-1.2-.2-1.7-.7c-.4-.5-.7-1.2-.7-2.1 0-.9.2-1.6.7-2.1.4-.5 1-.7 1.7-.7s1.2.3 1.6.8c.4.5.6 1.2.6 2s-.2 1.4-.6 2z" /> </SVG> ); diff --git a/packages/icons/src/library/login.js b/packages/icons/src/library/login.js new file mode 100644 index 00000000000000..9f529a0229fa4f --- /dev/null +++ b/packages/icons/src/library/login.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const login = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Path d="M11 14.5l1.1 1.1 3-3 .5-.5-.6-.6-3-3-1 1 1.7 1.7H5v1.5h7.7L11 14.5zM16.8 5h-7c-1.1 0-2 .9-2 2v1.5h1.5V7c0-.3.2-.5.5-.5h7c.3 0 .5.2.5.5v10c0 .3-.2.5-.5.5h-7c-.3 0-.5-.2-.5-.5v-1.5H7.8V17c0 1.1.9 2 2 2h7c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2z" /> + </SVG> +); + +export default login; diff --git a/packages/icons/src/library/post-content.js b/packages/icons/src/library/post-content.js new file mode 100644 index 00000000000000..fac55cf5f4e194 --- /dev/null +++ b/packages/icons/src/library/post-content.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const postContent = ( + <SVG xmlns="https://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Path d="M4 20h16v-1.5H4V20zm0-4.8h16v-1.5H4v1.5zm0-6.4v1.5h16V8.8H4zM16 4H4v1.5h12V4z" /> + </SVG> +); + +export default postContent; diff --git a/packages/icons/src/library/term-description.js b/packages/icons/src/library/term-description.js new file mode 100644 index 00000000000000..5c3efead4ca0c3 --- /dev/null +++ b/packages/icons/src/library/term-description.js @@ -0,0 +1,20 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const tag = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Path + stroke="#1E1E1E" + strokeWidth="1.5" + d="M9 19.25h6M4 19.25h4M12 15.25h8M4 15.25h7" + /> + <Path + d="M8.994 10.103H6.08L5.417 12H4l2.846-8h1.383l2.845 8H9.657l-.663-1.897zm-.457-1.28l-.994-2.857-1.006 2.857h2z" + fill="#1E1E1E" + /> + </SVG> +); + +export default tag; diff --git a/packages/interface/README.md b/packages/interface/README.md index c5d3cdb038e076..3779ce57e30d0b 100644 --- a/packages/interface/README.md +++ b/packages/interface/README.md @@ -1,6 +1,6 @@ # (Experimental) Interface -The Interface Package contains the basis the start a new WordPress screen as Edit Post or Edit Site. The package offers a data store and a set of components. The store is useful to contain common data required by a screen (e.g., active areas). The information is persisted across screen reloads. The components allow one to implement functionality like a sidebar or menu items. Third-party plugins by default, can extend them. +The Interface Package contains the basis to start a new WordPress screen as Edit Post or Edit Site. The package offers a data store and a set of components. The store is useful to contain common data required by a screen (e.g., active areas). The information is persisted across screen reloads. The components allow one to implement functionality like a sidebar or menu items. Third-party plugins can extend them by default. ## Installation @@ -17,7 +17,7 @@ _This package assumes that your code will run in an **ES2015+** environment. If ### Complementary Areas -This component was named after a [complementatry landmark](https://www.w3.org/TR/wai-aria-practices/examples/landmarks/complementary.html) โ€“ a supporting section of the document, designed to be complementary to the main content at a similar level in the DOM hierarchy, but remains meaningful when separated from the main content. +This component was named after a [complementary landmark](https://www.w3.org/TR/wai-aria-practices/examples/landmarks/complementary.html) โ€“ a supporting section of the document, designed to be complementary to the main content at a similar level in the DOM hierarchy, but remains meaningful when separated from the main content. `ComplementaryArea` and `ComplementaryArea.Slot` form a slot fill pair to render complementary areas. Multiple `ComplementaryArea` components representing different complementary areas may be rendered at the same time, but only one appears on the slot depending on which complementary area is enabled. diff --git a/packages/interface/package.json b/packages/interface/package.json index bed3bc60976507..cb73744455a39b 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -28,7 +28,7 @@ "{src,build,build-module}/{index.js,store/index.js}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", diff --git a/packages/interface/src/components/action-item/index.js b/packages/interface/src/components/action-item/index.js index 2474b4fd011892..23d3e147eefe3c 100644 --- a/packages/interface/src/components/action-item/index.js +++ b/packages/interface/src/components/action-item/index.js @@ -21,6 +21,8 @@ function ActionItemSlot( { deprecated( 'Passing a tuple of components with `as` prop to `ActionItem.Slot` component', { + since: '10.2', + plugin: 'Gutenberg', alternative: 'a component with `as` prop', version: '10.3', } diff --git a/packages/interface/src/components/interface-skeleton/style.scss b/packages/interface/src/components/interface-skeleton/style.scss index f038b4bb893d36..ed5f3c5f8d7665 100644 --- a/packages/interface/src/components/interface-skeleton/style.scss +++ b/packages/interface/src/components/interface-skeleton/style.scss @@ -128,16 +128,6 @@ html.interface-interface-skeleton__html-container { border-bottom: $border-width solid $gray-200; z-index: z-index(".interface-interface-skeleton__header"); color: $gray-900; - - // On Mobile the header is sticky. - position: sticky; - top: 0; - - // Cancel the fixed positioning used on Mobile. - @include break-small() { - position: initial; - top: 0; - } } .interface-interface-skeleton__footer { diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 611acbfc4dec52..16f14b78d6c94b 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -33,7 +33,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 474de8b3d60f5c..71b592a5cf401a 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -30,7 +30,7 @@ "main": "build/index.js", "module": "build-module/index.js", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "jest-matcher-utils": "^26.6.2", "lodash": "^4.17.19" }, diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index 49c8cd473536ee..e1e4fd1f2327dc 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -32,7 +32,7 @@ "module": "build-module/index.js", "dependencies": { "@axe-core/puppeteer": "^4.0.0", - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "peerDependencies": { "jest": ">=26", diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index e39736c0da9598..5e25e4829cc2ef 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", "@wordpress/element": "file:../element", diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 4ee610bd439329..835b65df347ec6 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/i18n": "file:../i18n", "lodash": "^4.17.19" }, diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index a2b49fea843735..ad54ffa8027a13 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -22,7 +22,7 @@ "main": "build/index.js", "module": "build-module/index.js", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index f81dacd50fd187..8fd428398f3c20 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -22,7 +22,7 @@ "main": "build/index.js", "module": "build-module/index.js", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", "@wordpress/element": "file:../element", diff --git a/packages/notices/package.json b/packages/notices/package.json index ce8b9a343d8ece..b03242933a39f0 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/a11y": "file:../a11y", "@wordpress/data": "file:../data", "lodash": "^4.17.19" diff --git a/packages/nux/package.json b/packages/nux/package.json index 5dc09603a8fe23..3bfa3d24319c0a 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -27,7 +27,7 @@ "{src,build,build-module}/{index.js,store/index.js}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", diff --git a/packages/nux/src/index.js b/packages/nux/src/index.js index ffd4a5e8818828..33f9f766c2b827 100644 --- a/packages/nux/src/index.js +++ b/packages/nux/src/index.js @@ -7,5 +7,6 @@ export { store } from './store'; export { default as DotTip } from './components/dot-tip'; deprecated( 'wp.nux', { + since: '5.4', hint: 'wp.components.Guide can be used to show a user guide.', } ); diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 6212e46f4b0b60..1b29f991433ffe 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:../compose", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", diff --git a/packages/primitives/package.json b/packages/primitives/package.json index 113b1a5d8a8529..e6a22c9fb34064 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -27,7 +27,7 @@ ], "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:../element", "classnames": "^2.2.5" }, diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json index 7f545b038bb223..50a9d7d9e1414d 100644 --- a/packages/priority-queue/package.json +++ b/packages/priority-queue/package.json @@ -25,7 +25,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index e24be9e706cf0a..d9feff55a0c8fa 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -24,7 +24,7 @@ "dependencies": { "@actions/core": "^1.0.0", "@actions/github": "^1.0.0", - "@babel/runtime": "^7.12.5" + "@babel/runtime": "^7.13.10" }, "publishConfig": { "access": "public" diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index 4bec82a45b4eea..6b93601635c721 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "utility-types": "^3.10.0" diff --git a/packages/react-native-aztec/CHANGELOG.md b/packages/react-native-aztec/CHANGELOG.md index 5fb8038d15d3ef..d838712641a659 100644 --- a/packages/react-native-aztec/CHANGELOG.md +++ b/packages/react-native-aztec/CHANGELOG.md @@ -1,3 +1,13 @@ <!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> -## Unreleased \ No newline at end of file +<!-- +For each user feature we should also add a importance categorization label to indicate the relevance of the change for end users of GB Mobile. The format is the following: +[***] โ†’ Major new features, significant updates to core flows, or impactful fixes (e.g. a crash that impacts a lot of users) โ€” things our users should be aware of. + +[**] โ†’ Changes our users will probably notice, but doesnโ€™t impact core flows. Most fixes. + +[*] โ†’ Minor enhancements and fixes that address annoyances โ€” things our users can miss. +--> + +## Unreleased +* [*] Block split/merge fix for a (never shipped) regression (Android only) [#29683] diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java index edf30a01154e86..a8e02bfeb35f5d 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecManager.java @@ -217,7 +217,8 @@ public void setText(ReactAztecText view, ReadableMap inputMap) { // force a 2nd setText from JS side to Native, just set a high eventCount int eventCount = inputMap.getInt("eventCount"); - if (view.mNativeEventCount < eventCount) { + if (view.getEventCounter() < eventCount) { + view.setEventCounterSyncFromJS(eventCount); setTextfromJS(view, inputMap.getString("text"), inputMap.getMap("selection")); } } diff --git a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java index 4089b8d87d9d9a..cb702492349a8d 100644 --- a/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java +++ b/packages/react-native-aztec/android/src/main/java/org/wordpress/mobile/ReactNativeAztec/ReactAztecText.java @@ -66,7 +66,8 @@ public class ReactAztecText extends AztecText { // FIXME: Used in `incrementAndGetEventCounter` but never read. I guess we can get rid of it, but before this // check when it's used in EditText in RN. (maybe tests?) - int mNativeEventCount = 0; + private int mNativeEventCount = 0; // \ Using two distinct counters to avoid race conditions, + private int mEventCountSyncFromJS = 0; // / each side is responsible for bumping the respective counter. String lastSentFormattingOptionsEventString = ""; boolean shouldHandleOnEnter = false; @@ -377,7 +378,22 @@ private void setIntrinsicContentSize() { //// Text changed events + public int getEventCounter() { + return mNativeEventCount; + } + + public void setEventCounterSyncFromJS(int syncToValue) { + mEventCountSyncFromJS = syncToValue; + } + public int incrementAndGetEventCounter() { + if (mNativeEventCount < mEventCountSyncFromJS) { + // need to sync up to the counter the JS side is expecting. Avoiding setting + // mNativeEventCount directly from the JS side to avoid race conditions, and instead + // syncing just-in-time when we need the new increment + mNativeEventCount = mEventCountSyncFromJS; + } + return ++mNativeEventCount; } diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index b788b26aae22ce..bbd87100b2f0e6 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.48.0", + "version": "1.49.0", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index 9e92034710e11a..854b26f745f408 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -1,7 +1,8 @@ /** * External dependencies */ -import ReactNative, { +import { + findNodeHandle, requireNativeComponent, UIManager, TouchableWithoutFeedback, @@ -39,7 +40,7 @@ class AztecView extends Component { dispatch( command, params ) { params = params || []; UIManager.dispatchViewManagerCommand( - ReactNative.findNodeHandle( this ), + findNodeHandle( this ), command, params ); @@ -125,7 +126,7 @@ class AztecView extends Component { _onBlur( event ) { this.selectionEndCaretY = null; - TextInputState.blurTextInput( ReactNative.findNodeHandle( this ) ); + TextInputState.blurTextInput( findNodeHandle( this ) ); if ( ! this.props.onBlur ) { return; @@ -177,18 +178,16 @@ class AztecView extends Component { } blur() { - TextInputState.blurTextInput( ReactNative.findNodeHandle( this ) ); + TextInputState.blurTextInput( findNodeHandle( this ) ); } focus() { - TextInputState.focusTextInput( ReactNative.findNodeHandle( this ) ); + TextInputState.focusTextInput( findNodeHandle( this ) ); } isFocused() { const focusedField = TextInputState.currentlyFocusedField(); - return ( - focusedField && focusedField === ReactNative.findNodeHandle( this ) - ); + return focusedField && focusedField === findNodeHandle( this ); } _onPress( event ) { diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index b52699d7ce921c..318b4a255297b8 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -133,6 +133,10 @@ public class Gutenberg: NSObject { sendEvent(.replaceBlock, body: ["html": block.content, "clientId": block.id]) } + public func replace(blockID: String, content: String) { + sendEvent(.replaceBlock, body: ["html": content, "clientId": blockID]) + } + public func updateCapabilities() { let capabilites = dataSource.gutenbergCapabilities() sendEvent(.updateCapabilities, body: capabilites.toJSPayload()) diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index aa82a6a4c87b03..3bb0db42bd2885 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.48.0", + "version": "1.49.0", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 6eb71f0388b99f..de522914b7c4be 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,63 +11,84 @@ For each user feature we should also add a importance categorization label to i ## Unreleased +- [***] a11y: Screenreader improvements for the UnitControl component [#29741] + +## 1.49.0 + +* [*] Remove the cancel button from settings options (Android only) [https://github.com/WordPress/gutenberg/pull/29599] + ## 1.48.0 -* [**] Buttons block: added width setting. [#28543] + +- [**] Buttons block: added width setting. [#28543] + +## 1.47.2 + +- [**] Adds a `replaceBlock` method to iOS bridge delegate with a string to match the clientID and the contents to replace with. [#29734] + +## 1.47.1 + +- [**] Reduce the number of items per page when fetching reusable blocks to prevent a crash. [#29626] ## 1.47.0 -* [**] Add support for setting Cover block focal point. [#25810] + +- [**] Add support for setting Cover block focal point. [#25810] ## 1.46.1 -* [**] Make inserter long-press options "add to beginning" and "add to end" always available. [#28610] -* [*] Fix crash when Column block width attribute was empty. [#29015] + +- [**] Make inserter long-press options "add to beginning" and "add to end" always available. [#28610] +- [*] Fix crash when Column block width attribute was empty. [#29015] ## 1.46.0 -* [***] New Block: Audio [#27401, #27467, #28594] -* [**] Add support for setting heading anchors [#27935] -* [**] Disable Unsupported Block Editor for Reusable blocks [#28552] -* [**] Add proper handling for single use blocks such as the more block [#28339] + +- [***] New Block: Audio [#27401, #27467, #28594] +- [**] Add support for setting heading anchors [#27935] +- [**] Disable Unsupported Block Editor for Reusable blocks [#28552] +- [**] Add proper handling for single use blocks such as the more block [#28339] ## 1.45.0 -* [*] Use react-native-url-polyfill in globals - [https://github.com/WordPress/gutenberg/pull/27867] -* [*] Remove Old Layout Picker - [https://github.com/WordPress/gutenberg/pull/27640] + +- [*] Use react-native-url-polyfill in globals - [https://github.com/WordPress/gutenberg/pull/27867] +- [*] Remove Old Layout Picker - [https://github.com/WordPress/gutenberg/pull/27640] ## 1.44.1 -* [**] Fix crash in mobile paragraph blocks with custom font size [#28121] -* [**] Add move to top bottom when long pressing block movers [#27554] + +- [**] Fix crash in mobile paragraph blocks with custom font size [#28121] +- [**] Add move to top bottom when long pressing block movers [#27554] ## 1.44.0 -* [***] Add support for cross-posting between sites -* [***] Full-width and wide alignment support for Columns +- [***] Add support for cross-posting between sites +- [***] Full-width and wide alignment support for Columns ## 1.43.0 -* [***] New Block: File [#27228] -* [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] + +- [***] New Block: File [#27228] +- [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] ## 1.42.0 -* [***] Adding support for selecting different unit of value in Cover and Columns blocks [#26161] -* [**] Button block - Add link picker to the block settingsย [#26206] -* [**] Support to render background/text colors in Group, Paragraph and Quote blocksย [#25994] -* [*] Fix theme colors syncing with the editor [#26821] -* [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] + +- [***] Adding support for selecting different unit of value in Cover and Columns blocks [#26161] +- [**] Button block - Add link picker to the block settingsย [#26206] +- [**] Support to render background/text colors in Group, Paragraph and Quote blocksย [#25994] +- [*] Fix theme colors syncing with the editor [#26821] +- [**] Fix issue where a blocks would disappear when deleting all of the text inside without requiring the extra backspace to remove the block. [#27583] ## 1.41.0 -* [***] Faster editor start and overall operation on Android [#26732] -* [*] [Android] Enable multiple upload support for Image block +- [***] Faster editor start and overall operation on Android [#26732] +- [*] [Android] Enable multiple upload support for Image block ## 1.40.0 ## 1.39.1 -* [*] Heading block - Disable full-width/wide alignment [#26308] +- [*] Heading block - Disable full-width/wide alignment [#26308] ## 1.39.0 -* [***] Full-width and wide alignment support for Video, Latest-posts, Gallery, Media & text, and Pullquote block -* [***] Fix unsupported block bottom sheet is triggered when device is rotated -* [***] Unsupported Block Editor: Fixed issue when cannot view or interact with the classic block on Jetpack site - +- [***] Full-width and wide alignment support for Video, Latest-posts, Gallery, Media & text, and Pullquote block +- [***] Fix unsupported block bottom sheet is triggered when device is rotated +- [***] Unsupported Block Editor: Fixed issue when cannot view or interact with the classic block on Jetpack site ## 1.38.0 @@ -75,272 +96,272 @@ For each user feature we should also add a importance categorization label to i ## 1.37.0 -* [**] Add support for rounded style in Image block -* [***] Full-width and wide alignment support for Group, Cover and Image block +- [**] Add support for rounded style in Image block +- [***] Full-width and wide alignment support for Group, Cover and Image block ## 1.36.1 -* [**] [iOS] Fixed Dark Mode transition for editor menus. +- [**] [iOS] Fixed Dark Mode transition for editor menus. ## 1.36.0 -* [**] [Android] Removed pullquote dev only restriction in Android -* [**] Reflect changes of slider in block settings immediately. +- [**] [Android] Removed pullquote dev only restriction in Android +- [**] Reflect changes of slider in block settings immediately. ## 1.35.0 -* [***] Fixed empty text fields on RTL layout. Now they are selectable and placeholders are visible. -* [**] Add settings to allow changing column widths -* [**] Media editing support in Gallery block. +- [***] Fixed empty text fields on RTL layout. Now they are selectable and placeholders are visible. +- [**] Add settings to allow changing column widths +- [**] Media editing support in Gallery block. ## 1.34.0 -* [***] Media editing support in Cover block. -* [*] Fixed a bug on the Heading block, where a heading with a link and string formatting showed a white shadow in dark mode. +- [***] Media editing support in Cover block. +- [*] Fixed a bug on the Heading block, where a heading with a link and string formatting showed a white shadow in dark mode. ## 1.33.1 -* Fixed a bug in the @-mentions feature where dismissing the @-mentions UI removed the @ character from the post. +- Fixed a bug in the @-mentions feature where dismissing the @-mentions UI removed the @ character from the post. ## 1.33.0 -* [***] Media editing support in Media & Text block. -* [***] New block: Social Icons -* [*] Cover block placeholder is updated to allow users start the block with a background color +- [***] Media editing support in Media & Text block. +- [***] New block: Social Icons +- [*] Cover block placeholder is updated to allow users start the block with a background color ## 1.32.0 -* [***] Adds Copy, Cut, Paste, and Duplicate functionality to blocks -* [***] Add support for mentions. -* [***] Users can now individually edit unsupported blocks found in posts or pages. -* [*] [iOS] Improved editor loading experience with Ghost Effect. +- [***] Adds Copy, Cut, Paste, and Duplicate functionality to blocks +- [***] Add support for mentions. +- [***] Users can now individually edit unsupported blocks found in posts or pages. +- [*] [iOS] Improved editor loading experience with Ghost Effect. ## 1.31.1 -* Fix for pullquote stylying in dark mode. -* Fix for button style. +- Fix for pullquote stylying in dark mode. +- Fix for button style. ## 1.31.0 -* [**] Add support for customizing gradient type and angle in Buttons and Cover blocks. -* [*] Show content information (block, word and characters counts). -* [*] [Android] Fix handling of upload completion while re-opening the editor +- [**] Add support for customizing gradient type and angle in Buttons and Cover blocks. +- [*] Show content information (block, word and characters counts). +- [*] [Android] Fix handling of upload completion while re-opening the editor ## 1.30.0 -* [**] Adds editor support for theme defined colors and theme defined gradients on cover and button blocks. -* [*] Support for breaking out of captions/citation authors by pressing enter on the following blocks: image, video, gallery, quote, and pullquote. +- [**] Adds editor support for theme defined colors and theme defined gradients on cover and button blocks. +- [*] Support for breaking out of captions/citation authors by pressing enter on the following blocks: image, video, gallery, quote, and pullquote. ## 1.29.1 -* Revert Creating undo levels less frequently +- Revert Creating undo levels less frequently ## 1.29.0 -* [**] Add support for changing overlay color settings in Cover block -* Add enter/exit animation in FloatingToolbar -* [***] New block: Verse -* [*] Fix merging of text blocks when text had active formatting (bold, italic, strike, link) -* [***] Trash icon that is used to remove blocks is moved to the new menu reachable via ellipsis button in the block toolbar -* [**] Block toolbar can now collapse when the block width is smaller than the toolbar content -* [**] Creating undo levels less frequently -* [**] Tooltip for page template selection buttons -* [*] Fix button alignment in page templates and make strings consistent -* [*] Add support for displaying radial gradients in Buttons and Cover blocks +- [**] Add support for changing overlay color settings in Cover block +- Add enter/exit animation in FloatingToolbar +- [***] New block: Verse +- [*] Fix merging of text blocks when text had active formatting (bold, italic, strike, link) +- [***] Trash icon that is used to remove blocks is moved to the new menu reachable via ellipsis button in the block toolbar +- [**] Block toolbar can now collapse when the block width is smaller than the toolbar content +- [**] Creating undo levels less frequently +- [**] Tooltip for page template selection buttons +- [*] Fix button alignment in page templates and make strings consistent +- [*] Add support for displaying radial gradients in Buttons and Cover blocks ## 1.28.2 -* [***] Disable Pullquote Block on Android +- [***] Disable Pullquote Block on Android ## 1.28.1 -* [**] Avoid crash when editor selection state becomes invalid +- [**] Avoid crash when editor selection state becomes invalid ## 1.28.0 -* [***] New block: Pullquote -* [**] Add support for changing background and text color in Buttons block -* [*] Fix the icons and buttons in Gallery, Paragraph, List and MediaText block on RTL mode -* [**] Remove Subscription Button from the Blog template since it didn't have an initial functionality and it is hard to configure for users. -* [**] [iOS] Add support for the subscript `<sub>` and superscript `<sup>`HTML elements in text blocks -* [**] Update page templates to use recently added blocks +- [***] New block: Pullquote +- [**] Add support for changing background and text color in Buttons block +- [*] Fix the icons and buttons in Gallery, Paragraph, List and MediaText block on RTL mode +- [**] Remove Subscription Button from the Blog template since it didn't have an initial functionality and it is hard to configure for users. +- [**] [iOS] Add support for the subscript `<sub>` and superscript `<sup>`HTML elements in text blocks +- [**] Update page templates to use recently added blocks ## 1.27.1 -* Remove Subscription Button from the Blog template since it didn't have an initial functionality and it is hard to configure for users. +- Remove Subscription Button from the Blog template since it didn't have an initial functionality and it is hard to configure for users. ## 1.27.0 -* Block Editor: Add dialog for mentioning other users in your post -* Prefill caption for image blocks when available on the Media library -* New block: Buttons. From now youโ€™ll be able to add the individual Button block only inside the Buttons block -* Fix bug where whitespaces at start of text blocks were being removed -* Add support for upload options in Cover block -* [Android] Floating toolbar, previously located above nested blocks, is now placed at the top of the screen -* [iOS] Floating toolbar, previously located above nested blocks, is now placed at the bottom of the screen -* Fix the icons in FloatingToolbar on RTL mode -* [Android] Add alignment options for heading block -* Fix Quote block so it visually reflects selected alignment -* Fix bug where buttons in page templates were not rendering correctly on web +- Block Editor: Add dialog for mentioning other users in your post +- Prefill caption for image blocks when available on the Media library +- New block: Buttons. From now youโ€™ll be able to add the individual Button block only inside the Buttons block +- Fix bug where whitespaces at start of text blocks were being removed +- Add support for upload options in Cover block +- [Android] Floating toolbar, previously located above nested blocks, is now placed at the top of the screen +- [iOS] Floating toolbar, previously located above nested blocks, is now placed at the bottom of the screen +- Fix the icons in FloatingToolbar on RTL mode +- [Android] Add alignment options for heading block +- Fix Quote block so it visually reflects selected alignment +- Fix bug where buttons in page templates were not rendering correctly on web ## 1.26.0 -* [iOS] Disable ripple effect in all BottomSheet's controls. -* [Android] Disable ripple effect for Slider control -* New block: Columns -* New starter page template: Blog -* Make Starter Page Template picker buttons visible only when the screen height is enough -* Fix a bug which caused to show URL settings modal randomly when changing the device orientation multiple times during the time Starter Page Template Preview is open +- [iOS] Disable ripple effect in all BottomSheet's controls. +- [Android] Disable ripple effect for Slider control +- New block: Columns +- New starter page template: Blog +- Make Starter Page Template picker buttons visible only when the screen height is enough +- Fix a bug which caused to show URL settings modal randomly when changing the device orientation multiple times during the time Starter Page Template Preview is open ## 1.25.0 -* New block: Cover -* [Android] Dark Mode -* [Android] Improve icon on the "Take a Video" media option -* Removed the dimming effect on unselected blocks -* [iOS] Add alignment options for heading block -* Implemented dropdown toolbar for alignment toolbar in Heading, Paragraph, Image, MediaText blocks -* Block Editor: When editing link settings, tapping the keyboard return button now closes the settings panel as well as closing the keyboard. -* [Android] Show an "Edit" button overlay on selected image blocks +- New block: Cover +- [Android] Dark Mode +- [Android] Improve icon on the "Take a Video" media option +- Removed the dimming effect on unselected blocks +- [iOS] Add alignment options for heading block +- Implemented dropdown toolbar for alignment toolbar in Heading, Paragraph, Image, MediaText blocks +- Block Editor: When editing link settings, tapping the keyboard return button now closes the settings panel as well as closing the keyboard. +- [Android] Show an "Edit" button overlay on selected image blocks ## 1.24.0 -* New block: Latest Posts -* Fix Quote block's left border not being visible in Dark Mode -* Added Starter Page Templates: when you create a new page, we now show you a few templates to get started more quickly. -* Fix crash when pasting HTML content with embeded images on paragraphs +- New block: Latest Posts +- Fix Quote block's left border not being visible in Dark Mode +- Added Starter Page Templates: when you create a new page, we now show you a few templates to get started more quickly. +- Fix crash when pasting HTML content with embeded images on paragraphs ## 1.23.0 -* New block: Group -* Add support for upload options in Gallery block -* Add support for size options in the Image block -* New block: Button -* Add scroll support inside block picker and block settings -* [Android] Fix issue preventing correct placeholder image from displaying during image upload -* [iOS] Fix diplay of large numbers on ordered lists -* Fix issue where adding emojis to the post title add strong HTML elements to the title of the post -* [iOS] Fix issue where alignment of paragraph blocks was not always being respected when splitting the paragraph or reading the post's html content. -* Weโ€™ve introduced a new toolbar that floats above the block youโ€™re editing, which makes navigating your blocks easier โ€” especially complex ones. +- New block: Group +- Add support for upload options in Gallery block +- Add support for size options in the Image block +- New block: Button +- Add scroll support inside block picker and block settings +- [Android] Fix issue preventing correct placeholder image from displaying during image upload +- [iOS] Fix diplay of large numbers on ordered lists +- Fix issue where adding emojis to the post title add strong HTML elements to the title of the post +- [iOS] Fix issue where alignment of paragraph blocks was not always being respected when splitting the paragraph or reading the post's html content. +- Weโ€™ve introduced a new toolbar that floats above the block youโ€™re editing, which makes navigating your blocks easier โ€” especially complex ones. ## 1.22.0 -* Make inserter to show options on long-press to add before/after -* Retry displaying image when connectivity restores -* [iOS] Show an "Edit" button overlay on selected image blocks -* [Android] Fix blank post when sharing media from another app -* Add support for image size options in the gallery block -* Fix issue that sometimes prevented merging paragraph blocks +- Make inserter to show options on long-press to add before/after +- Retry displaying image when connectivity restores +- [iOS] Show an "Edit" button overlay on selected image blocks +- [Android] Fix blank post when sharing media from another app +- Add support for image size options in the gallery block +- Fix issue that sometimes prevented merging paragraph blocks ## 1.21.0 -* Reduced padding around text on Rich Text based blocks. -* [Android] Improved stability on very long posts. +- Reduced padding around text on Rich Text based blocks. +- [Android] Improved stability on very long posts. ## 1.20.0 -* Fix bug where image placeholders would sometimes not be shown -* Fix crash on undo -* Style fixes on the navigation UI -* [iOS] Fix focus issue -* New block: Shortcode. You can now create and edit Shortcode blocks in the editor. +- Fix bug where image placeholders would sometimes not be shown +- Fix crash on undo +- Style fixes on the navigation UI +- [iOS] Fix focus issue +- New block: Shortcode. You can now create and edit Shortcode blocks in the editor. ## 1.19.0 -* Add support for changing Settings in List Block. -* [iOS] Fix crash dismissing bottom-sheet after device rotation. -* [Android] Add support for Preformatted block. -* New block: Gallery. You can now create image galleries using WordPress Media library. Upload feature is coming soon. -* Add support for Video block settings +- Add support for changing Settings in List Block. +- [iOS] Fix crash dismissing bottom-sheet after device rotation. +- [Android] Add support for Preformatted block. +- New block: Gallery. You can now create image galleries using WordPress Media library. Upload feature is coming soon. +- Add support for Video block settings ## 1.18.0 -* [iOS] Added native fullscreen preview when clicking image from Image Block -* New block: Spacer +- [iOS] Added native fullscreen preview when clicking image from Image Block +- New block: Spacer ## 1.17.0 -* Include block title in Unsupported block's UI -* Show new-block-indicator when no blocks at all and when at the last block -* Use existing links in the clipboard to prefill url field when inserting new link. -* Media & Text block alignment options -* Add alignment controls for paragraph blocks -* [iOS] Fix issue where the keyboard would not capitalize sentences correctly on some cases. -* [iOS] Support for Pexels image library -* [Android] Added native fullscreen preview when clicking image from Image Block -* [iOS] Add support for Preformatted block. -* [Android] Fix issue when removing image/page break block crashes the app +- Include block title in Unsupported block's UI +- Show new-block-indicator when no blocks at all and when at the last block +- Use existing links in the clipboard to prefill url field when inserting new link. +- Media & Text block alignment options +- Add alignment controls for paragraph blocks +- [iOS] Fix issue where the keyboard would not capitalize sentences correctly on some cases. +- [iOS] Support for Pexels image library +- [Android] Added native fullscreen preview when clicking image from Image Block +- [iOS] Add support for Preformatted block. +- [Android] Fix issue when removing image/page break block crashes the app ## 1.16.1 -* [iOS] Fix tap on links bug that reappear on iOS 13.2 +- [iOS] Fix tap on links bug that reappear on iOS 13.2 ## 1.16.0 -* [Android] Add support for pexels images -* Add left, center, and right image alignment controls +- [Android] Add support for pexels images +- Add left, center, and right image alignment controls ## 1.15.3 -* [iOS] Fix a layout bug in RCTAztecView in iOS 13.2 +- [iOS] Fix a layout bug in RCTAztecView in iOS 13.2 ## 1.15.2 -* Fix issue when copy/paste photos from other apps, was not inserting an image on the post. -* Fix issue where the block inserter layout wasn't correct after device rotation. +- Fix issue when copy/paste photos from other apps, was not inserting an image on the post. +- Fix issue where the block inserter layout wasn't correct after device rotation. ## 1.15.0 -* Fix issue when multiple media selection adds only one image or video block on Android -* Fix issue when force Touch app shortcut doesn't work properly selecting "New Photo Post" on iOS -* Add Link Target (Open in new tab) to Image Block. -* [iOS] DarkMode improvements. -* [iOS] Update to iOS 11 and Swift 5 -* New block: Media & Text +- Fix issue when multiple media selection adds only one image or video block on Android +- Fix issue when force Touch app shortcut doesn't work properly selecting "New Photo Post" on iOS +- Add Link Target (Open in new tab) to Image Block. +- [iOS] DarkMode improvements. +- [iOS] Update to iOS 11 and Swift 5 +- New block: Media & Text ## 1.14.0 -* Fix a bug on iOS 13.0 were tapping on a link opens Safari -* Fix a link editing issue, where trying to add a empty link at the start of another link would remove the existing link. -* Fix missing content on long posts in html mode on Android +- Fix a bug on iOS 13.0 were tapping on a link opens Safari +- Fix a link editing issue, where trying to add a empty link at the start of another link would remove the existing link. +- Fix missing content on long posts in html mode on Android ## 1.12.0 -* Add rich text styling to video captions -* Prevent keyboard dismissal when switching between caption and text block on Android -* Blocks that would be replaced are now hidden when add block bottom sheet displays -* Tapping on empty editor area now always inserts new block at end of post +- Add rich text styling to video captions +- Prevent keyboard dismissal when switching between caption and text block on Android +- Blocks that would be replaced are now hidden when add block bottom sheet displays +- Tapping on empty editor area now always inserts new block at end of post ## 1.11.0 -* Toolbar scroll position now resets when its content changes. -* Dark Mode for iOS. +- Toolbar scroll position now resets when its content changes. +- Dark Mode for iOS. ## 1.10.0 -* Adding a block from the post title now shows the add block here indicator. -* Deselect post title any time a block is added -* Fix loss of center alignment in image captions on Android +- Adding a block from the post title now shows the add block here indicator. +- Deselect post title any time a block is added +- Fix loss of center alignment in image captions on Android ## 1.9.0 -* Enable video block on Android platform -* Tapping on an empty editor area will create a new paragraph block -* Fix content loss issue when loading unsupported blocks containing inner blocks. -* Adding a block from the Post Title now inserts the block at the top of the Post. +- Enable video block on Android platform +- Tapping on an empty editor area will create a new paragraph block +- Fix content loss issue when loading unsupported blocks containing inner blocks. +- Adding a block from the Post Title now inserts the block at the top of the Post. ## 1.8.0 -* Fix pasting simple text on Post Title -* Remove editable empty line after list on the List block -* Performance improvements on rich text editing +- Fix pasting simple text on Post Title +- Remove editable empty line after list on the List block +- Performance improvements on rich text editing ## 1.7.0 -* Fixed keyboard flickering issue after pressing Enter repeatedly on the Post Title. -* New blocks are available: video/quote/more +- Fixed keyboard flickering issue after pressing Enter repeatedly on the Post Title. +- New blocks are available: video/quote/more ## 1.6.0 -* Fixed issue with link settings where โ€œOpen in New Tabโ€ was always OFF on open. -* Added UI to display a warning when a block has invalid content. +- Fixed issue with link settings where โ€œOpen in New Tabโ€ was always OFF on open. +- Added UI to display a warning when a block has invalid content. diff --git a/packages/react-native-editor/__device-tests__/helpers/caps.js b/packages/react-native-editor/__device-tests__/helpers/caps.js index 804ef802fb5c48..30f23cc2ffa5e6 100644 --- a/packages/react-native-editor/__device-tests__/helpers/caps.js +++ b/packages/react-native-editor/__device-tests__/helpers/caps.js @@ -14,6 +14,8 @@ const ios = { exports.iosLocal = { ...ios, deviceName: 'iPhone 11', + wdaLaunchTimeout: 240000, + usePrebuiltWDA: true, }; exports.iosServer = { diff --git a/packages/react-native-editor/__device-tests__/helpers/utils.js b/packages/react-native-editor/__device-tests__/helpers/utils.js index 50654b582ee434..eec48ba84a26b4 100644 --- a/packages/react-native-editor/__device-tests__/helpers/utils.js +++ b/packages/react-native-editor/__device-tests__/helpers/utils.js @@ -28,6 +28,7 @@ const defaultAndroidAppPath = './android/app/build/outputs/apk/debug/app-debug.apk'; const defaultIOSAppPath = './ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app'; +const webDriverAgentPath = process.env.WDA_PATH || './ios/build/WDA'; const localAndroidAppPath = process.env.ANDROID_APP_PATH || defaultAndroidAppPath; @@ -55,6 +56,18 @@ const isLocalEnvironment = () => { return testEnvironment.toLowerCase() === 'local'; }; +const getIOSPlatformVersions = () => { + const { runtimes = [] } = JSON.parse( + childProcess.execSync( 'xcrun simctl list runtimes --json' ).toString() + ); + + return runtimes + .reverse() + .filter( + ( { name, isAvailable } ) => name.startsWith( 'iOS' ) && isAvailable + ); +}; + // Initialises the driver and desired capabilities for appium const setupDriver = async () => { const branch = process.env.CIRCLE_BRANCH || ''; @@ -105,7 +118,30 @@ const setupDriver = async () => { desiredCaps.app = `sauce-storage:Gutenberg-${ safeBranchName }.app.zip`; // App should be preloaded to sauce storage, this can also be a URL if ( isLocalEnvironment() ) { desiredCaps = _.clone( iosLocal ); + + const iosPlatformVersions = getIOSPlatformVersions(); + if ( iosPlatformVersions.length === 0 ) { + throw new Error( + 'No iOS simulators available! Please verify that you have iOS simulators installed.' + ); + } + // eslint-disable-next-line no-console + console.log( + 'Available iOS platform versions:', + iosPlatformVersions.map( ( { name } ) => name ) + ); + + if ( ! desiredCaps.platformVersion ) { + desiredCaps.platformVersion = iosPlatformVersions[ 0 ].version; + + // eslint-disable-next-line no-console + console.log( + `Using iOS ${ desiredCaps.platformVersion } platform version` + ); + } + desiredCaps.app = path.resolve( localIOSAppPath ); + desiredCaps.derivedDataPath = path.resolve( webDriverAgentPath ); } } diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 1c9bb66ecfa608..916d9951bc5128 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -21,7 +21,7 @@ PODS: - DoubleConversion - glog - glog (0.3.5) - - Gutenberg (1.48.0): + - Gutenberg (1.49.0): - React-Core (= 0.61.5) - React-CoreModules (= 0.61.5) - React-RCTImage (= 0.61.5) @@ -253,7 +253,7 @@ PODS: - React-Core - RNSVG (9.13.6-gb): - React-Core - - RNTAztecView (1.48.0): + - RNTAztecView (1.49.0): - React-Core - WordPress-Aztec-iOS (~> 1.19.4) - WordPress-Aztec-iOS (1.19.4) @@ -402,7 +402,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 118d0d177724c2d67f08a59136eb29ef5943ec75 Folly: 30e7936e1c45c08d884aa59369ed951a8e68cf51 glog: 1f3da668190260b06b429bb211bfbee5cd790c28 - Gutenberg: 670f7b82bc18bde86a516834b72b7eeded1011e8 + Gutenberg: aae81808f86c8efff041d8a987c4f7a84dbedc61 RCTRequired: b153add4da6e7dbc44aebf93f3cf4fcae392ddf1 RCTTypeSafety: 9aa1b91d7f9310fc6eadc3cf95126ffe818af320 React: b6a59ef847b2b40bb6e0180a97d0ca716969ac78 @@ -435,7 +435,7 @@ SPEC CHECKSUMS: RNReanimated: f05baf4cd76b6eab2e4d7e2b244424960b968918 RNScreens: 953633729a42e23ad0c93574d676b361e3335e8b RNSVG: 46c4b680fe18237fa01eb7d7b311d77618fde31f - RNTAztecView: 985f01fae9ea8ce9cb18a6f7af9b571084d7c9f9 + RNTAztecView: ec5de892796a349f1195d7965366307200ffdfe8 WordPress-Aztec-iOS: 870c93297849072aadfc2223e284094e73023e82 Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index c6b2c8f4bd8afa..08c90f761514ba 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.48.0", + "version": "1.49.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -29,7 +29,7 @@ "main": "src/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@react-native-community/blur": "3.6.0", "@react-native-community/masked-view": "git+https://github.com/wordpress-mobile/react-native-masked-view.git#f65a51a3320e58404d7f38d967bfd1f42b439ca9", "@react-native-community/slider": "git+https://github.com/wordpress-mobile/react-native-slider.git#d263ff16cdd9fb7352b354342522ff030f220f42", @@ -113,7 +113,8 @@ "test:e2e:android:local": "npm run test:e2e:bundle:android && npm run test:e2e:build-app:android && TEST_RN_PLATFORM=android npm run device-tests:local", "test:e2e:bundle:ios": "mkdir -p ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app && npm run rn-bundle -- --reset-cache --platform=ios --dev=false --minify false --entry-file=index.js --bundle-output=./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle --assets-dest=./ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app", "test:e2e:build-app:ios": "npm run preios && SKIP_BUNDLING=true xcodebuild -workspace ios/GutenbergDemo.xcworkspace -configuration Release -scheme GutenbergDemo -destination 'platform=iOS Simulator,name=iPhone 11' -derivedDataPath ios/build/GutenbergDemo", - "test:e2e:ios:local": "npm run test:e2e:bundle:ios && npm run test:e2e:build-app:ios && TEST_RN_PLATFORM=ios npm run device-tests:local", + "test:e2e:build-wda": "xcodebuild -project ../../node_modules/appium/node_modules/appium-webdriveragent/WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'platform=iOS Simulator,name=iPhone 11' -derivedDataPath ios/build/WDA", + "test:e2e:ios:local": "npm run test:e2e:bundle:ios && npm run test:e2e:build-app:ios && npm run test:e2e:build-wda && TEST_RN_PLATFORM=ios npm run device-tests:local", "build:gutenberg": "cd gutenberg && npm ci && npm run build", "clean": "npm run clean:build-artifacts; npm run clean:aztec; npm run clean:haste; npm run clean:jest; npm run clean:metro; npm run clean:react; npm run clean:watchman", "clean:runtime": "npm run clean:haste; npm run clean:react; npm run clean:metro; npm run clean:jest; npm run clean:watchman; npm run clean:babel-cache", diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index e0ecd15005e35b..baca540dee7df9 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -25,7 +25,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "is-promise": "^4.0.0", "lodash": "^4.17.19", "rungen": "^0.3.2" diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 5310a2fc9f1845..969604faf28b79 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -26,10 +26,9 @@ "{src,build,build-module}/{index.js,store/index.js}" ], "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", - "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/escape-html": "file:../escape-html", diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index ab1667b7fc401b..6a14db55369d25 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -18,7 +18,6 @@ import { SPACE, ESCAPE, } from '@wordpress/keycodes'; -import deprecated from '@wordpress/deprecated'; import { getFilesFromDataTransfer } from '@wordpress/dom'; import { useMergeRefs } from '@wordpress/compose'; @@ -147,8 +146,6 @@ function RichText( onSelectionChange, onChange, unstableOnFocus: onFocus, - setFocusedElement, - instanceId, clientId, identifier, __unstableMultilineTag: multilineTag, @@ -963,12 +960,6 @@ function RichText( * documented, as the current requirements where it is used are subject to * future refactoring following `isSelected` handling. * - * In contrast with `setFocusedElement`, this is only triggered in response - * to focus within the contenteditable field, whereas `setFocusedElement` - * is triggered on focus within any `RichText` descendent element. - * - * @see setFocusedElement - * * @private */ function handleFocus() { @@ -1010,13 +1001,6 @@ function RichText( rafId.current = getWin().requestAnimationFrame( handleSelectionChange ); getDoc().addEventListener( 'selectionchange', handleSelectionChange ); - - if ( setFocusedElement ) { - deprecated( 'wp.blockEditor.RichText setFocusedElement prop', { - alternative: 'selection state from the block editor store.', - } ); - setFocusedElement( instanceId ); - } } function handleBlur() { diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 6185f53b1bdf82..ac83e09cec485b 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -117,6 +117,12 @@ export class RichText extends Component { this.convertFontSizeFromString = this.convertFontSizeFromString.bind( this ); + this.manipulateEventCounterToForceNativeToRefresh = this.manipulateEventCounterToForceNativeToRefresh.bind( + this + ); + this.shouldDropEventFromAztec = this.shouldDropEventFromAztec.bind( + this + ); this.state = { activeFormats: [], selectedFormat: null, @@ -221,7 +227,7 @@ export class RichText extends Component { insertString( record, string ) { if ( record && string ) { - this.lastEventCount = undefined; + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side const toInsert = insert( record, string ); this.onFormatChange( toInsert ); } @@ -285,6 +291,10 @@ export class RichText extends Component { * Handles any case where the content of the AztecRN instance has changed */ onChangeFromAztec( event ) { + if ( this.shouldDropEventFromAztec( event, 'onChange' ) ) { + return; + } + const contentWithoutRootTag = this.removeRootTagsProduceByAztec( unescapeSpaces( event.nativeEvent.text ) ); @@ -365,6 +375,10 @@ export class RichText extends Component { } handleDelete( event ) { + if ( this.shouldDropEventFromAztec( event, 'handleDelete' ) ) { + return; + } + const { keyCode } = event; if ( keyCode !== DELETE && keyCode !== BACKSPACE ) { @@ -592,7 +606,22 @@ export class RichText extends Component { this.props.onSelectionChange( start, end ); } + shouldDropEventFromAztec( event, logText ) { + const shouldDrop = + ! this.isIOS && event.nativeEvent.eventCount <= this.lastEventCount; + if ( shouldDrop ) { + window.console.log( + `Dropping ${ logText } from Aztec as its event counter is older than latest sent to the native side. Got ${ event.nativeEvent.eventCount } but lastEventCount is ${ this.lastEventCount }.` + ); + } + return shouldDrop; + } + onSelectionChangeFromAztec( start, end, text, event ) { + if ( this.shouldDropEventFromAztec( event, 'onSelectionChange' ) ) { + return; + } + // `end` can be less than `start` on iOS // Let's fix that here so `rich-text/slice` can work properly const realStart = Math.min( start, end ); @@ -667,13 +696,28 @@ export class RichText extends Component { return value; } + manipulateEventCounterToForceNativeToRefresh() { + if ( this.isIOS ) { + this.lastEventCount = undefined; + return; + } + + if ( typeof this.lastEventCount !== 'undefined' ) { + this.lastEventCount += 100; // bump by a hundred, hopefully native hasn't bombarded the JS side in the meantime. + } else { + window.console.warn( + "Tried to bump the RichText native event counter but was 'undefined'. Aborting bump." + ); + } + } + shouldComponentUpdate( nextProps ) { if ( nextProps.tagName !== this.props.tagName || nextProps.reversed !== this.props.reversed || nextProps.start !== this.props.start ) { - this.lastEventCount = undefined; + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side this.value = undefined; return true; } @@ -703,7 +747,7 @@ export class RichText extends Component { this.needsSelectionUpdate = true; } - this.lastEventCount = undefined; // force a refresh on the native side + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side } if ( ! this.comesFromAztec ) { @@ -715,7 +759,7 @@ export class RichText extends Component { nextProps.__unstableIsSelected ) { this.needsSelectionUpdate = true; - this.lastEventCount = undefined; // force a refresh on the native side + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side } } @@ -748,7 +792,6 @@ export class RichText extends Component { componentDidUpdate( prevProps ) { if ( this.props.value !== this.value ) { this.value = this.props.value; - this.lastEventCount = undefined; } const { __unstableIsSelected: isSelected } = this.props; @@ -772,7 +815,7 @@ export class RichText extends Component { let value = this.valueToFormat( record ); if ( value === undefined ) { - this.lastEventCount = undefined; // force a refresh on the native side + this.manipulateEventCounterToForceNativeToRefresh(); // force a refresh on the native side value = ''; } // On android if content is empty we need to send no content or else the placeholder will not show. diff --git a/packages/rich-text/src/component/use-inline-warning.js b/packages/rich-text/src/component/use-inline-warning.js index dfd3ba8de85156..0056771cc1dc2f 100644 --- a/packages/rich-text/src/component/use-inline-warning.js +++ b/packages/rich-text/src/component/use-inline-warning.js @@ -3,6 +3,9 @@ */ import { useEffect } from '@wordpress/element'; +const message = + 'RichText cannot be used with an inline container. Please use a different display property.'; + export function useInlineWarning( { ref } ) { useEffect( () => { if ( process.env.NODE_ENV === 'development' ) { @@ -12,9 +15,7 @@ export function useInlineWarning( { ref } ) { if ( computedStyle.display === 'inline' ) { // eslint-disable-next-line no-console - console.warn( - 'RichText cannot be used with an inline container. Please use a different tagName.' - ); + console.warn( message ); } } }, [] ); diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index ae95a1f8129971..d246897734a572 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancements + +- The bundled `babel-loader` dependency has been updated from requiring `^8.1.0` to requiring `^8.2.2` ([#30018](https://github.com/WordPress/gutenberg/pull/30018)). + ## 14.0.0 (2021-03-17) ### Breaking Changes diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 3b41cc2854fd7a..fb615ccef62d26 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "14.0.1", + "version": "14.0.0", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -42,7 +42,7 @@ "@wordpress/prettier-config": "file:../prettier-config", "@wordpress/stylelint-config": "file:../stylelint-config", "babel-jest": "^26.6.3", - "babel-loader": "^8.1.0", + "babel-loader": "^8.2.2", "chalk": "^4.0.0", "check-node-version": "^4.1.0", "clean-webpack-plugin": "^3.0.0", diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 43f8c5534d09cf..41c4df74b4a2d5 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -23,10 +23,11 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blocks": "file:../blocks", "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", "@wordpress/deprecated": "file:../deprecated", "@wordpress/element": "file:../element", diff --git a/packages/server-side-render/src/index.js b/packages/server-side-render/src/index.js index cec07d826d1c77..30af329d429a5f 100644 --- a/packages/server-side-render/src/index.js +++ b/packages/server-side-render/src/index.js @@ -43,6 +43,7 @@ const ExportedServerSideRender = withSelect( ( select ) => { if ( window && window.wp && window.wp.components ) { window.wp.components.ServerSideRender = forwardRef( ( props, ref ) => { deprecated( 'wp.components.ServerSideRender', { + since: '5.3', alternative: 'wp.serverSideRender', } ); return <ExportedServerSideRender { ...props } ref={ ref } />; diff --git a/packages/server-side-render/src/server-side-render.js b/packages/server-side-render/src/server-side-render.js index 4ef1e723b7c6ca..c858165ea129dd 100644 --- a/packages/server-side-render/src/server-side-render.js +++ b/packages/server-side-render/src/server-side-render.js @@ -1,12 +1,13 @@ /** * External dependencies */ -import { isEqual, debounce } from 'lodash'; +import { isEqual } from 'lodash'; /** * WordPress dependencies */ -import { Component, RawHTML } from '@wordpress/element'; +import { useDebounce, usePrevious } from '@wordpress/compose'; +import { RawHTML, useEffect, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; @@ -21,45 +22,55 @@ export function rendererPath( block, attributes = null, urlQueryArgs = {} ) { } ); } -export class ServerSideRender extends Component { - constructor( props ) { - super( props ); - this.state = { - response: null, - }; - } +function DefaultEmptyResponsePlaceholder( { className } ) { + return ( + <Placeholder className={ className }> + { __( 'Block rendered as empty.' ) } + </Placeholder> + ); +} - componentDidMount() { - this.isStillMounted = true; - this.fetch( this.props ); - // Only debounce once the initial fetch occurs to ensure that the first - // renders show data as soon as possible. - this.fetch = debounce( this.fetch, 500 ); - } +function DefaultErrorResponsePlaceholder( { response, className } ) { + const errorMessage = sprintf( + // translators: %s: error message describing the problem + __( 'Error loading block: %s' ), + response.errorMsg + ); + return <Placeholder className={ className }>{ errorMessage }</Placeholder>; +} - componentWillUnmount() { - this.isStillMounted = false; - } +function DefaultLoadingResponsePlaceholder( { className } ) { + return ( + <Placeholder className={ className }> + <Spinner /> + </Placeholder> + ); +} - componentDidUpdate( prevProps ) { - if ( ! isEqual( prevProps, this.props ) ) { - this.fetch( this.props ); - } - } +export default function ServerSideRender( props ) { + const { + attributes, + block, + className, + httpMethod = 'GET', + urlQueryArgs, + EmptyResponsePlaceholder = DefaultEmptyResponsePlaceholder, + ErrorResponsePlaceholder = DefaultErrorResponsePlaceholder, + LoadingResponsePlaceholder = DefaultLoadingResponsePlaceholder, + } = props; + + const isMountedRef = useRef( true ); + const fetchRequestRef = useRef(); + const [ response, setResponse ] = useState( null ); + const prevProps = usePrevious( props ); - fetch( props ) { - if ( ! this.isStillMounted ) { + function fetchData() { + if ( ! isMountedRef.current ) { return; } - if ( null !== this.state.response ) { - this.setState( { response: null } ); + if ( null !== response ) { + setResponse( null ); } - const { - block, - attributes = null, - httpMethod = 'GET', - urlQueryArgs = {}, - } = props; const sanitizedAttributes = attributes && @@ -68,105 +79,73 @@ export class ServerSideRender extends Component { // If httpMethod is 'POST', send the attributes in the request body instead of the URL. // This allows sending a larger attributes object than in a GET request, where the attributes are in the URL. const isPostRequest = 'POST' === httpMethod; - const urlAttributes = isPostRequest ? null : sanitizedAttributes; + const urlAttributes = isPostRequest + ? null + : sanitizedAttributes ?? null; const path = rendererPath( block, urlAttributes, urlQueryArgs ); - const data = isPostRequest ? { attributes: sanitizedAttributes } : null; + const data = isPostRequest + ? { attributes: sanitizedAttributes ?? null } + : null; // Store the latest fetch request so that when we process it, we can // check if it is the current request, to avoid race conditions on slow networks. - const fetchRequest = ( this.currentFetchRequest = apiFetch( { + const fetchRequest = ( fetchRequestRef.current = apiFetch( { path, data, method: isPostRequest ? 'POST' : 'GET', } ) - .then( ( response ) => { + .then( ( fetchResponse ) => { if ( - this.isStillMounted && - fetchRequest === this.currentFetchRequest && - response + isMountedRef.current && + fetchRequest === fetchRequestRef.current && + fetchResponse ) { - this.setState( { response: response.rendered } ); + setResponse( fetchResponse.rendered ); } } ) .catch( ( error ) => { if ( - this.isStillMounted && - fetchRequest === this.currentFetchRequest + isMountedRef.current && + fetchRequest === fetchRequestRef.current ) { - this.setState( { - response: { - error: true, - errorMsg: error.message, - }, + setResponse( { + error: true, + errorMsg: error.message, } ); } } ) ); + return fetchRequest; } - render() { - const response = this.state.response; - const { - className, - EmptyResponsePlaceholder, - ErrorResponsePlaceholder, - LoadingResponsePlaceholder, - } = this.props; - - if ( response === '' ) { - return ( - <EmptyResponsePlaceholder - response={ response } - { ...this.props } - /> - ); - } else if ( ! response ) { - return ( - <LoadingResponsePlaceholder - response={ response } - { ...this.props } - /> - ); - } else if ( response.error ) { - return ( - <ErrorResponsePlaceholder - response={ response } - { ...this.props } - /> - ); + const debouncedFetchData = useDebounce( fetchData, 500 ); + + // When the component unmounts, set isMountedRef to false. This will + // let the async fetch callbacks know when to stop. + useEffect( + () => () => { + isMountedRef.current = false; + }, + [] + ); + + useEffect( () => { + // Don't debounce the first fetch. This ensures that the first render + // shows data as soon as possible + if ( prevProps === undefined ) { + fetchData(); + } else if ( ! isEqual( prevProps, props ) ) { + debouncedFetchData(); } + } ); - return ( - <RawHTML key="html" className={ className }> - { response } - </RawHTML> - ); + if ( response === '' ) { + return <EmptyResponsePlaceholder { ...props } />; + } else if ( ! response ) { + return <LoadingResponsePlaceholder { ...props } />; + } else if ( response.error ) { + return <ErrorResponsePlaceholder response={ response } { ...props } />; } -} -ServerSideRender.defaultProps = { - EmptyResponsePlaceholder: ( { className } ) => ( - <Placeholder className={ className }> - { __( 'Block rendered as empty.' ) } - </Placeholder> - ), - ErrorResponsePlaceholder: ( { response, className } ) => { - const errorMessage = sprintf( - // translators: %s: error message describing the problem - __( 'Error loading block: %s' ), - response.errorMsg - ); - return ( - <Placeholder className={ className }>{ errorMessage }</Placeholder> - ); - }, - LoadingResponsePlaceholder: ( { className } ) => { - return ( - <Placeholder className={ className }> - <Spinner /> - </Placeholder> - ); - }, -}; - -export default ServerSideRender; + return <RawHTML className={ className }>{ response }</RawHTML>; +} diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index 75e748310dd30e..866e44031d6c27 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19", "memize": "^1.1.0" }, diff --git a/packages/token-list/package.json b/packages/token-list/package.json index fbf73ff77134dc..7447295f485208 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -23,7 +23,7 @@ "react-native": "src/index", "types": "build-types", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/packages/url/package.json b/packages/url/package.json index 36ef3dc2097a99..5dffbea26b43d4 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -24,7 +24,7 @@ "types": "build-types", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19", "react-native-url-polyfill": "^1.1.2" }, diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 521a065a5ed44c..f6d3f1d1e31dfb 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -22,7 +22,7 @@ "module": "build-module/index.js", "react-native": "src/index", "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", "lodash": "^4.17.19" diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json index 3f16a7690d537c..feb5680bd5d111 100644 --- a/packages/wordcount/package.json +++ b/packages/wordcount/package.json @@ -23,7 +23,7 @@ "react-native": "src/index", "sideEffects": false, "dependencies": { - "@babel/runtime": "^7.12.5", + "@babel/runtime": "^7.13.10", "lodash": "^4.17.19" }, "publishConfig": { diff --git a/phpunit/bootstrap.php b/phpunit/bootstrap.php index 96be683a5e2bad..8315c12292899b 100644 --- a/phpunit/bootstrap.php +++ b/phpunit/bootstrap.php @@ -77,6 +77,9 @@ function fail_if_died( $message ) { ), ); +// Enable the widget block editor. +tests_add_filter( 'gutenberg_use_widgets_block_editor', '__return_true' ); + // Start up the WP testing environment. require $_tests_dir . '/includes/bootstrap.php'; diff --git a/phpunit/class-block-templates-test.php b/phpunit/class-block-templates-test.php index 34fc0cc51746c3..43b6d0a891b309 100644 --- a/phpunit/class-block-templates-test.php +++ b/phpunit/class-block-templates-test.php @@ -20,6 +20,24 @@ public static function wpSetUpBeforeClass() { gutenberg_register_wp_theme_taxonomy(); gutenberg_register_wp_template_part_area_taxonomy(); + // Set up a template post corresponding to a different theme. + // We do this to ensure resolution and slug creation works as expected, + // even with another post of that same name present for another theme. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + 'this-theme-should-not-resolve', + ), + ), + ); + self::$post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( self::$post->ID, 'this-theme-should-not-resolve', 'wp_theme' ); + // Set up template post. $args = array( 'post_type' => 'wp_template', diff --git a/phpunit/class-extend-styles-test.php b/phpunit/class-extend-styles-test.php deleted file mode 100644 index 6ae12ada227973..00000000000000 --- a/phpunit/class-extend-styles-test.php +++ /dev/null @@ -1,185 +0,0 @@ -<?php -/** - * Test `gutenberg_extend_block_editor_styles`. - * - * @package Gutenberg - */ - -class Extend_Styles_Test extends WP_UnitTestCase { - - /** - * Path of the original `editor-styles.css` file. - * - * @var string|null - */ - protected $orignal_file = null; - - /** - * Contents of the `editor-styles.css` file. - * - * @var string - */ - protected $style_contents = null; - - public function wpTearDown() { - parent::wpTearDown(); - - $this->restore_editor_styles(); - } - - /** - * Restores the existence of `editor-styles.css` to its original state. - */ - protected function restore_editor_styles() { - $path = gutenberg_dir_path() . 'build/editor/editor-styles.css'; - - if ( $this->original_file ) { - if ( $this->original_file !== $path ) { - rename( $this->original_file, $path ); - } - } elseif ( file_exists( $path ) ) { - unlink( $path ); - } - - $this->style_contents = null; - $this->original_file = null; - } - - /** - * Guarantees that an `editor-styles.css` file exists, if and only if it - * should exist. Assigns `style_contents` according to the contents of the - * file if it should exist. Renames the existing file temporarily if it - * exists but should not. - * - * @param bool $should_exist Whether the editor styles file should exist. - */ - protected function ensure_editor_styles( $should_exist = true ) { - $path = gutenberg_dir_path() . 'build/editor/editor-styles.css'; - - if ( file_exists( $path ) ) { - if ( $should_exist ) { - $this->style_contents = file_get_contents( $path ); - $this->original_file = $path; - } else { - rename( $path, $path . '.bak' ); - $this->original_file = $path . '.bak'; - } - } elseif ( $should_exist ) { - $this->style_contents = ''; - file_put_contents( $path, $this->style_contents ); - $this->original_file = null; - } - } - - /** - * Tests a non-existent build `styles`. - */ - function test_without_built_styles() { - $this->ensure_editor_styles( false ); - - $settings = array( - 'styles' => array( - array( 'css' => 'original' ), - array( 'css' => 'someother' ), - ), - ); - - $result = gutenberg_extend_block_editor_styles( $settings ); - - $this->assertEquals( $settings, $result ); - } - - /** - * Tests an unset `styles` setting. - */ - function test_unset_editor_settings_style() { - $this->ensure_editor_styles(); - - $settings = array(); - - $settings = gutenberg_extend_block_editor_styles( $settings ); - - $this->assertEquals( - array( array( 'css' => $this->style_contents ) ), - $settings['styles'] - ); - } - - /** - * Tests replacing the default styles. - */ - function test_replace_default_editor_styles() { - $this->ensure_editor_styles(); - $default_styles = file_get_contents( - ABSPATH . WPINC . '/css/dist/editor/editor-styles.css' - ); - - $settings = array( - 'styles' => array( - array( 'css' => $default_styles ), - array( 'css' => 'someother' ), - ), - ); - - $settings = gutenberg_extend_block_editor_styles( $settings ); - - $this->assertEquals( - array( - array( 'css' => $this->style_contents ), - array( 'css' => 'someother' ), - ), - $settings['styles'] - ); - } - - /** - * Tests replacing the rearranged default styles. - */ - function test_replace_rearranged_default_editor_styles() { - $this->ensure_editor_styles(); - $default_styles = file_get_contents( - ABSPATH . WPINC . '/css/dist/editor/editor-styles.css' - ); - - $settings = array( - 'styles' => array( - array( 'css' => 'someother' ), - array( 'css' => $default_styles ), - ), - ); - - $settings = gutenberg_extend_block_editor_styles( $settings ); - - $this->assertEquals( - array( - array( 'css' => 'someother' ), - array( 'css' => $this->style_contents ), - ), - $settings['styles'] - ); - } - - /** - * Tests when the default styles aren't in the styles setting. - */ - function test_without_default_editor_styles() { - $this->ensure_editor_styles(); - - $settings = array( - 'styles' => array( - array( 'css' => 'someother' ), - ), - ); - - $settings = gutenberg_extend_block_editor_styles( $settings ); - - $this->assertEquals( - array( - array( 'css' => $this->style_contents ), - array( 'css' => 'someother' ), - ), - $settings['styles'] - ); - } - -} diff --git a/phpunit/class-rest-widget-types-controller-test.php b/phpunit/class-rest-widget-types-controller-test.php index 8ecb078bc57452..b670151cf11701 100644 --- a/phpunit/class-rest-widget-types-controller-test.php +++ b/phpunit/class-rest-widget-types-controller-test.php @@ -255,6 +255,7 @@ protected function check_widget_type_object( $widget_type, $data, $links ) { } public function test_get_widget_form() { + $this->setExpectedDeprecated( 'WP_REST_Widget_Types_Controller::get_widget_form' ); $widget_name = 'calendar'; wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/' . $widget_name . '/form-renderer' ); @@ -264,6 +265,112 @@ public function test_get_widget_form() { $this->assertArrayHasKey( 'form', $data ); } + public function test_encode_form_data_with_no_input() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "<p>\n" . + "\t\t\t<label for=\"widget-search-1-title\">Title:</label>\n" . + "\t\t\t<input class=\"widefat\" id=\"widget-search-1-title\" name=\"widget-search[1][title]\" type=\"text\" value=\"\" />\n" . + "\t\t</p>", + $data['form'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array() ) ), + 'hash' => wp_hash( serialize( array() ) ), + 'raw' => new stdClass, + ), + $data['instance'] + ); + } + + public function test_encode_form_data_with_instance() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( + 'instance', + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "<p>\n" . + "\t\t\t<label for=\"widget-search-1-title\">Title:</label>\n" . + "\t\t\t<input class=\"widefat\" id=\"widget-search-1-title\" name=\"widget-search[1][title]\" type=\"text\" value=\"Test title\" />\n" . + "\t\t</p>", + $data['form'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + 'raw' => array( 'title' => 'Test title' ), + ), + $data['instance'] + ); + } + + public function test_encode_form_data_with_form_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( 'form_data', 'widget-search[1][title]=Updated+title' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "<p>\n" . + "\t\t\t<label for=\"widget-search-1-title\">Title:</label>\n" . + "\t\t\t<input class=\"widefat\" id=\"widget-search-1-title\" name=\"widget-search[1][title]\" type=\"text\" value=\"Updated title\" />\n" . + "\t\t</p>", + $data['form'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Updated title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Updated title' ) ) ), + 'raw' => array( 'title' => 'Updated title' ), + ), + $data['instance'] + ); + } + + public function test_encode_form_data_no_raw() { + global $wp_widget_factory; + wp_set_current_user( self::$admin_id ); + $wp_widget_factory->widgets['WP_Widget_Search']->show_instance_in_rest = false; + $request = new WP_REST_Request( 'POST', '/wp/v2/widget-types/search/encode' ); + $request->set_param( + 'instance', + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( + "<p>\n" . + "\t\t\t<label for=\"widget-search-1-title\">Title:</label>\n" . + "\t\t\t<input class=\"widefat\" id=\"widget-search-1-title\" name=\"widget-search[1][title]\" type=\"text\" value=\"Test title\" />\n" . + "\t\t</p>", + $data['form'] + ); + $this->assertEqualSets( + array( + 'encoded' => base64_encode( serialize( array( 'title' => 'Test title' ) ) ), + 'hash' => wp_hash( serialize( array( 'title' => 'Test title' ) ) ), + ), + $data['instance'] + ); + $wp_widget_factory->widgets['WP_Widget_Search']->show_instance_in_rest = true; + } + + /** * The test_create_item() method does not exist for widget types. */ diff --git a/phpunit/class-rest-widgets-controller-test.php b/phpunit/class-rest-widgets-controller-test.php index b0740a44b57266..0e215c0e658e07 100644 --- a/phpunit/class-rest-widgets-controller-test.php +++ b/phpunit/class-rest-widgets-controller-test.php @@ -90,24 +90,72 @@ public static function wpSetUpBeforeClass( $factory ) { } public function setUp() { + global $wp_registered_widgets, $wp_registered_sidebars, $_wp_sidebars_widgets, $wp_widget_factory; + parent::setUp(); wp_set_current_user( self::$admin_id ); // Unregister all widgets and sidebars. - global $wp_registered_sidebars, $_wp_sidebars_widgets; + $wp_registered_widgets = array(); $wp_registered_sidebars = array(); $_wp_sidebars_widgets = array(); update_option( 'sidebars_widgets', array() ); + + // Re-register core widgets. + $wp_widget_factory->_register_widgets(); + + // Register a non-multi widget for testing. + wp_register_widget_control( + 'testwidget', + 'WP test widget', + function () { + $settings = get_option( 'widget_testwidget' ); + + // check if anything's been sent. + if ( isset( $_POST['update_testwidget'] ) ) { + $settings['id'] = $_POST['test_id']; + $settings['title'] = $_POST['test_title']; + + update_option( 'widget_testwidget', $settings ); + } + + echo 'WP test widget form'; + }, + 100, + 200 + ); + wp_register_sidebar_widget( + 'testwidget', + 'WP test widget', + function () { + $settings = wp_parse_args( + get_option( 'widget_testwidget' ), + array( + 'id' => 'Default id', + 'title' => 'Default text', + ) + ); + echo '<h1>' . $settings['id'] . '</h1><span>' . $settings['title'] . '</span>'; + }, + array( + 'description' => 'A non-multi widget for testing.', + ) + ); } - private function setup_widget( $option_name, $number, $settings ) { + private function setup_widget( $id_base, $number, $settings ) { + $option_name = "widget_$id_base"; update_option( $option_name, array( $number => $settings, ) ); + + $widget_object = gutenberg_get_widget_object( $id_base ); + $widget_object->_set( $number ); + $widget_object->_register_one( $number ); } private function setup_sidebar( $id, $attrs = array(), $widgets = array() ) { @@ -131,13 +179,6 @@ private function setup_sidebar( $id, $attrs = array(), $widgets = array() ) { ), $attrs ); - - global $wp_registered_widgets; - foreach ( $wp_registered_widgets as $wp_registered_widget ) { - if ( is_array( $wp_registered_widget['callback'] ) ) { - $wp_registered_widget['callback'][0]->_register(); - } - } } /** @@ -190,18 +231,24 @@ public function test_get_items_wrong_permission_author() { * @ticket 51460 */ public function test_get_items() { + global $wp_widget_factory; + + $wp_widget_factory->widgets['WP_Widget_RSS']->show_instance_in_rest = false; + + $block_content = '<!-- wp:paragraph --><p>Block test</p><!-- /wp:paragraph -->'; + $this->setup_widget( - 'widget_rss', + 'rss', 1, array( 'title' => 'RSS test', ) ); $this->setup_widget( - 'widget_text', + 'block', 1, array( - 'text' => 'Custom text test', + 'content' => $block_content, ) ); $this->setup_sidebar( @@ -209,7 +256,7 @@ public function test_get_items() { array( 'name' => 'Test sidebar', ), - array( 'text-1', 'rss-1' ) + array( 'block-1', 'rss-1', 'testwidget' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); @@ -219,17 +266,36 @@ public function test_get_items() { $this->assertEqualSets( array( array( - 'id' => 'text-1', + 'id' => 'block-1', 'sidebar' => 'sidebar-1', 'settings' => array( - 'text' => 'Custom text test', + 'content' => $block_content, + ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'content' => $block_content, + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'content' => $block_content, + ) + ) + ), + 'raw' => array( + 'content' => $block_content, + ), ), - 'id_base' => 'text', - 'widget_class' => 'WP_Widget_Text', - 'name' => 'Text', - 'description' => 'Arbitrary text.', + 'id_base' => 'block', + 'widget_class' => 'WP_Widget_Block', + 'name' => 'Block', + 'description' => 'Gutenberg block.', 'number' => 1, - 'rendered' => '<div class="textwidget">Custom text test</div>', + 'rendered' => '<p>Block test</p>', ), array( 'id' => 'rss-1', @@ -237,6 +303,22 @@ public function test_get_items() { 'settings' => array( 'title' => 'RSS test', ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'title' => 'RSS test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'title' => 'RSS test', + ) + ) + ), + ), 'id_base' => 'rss', 'widget_class' => 'WP_Widget_RSS', 'name' => 'RSS', @@ -244,9 +326,23 @@ public function test_get_items() { 'number' => 1, 'rendered' => '', ), + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'settings' => array(), + 'instance' => null, + 'id_base' => 'testwidget', + 'widget_class' => '', + 'name' => 'WP test widget', + 'description' => 'A non-multi widget for testing.', + 'number' => 0, + 'rendered' => '<h1>Default id</h1><span>Default text</span>', + ), ), $data ); + + $wp_widget_factory->widgets['WP_Widget_RSS']->show_instance_in_rest = true; } /** @@ -254,7 +350,7 @@ public function test_get_items() { */ public function test_get_items_edit_context() { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -265,7 +361,7 @@ public function test_get_items_edit_context() { array( 'name' => 'Test sidebar', ), - array( 'text-1' ) + array( 'text-1', 'testwidget' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); @@ -281,6 +377,25 @@ public function test_get_items_edit_context() { 'settings' => array( 'text' => 'Custom text test', ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), 'id_base' => 'text', 'widget_class' => 'WP_Widget_Text', 'name' => 'Text', @@ -292,6 +407,19 @@ public function test_get_items_edit_context() { ' <input id="widget-text-1-filter" name="widget-text[1][filter]" class="filter sync-input" type="hidden" value="on">' . "\n" . ' <input id="widget-text-1-visual" name="widget-text[1][visual]" class="visual sync-input" type="hidden" value="on">', ), + array( + 'id' => 'testwidget', + 'sidebar' => 'sidebar-1', + 'settings' => array(), + 'instance' => null, + 'id_base' => 'testwidget', + 'widget_class' => '', + 'name' => 'WP test widget', + 'description' => 'A non-multi widget for testing.', + 'number' => 0, + 'rendered' => '<h1>Default id</h1><span>Default text</span>', + 'rendered_form' => 'WP test widget form', + ), ), $data ); @@ -302,7 +430,7 @@ public function test_get_items_edit_context() { */ public function test_get_item() { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -326,6 +454,25 @@ public function test_get_item() { 'settings' => array( 'text' => 'Custom text test', ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), 'id_base' => 'text', 'widget_class' => 'WP_Widget_Text', 'name' => 'Text', @@ -344,7 +491,7 @@ public function test_get_item_no_permission() { wp_set_current_user( 0 ); $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -369,7 +516,7 @@ public function test_get_item_no_permission() { public function test_get_item_wrong_permission_author() { wp_set_current_user( self::$author_id ); $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -398,6 +545,213 @@ public function test_create_item() { ) ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + get_option( 'widget_text' )[2] + ); + } + + /** + * @ticket 51460 + */ + public function test_create_item_malformed_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Updated text test', + ) + ) + ), + 'hash' => 'badhash', + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + } + + /** + * @ticket 51460 + */ + public function test_create_item_bad_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array(), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + } + + /** + * @ticket 51460 + */ + public function test_create_item_using_raw_instance() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'content' => '<!-- wp:paragraph --><p>Block test</p><!-- /wp:paragraph -->', + ), + ), + 'id_base' => 'block', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'block-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEqualSets( + array( + 'content' => '<!-- wp:paragraph --><p>Block test</p><!-- /wp:paragraph -->', + ), + get_option( 'widget_block' )[2] + ); + } + + /** + * @ticket 51460 + */ + public function test_create_item_raw_instance_not_supported() { + global $wp_widget_factory; + + $wp_widget_factory->widgets['WP_Widget_Text']->show_instance_in_rest = false; + + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'instance' => array( + 'raw' => array( + 'title' => 'Updated text test', + ), + ), + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_widget', $response, 400 ); + + $wp_widget_factory->widgets['WP_Widget_Text']->show_instance_in_rest = true; + } + + /** + * @ticket 51460 + */ + public function test_create_item_using_form_data() { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); + $request->set_body_params( + array( + 'sidebar' => 'sidebar-1', + 'form_data' => 'widget-text[2][text]=Updated+text+test', + 'id_base' => 'text', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'sidebar-1', $data['sidebar'] ); + $this->assertEquals( 2, $data['number'] ); + $this->assertEqualSets( + array( + 'text' => 'Updated text test', + 'title' => '', + 'filter' => false, + ), + $data['settings'] + ); + } + + /** + * @ticket 51460 + */ + public function test_create_item_using_settings() { + $this->setExpectedDeprecated( 'settings' ); + + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/widgets' ); $request->set_body_params( array( @@ -410,9 +764,9 @@ public function test_create_item() { ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertEquals( 'text-1', $data['id'] ); + $this->assertEquals( 'text-2', $data['id'] ); $this->assertEquals( 'sidebar-1', $data['sidebar'] ); - $this->assertEquals( 1, $data['number'] ); + $this->assertEquals( 2, $data['number'] ); $this->assertEqualSets( array( 'text' => 'Updated text test', @@ -427,6 +781,8 @@ public function test_create_item() { * @ticket 51460 */ public function test_create_item_multiple_in_a_row() { + $this->setExpectedDeprecated( 'settings' ); + $this->setup_sidebar( 'sidebar-1', array( @@ -444,9 +800,9 @@ public function test_create_item_multiple_in_a_row() { ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertEquals( 'text-1', $data['id'] ); + $this->assertEquals( 'text-2', $data['id'] ); $this->assertEquals( 'sidebar-1', $data['sidebar'] ); - $this->assertEquals( 1, $data['number'] ); + $this->assertEquals( 2, $data['number'] ); $this->assertEqualSets( array( 'text' => 'Text 1', @@ -466,9 +822,9 @@ public function test_create_item_multiple_in_a_row() { ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertEquals( 'text-2', $data['id'] ); + $this->assertEquals( 'text-3', $data['id'] ); $this->assertEquals( 'sidebar-1', $data['sidebar'] ); - $this->assertEquals( 2, $data['number'] ); + $this->assertEquals( 3, $data['number'] ); $this->assertEqualSets( array( 'text' => 'Text 2', @@ -479,16 +835,18 @@ public function test_create_item_multiple_in_a_row() { ); $sidebar = rest_do_request( '/wp/v2/sidebars/sidebar-1' ); - $this->assertContains( 'text-1', $sidebar->get_data()['widgets'] ); $this->assertContains( 'text-2', $sidebar->get_data()['widgets'] ); + $this->assertContains( 'text-3', $sidebar->get_data()['widgets'] ); } /** * @ticket 51460 */ public function test_create_item_second_instance() { + $this->setExpectedDeprecated( 'settings' ); + $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -530,8 +888,10 @@ public function test_create_item_second_instance() { * @ticket 51460 */ public function test_update_item() { + $this->setExpectedDeprecated( 'settings' ); + $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -581,7 +941,7 @@ public function test_update_item() { */ public function test_update_item_reassign_sidebar() { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -624,8 +984,10 @@ public function test_update_item_reassign_sidebar() { * @ticket 51460 */ public function test_update_item_shouldnt_require_id_base() { + $this->setExpectedDeprecated( 'settings' ); + $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -668,6 +1030,7 @@ public function test_update_item_shouldnt_require_id_base() { * @group multisite */ public function test_store_html_as_admin() { + $this->setExpectedDeprecated( 'settings' ); if ( is_multisite() ) { $this->assertEquals( '<div class="textwidget">alert(1)</div>', @@ -685,6 +1048,7 @@ public function test_store_html_as_admin() { * @group multisite */ public function test_store_html_as_superadmin() { + $this->setExpectedDeprecated( 'settings' ); wp_set_current_user( self::$superadmin_id ); if ( is_multisite() ) { $this->assertEquals( @@ -701,7 +1065,7 @@ public function test_store_html_as_superadmin() { protected function update_text_widget_with_raw_html( $html ) { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -738,62 +1102,21 @@ protected function update_text_widget_with_raw_html( $html ) { /** * @ticket 51460 */ - public function test_update_item_legacy_widget_1() { - $this->do_test_update_item_legacy_widget( 'testwidget-1' ); - } - - /** - * @ticket 51460 - */ - public function test_update_item_legacy_widget_2() { - $this->do_test_update_item_legacy_widget( 'testwidget' ); - } + public function test_update_item_legacy_widget() { + $this->setExpectedDeprecated( 'settings' ); - /** - * @ticket 51460 - */ - public function do_test_update_item_legacy_widget( $widget_id ) { - // @TODO: Use @dataProvider instead (it doesn't work with custom constructors like the one we have in this class) - wp_register_widget_control( - $widget_id, - 'WP test widget', - function () { - $settings = get_option( 'widget_testwidget' ); - - // check if anything's been sent. - if ( isset( $_POST['update_testwidget'] ) ) { - $settings['id'] = $_POST['test_id']; - $settings['title'] = $_POST['test_title']; - - update_option( 'widget_testwidget', $settings ); - } - }, - 100, - 200 - ); - wp_register_sidebar_widget( - $widget_id, - 'WP test widget', - function () { - $settings = get_option( 'widget_testwidget' ) ? get_option( 'widget_testwidget' ) : array( - 'id' => '', - 'title' => '', - ); - echo '<h1>' . $settings['id'] . '</h1><span>' . $settings['title'] . '</span>'; - } - ); $this->setup_sidebar( 'sidebar-1', array( 'name' => 'Test sidebar', ), - array( $widget_id ) + array( 'testwidget' ) ); - $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/' . $widget_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/testwidget' ); $request->set_body_params( array( - 'id' => $widget_id, + 'id' => 'testwidget', 'name' => 'WP test widget', 'settings' => array( 'test_id' => 'My test id', @@ -807,16 +1130,17 @@ function () { $data = $this->remove_links( $data ); $this->assertEquals( array( - 'id' => $widget_id, + 'id' => 'testwidget', 'sidebar' => 'sidebar-1', 'settings' => array(), + 'instance' => null, 'rendered' => '<h1>My test id</h1><span>My test title</span>', 'name' => 'WP test widget', 'number' => 0, - 'rendered_form' => '', + 'rendered_form' => 'WP test widget form', 'widget_class' => '', - 'id_base' => '', - 'description' => '', + 'id_base' => 'testwidget', + 'description' => 'A non-multi widget for testing.', ), $data ); @@ -825,50 +1149,9 @@ function () { /** * @ticket 51460 */ - public function test_create_item_legacy_widget_1() { - $this->do_test_create_item_legacy_widget( 'testwidget-1' ); - } - - /** - * @ticket 51460 - */ - public function test_create_item_legacy_widget_2() { - $this->do_test_create_item_legacy_widget( 'testwidget' ); - } + public function test_create_item_legacy_widget() { + $this->setExpectedDeprecated( 'settings' ); - /** - * @ticket 51460 - */ - public function do_test_create_item_legacy_widget( $widget_id ) { - // @TODO: Use @dataProvider instead (it doesn't work with custom constructors like the one we have in this class) - wp_register_widget_control( - $widget_id, - 'WP test widget', - function () { - $settings = get_option( 'widget_testwidget' ); - - // check if anything's been sent. - if ( isset( $_POST['update_testwidget'] ) ) { - $settings['id'] = $_POST['test_id']; - $settings['title'] = $_POST['test_title']; - - update_option( 'widget_testwidget', $settings ); - } - }, - 100, - 200 - ); - wp_register_sidebar_widget( - $widget_id, - 'WP test widget', - function () { - $settings = get_option( 'widget_testwidget' ) ? get_option( 'widget_testwidget' ) : array( - 'id' => '', - 'title' => '', - ); - echo '<h1>' . $settings['id'] . '</h1><span>' . $settings['title'] . '</span>'; - } - ); $this->setup_sidebar( 'sidebar-1', array( @@ -877,10 +1160,10 @@ function () { array() ); - $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/' . $widget_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/testwidget' ); $request->set_body_params( array( - 'id' => $widget_id, + 'id' => 'testwidget', 'sidebar' => 'sidebar-1', 'name' => 'WP test widget', 'settings' => array( @@ -895,16 +1178,17 @@ function () { $data = $this->remove_links( $data ); $this->assertEquals( array( - 'id' => $widget_id, + 'id' => 'testwidget', 'sidebar' => 'sidebar-1', 'settings' => array(), + 'instance' => null, 'rendered' => '<h1>My test id</h1><span>My test title</span>', 'name' => 'WP test widget', 'number' => 0, - 'rendered_form' => '', + 'rendered_form' => 'WP test widget form', 'widget_class' => '', - 'id_base' => '', - 'description' => '', + 'id_base' => 'testwidget', + 'description' => 'A non-multi widget for testing.', ), $data ); @@ -946,7 +1230,9 @@ public function test_update_item_wrong_permission_author() { * Tests if the endpoint correctly handles "slashable" characters such as " or '. */ public function test_update_item_slashing() { - $this->setup_widget( 'widget_text', 1, array( 'text' => 'Custom text test' ) ); + $this->setExpectedDeprecated( 'settings' ); + + $this->setup_widget( 'text', 1, array( 'text' => 'Custom text test' ) ); $this->setup_sidebar( 'sidebar-1', array( 'name' => 'Test sidebar' ), array( 'text-1', 'rss-1' ) ); $request = new WP_REST_Request( 'PUT', '/wp/v2/widgets/text-1' ); @@ -987,7 +1273,7 @@ public function test_update_item_slashing() { */ public function test_delete_item() { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -1011,6 +1297,25 @@ public function test_delete_item() { 'settings' => array( 'text' => 'Custom text test', ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), 'id_base' => 'text', 'widget_class' => 'WP_Widget_Text', 'name' => 'Text', @@ -1031,7 +1336,7 @@ public function test_delete_item() { */ public function test_delete_item_force() { $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -1059,6 +1364,25 @@ public function test_delete_item_force() { 'settings' => array( 'text' => 'Custom text test', ), + 'instance' => array( + 'encoded' => base64_encode( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'hash' => wp_hash( + serialize( + array( + 'text' => 'Custom text test', + ) + ) + ), + 'raw' => array( + 'text' => 'Custom text test', + ), + ), 'id_base' => 'text', 'widget_class' => 'WP_Widget_Text', 'name' => 'Text', @@ -1086,7 +1410,7 @@ public function test_delete_item_logged_out() { wp_set_current_user( 0 ); $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -1113,7 +1437,7 @@ public function test_delete_item_author() { wp_set_current_user( self::$author_id ); $this->setup_widget( - 'widget_text', + 'text', 1, array( 'text' => 'Custom text test', @@ -1149,7 +1473,7 @@ public function test_get_item_schema() { $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertEquals( 10, count( $properties ) ); + $this->assertEquals( 12, count( $properties ) ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'id_base', $properties ); $this->assertArrayHasKey( 'sidebar', $properties ); @@ -1160,6 +1484,8 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'rendered', $properties ); $this->assertArrayHasKey( 'rendered_form', $properties ); $this->assertArrayHasKey( 'settings', $properties ); + $this->assertArrayHasKey( 'instance', $properties ); + $this->assertArrayHasKey( 'form_data', $properties ); } /** diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index 6c2fa216bc9b0b..66c6d946c47210 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -8,8 +8,40 @@ class WP_Theme_JSON_Resolver_Test extends WP_UnitTestCase { - function test_presets_are_extracted() { - $actual = WP_Theme_JSON_Resolver::get_presets_to_translate(); + function setUp() { + parent::setUp(); + $this->theme_root = realpath( __DIR__ . '/data/themedir1' ); + + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + function tearDown() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + parent::tearDown(); + } + + function filter_set_theme_root() { + return $this->theme_root; + } + + function filter_set_locale_to_polish() { + return 'pl_PL'; + } + + function test_fields_are_extracted() { + $actual = WP_Theme_JSON_Resolver::get_fields_to_translate(); $expected = array( array( @@ -52,8 +84,54 @@ function test_presets_are_extracted() { 'key' => 'name', 'context' => 'Gradient name', ), + array( + 'path' => array( 'customTemplates' ), + 'key' => 'title', + 'context' => 'Custom template name', + ), ); $this->assertEquals( $expected, $actual ); } + + function test_translations_are_applied() { + add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); + load_textdomain( 'fse', realpath( __DIR__ . '/data/languages/themes/fse-pl_PL.mo' ) ); + + switch_theme( 'fse' ); + + $actual = WP_Theme_JSON_Resolver::get_theme_data(); + + unload_textdomain( 'fse' ); + remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); + + $this->assertSame( wp_get_theme()->get( 'TextDomain' ), 'fse' ); + $this->assertSame( + $actual->get_settings()['root']['color'], + array( + 'palette' => array( + array( + 'slug' => 'light', + 'name' => 'Jasny', + 'color' => '#f5f7f9', + ), + array( + 'slug' => 'dark', + 'name' => 'Ciemny', + 'color' => '#000', + ), + ), + 'custom' => false, + ) + ); + $this->assertSame( + $actual->get_custom_templates(), + array( + 'page-home' => array( + 'title' => 'Szablon strony gล‚รณwnej', + 'postTypes' => array( 'page' ), + ), + ) + ); + } } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 81e77bcb234692..baedc89d747632 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -145,41 +145,6 @@ function test_schema_validation_subtree_is_removed_if_empty() { $this->assertEqualSetsWithIndex( $expected, $result ); } - function test_schema_validation_subtree_is_removed_if_style_not_supported_by_block() { - $root_name = WP_Theme_JSON::ROOT_BLOCK_NAME; - $theme_json = new WP_Theme_JSON( - array( - 'styles' => array( - $root_name => array( - 'color' => array( - 'text' => 'var:preset|color|dark-gray', - ), - 'spacing' => array( - 'padding' => array( - 'top' => '1px', - 'right' => '1px', - 'bottom' => '1px', - 'left' => '1px', - ), - ), - ), - ), - ) - ); - - $actual = $theme_json->get_raw_data(); - $expected = array( - 'styles' => array( - $root_name => array( - 'color' => array( - 'text' => 'var:preset|color|dark-gray', - ), - ), - ), - ); - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - function test_get_settings() { $root_name = WP_Theme_JSON::ROOT_BLOCK_NAME; // See schema at WP_Theme_JSON::SCHEMA. @@ -813,8 +778,9 @@ function test_get_custom_templates() { $theme_json = new WP_Theme_JSON( array( 'customTemplates' => array( - 'page-home' => array( - 'title' => 'Some title', + array( + 'name' => 'page-home', + 'title' => 'Homepage template', ), ), ) @@ -826,7 +792,8 @@ function test_get_custom_templates() { $page_templates, array( 'page-home' => array( - 'title' => 'Some title', + 'title' => 'Homepage template', + 'postTypes' => array( 'page' ), ), ) ); @@ -836,8 +803,9 @@ function test_get_template_parts() { $theme_json = new WP_Theme_JSON( array( 'templateParts' => array( - 'header' => array( - 'area' => 'Some area', + array( + 'name' => 'small-header', + 'area' => 'header', ), ), ) @@ -848,8 +816,8 @@ function test_get_template_parts() { $this->assertEqualSetsWithIndex( $template_parts, array( - 'header' => array( - 'area' => 'Some area', + 'small-header' => array( + 'area' => 'header', ), ) ); diff --git a/phpunit/data/languages/themes/fse-pl_PL.mo b/phpunit/data/languages/themes/fse-pl_PL.mo new file mode 100644 index 00000000000000..0ff620e64ae4ad Binary files /dev/null and b/phpunit/data/languages/themes/fse-pl_PL.mo differ diff --git a/phpunit/data/languages/themes/fse-pl_PL.po b/phpunit/data/languages/themes/fse-pl_PL.po new file mode 100644 index 00000000000000..d55e02b8b54682 --- /dev/null +++ b/phpunit/data/languages/themes/fse-pl_PL.po @@ -0,0 +1,31 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: 2015-12-31 16:31+0100\n" +"PO-Revision-Date: 2021-03-15 13:10+0100\n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.2\n" +"X-Poedit-Basepath: .\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Poedit-KeywordsList: __;_e;_x:1,2c;_ex:1,2c;_n:1,2;_nx:1,2,4c;_n_noop:1,2;" +"_nx_noop:1,2,3c;esc_attr__;esc_html__;esc_attr_e;esc_html_e;esc_attr_x:1,2c;" +"esc_html_x:1,2c\n" +"X-Textdomain-Support: yes\n" +"Last-Translator: \n" +"Language-Team: \n" +"X-Poedit-SearchPath-0: .\n" + +msgctxt "Custom template name" +msgid "Homepage template" +msgstr "Szablon strony gล‚รณwnej" + +msgctxt "Color name" +msgid "Light" +msgstr "Jasny" + +msgctxt "Color name" +msgid "Dark" +msgstr "Ciemny" diff --git a/phpunit/data/themedir1/fse/experimental-theme.json b/phpunit/data/themedir1/fse/experimental-theme.json new file mode 100644 index 00000000000000..3cfe4accc5bebe --- /dev/null +++ b/phpunit/data/themedir1/fse/experimental-theme.json @@ -0,0 +1,33 @@ +{ + "settings": { + "root": { + "color": { + "palette": [ + { + "slug": "light", + "name": "Light", + "color": "#f5f7f9" + }, + { + "slug": "dark", + "name": "Dark", + "color": "#000" + } + ], + "custom": false + } + } + }, + "customTemplates": [ + { + "name": "page-home", + "title": "Homepage template" + } + ], + "templateParts": [ + { + "name": "small-header", + "area": "header" + } + ] +} diff --git a/phpunit/data/themedir1/fse/style.css b/phpunit/data/themedir1/fse/style.css new file mode 100644 index 00000000000000..efc417305327a2 --- /dev/null +++ b/phpunit/data/themedir1/fse/style.css @@ -0,0 +1,7 @@ +/* +Theme Name: FSE Theme +Theme URI: https://wordpress.org/ +Description: For testing purposes only. +Version: 1.0.0 +Text Domain: fse +*/ diff --git a/readme.txt b/readme.txt index c8b58929f235bc..55143c81c6ddd2 100644 --- a/readme.txt +++ b/readme.txt @@ -1,8 +1,6 @@ === Gutenberg === Contributors: matveb, joen, karmatosed -Requires at least: 5.6 Tested up to: 5.7 -Requires PHP: 5.6 Stable tag: V.V.V License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -57,4 +55,4 @@ View <a href="https://developer.wordpress.org/block-editor/principles/versions-i == Changelog == -To read the changelog for Gutenberg 10.2.0-rc.1, please navigate to the <a href="https://github.com/WordPress/gutenberg/releases/tag/v10.2.0-rc.1">release page</a>. +To read the changelog for Gutenberg 10.3.1, please navigate to the <a href="https://github.com/WordPress/gutenberg/releases/tag/v10.3.1">release page</a>. diff --git a/storybook/stories/playground/index.js b/storybook/stories/playground/index.js index 93c1d8c0183c52..8bd3adfab9eeba 100644 --- a/storybook/stories/playground/index.js +++ b/storybook/stories/playground/index.js @@ -44,6 +44,7 @@ function App() { </div> <div className="editor-styles-wrapper"> <Popover.Slot name="block-toolbar" /> + <BlockEditorKeyboardShortcuts.Register /> <BlockEditorKeyboardShortcuts /> <WritingFlow> <ObserveTyping> diff --git a/test/integration/blocks-raw-handling.test.js b/test/integration/blocks-raw-handling.test.js index 4493e202294828..8fac54be759151 100644 --- a/test/integration/blocks-raw-handling.test.js +++ b/test/integration/blocks-raw-handling.test.js @@ -146,6 +146,28 @@ describe( 'Blocks raw handling', () => { expect( console ).toHaveLogged(); } ); + it( 'should paste special whitespace', () => { + const filtered = pasteHandler( { + HTML: '<p>&thinsp;</p>', + plainText: 'โ€‰', + mode: 'AUTO', + } ); + + expect( console ).toHaveLogged(); + expect( filtered ).toBe( 'โ€‰' ); + } ); + + it( 'should paste special whitespace in plain text only', () => { + const filtered = pasteHandler( { + HTML: '', + plainText: 'โ€‰', + mode: 'AUTO', + } ); + + expect( console ).toHaveLogged(); + expect( filtered ).toBe( 'โ€‰' ); + } ); + it( 'should parse Markdown', () => { const filtered = pasteHandler( { HTML: '* one<br>* two<br>* three', diff --git a/test/native/__mocks__/react-native-aztec/index.js b/test/native/__mocks__/react-native-aztec/index.js deleted file mode 100644 index 6f0797e23d820a..00000000000000 --- a/test/native/__mocks__/react-native-aztec/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/** - * WordPress dependencies - */ -import { Component } from '@wordpress/element'; - -class AztecView extends Component { - blur = () => {}; - - focus = () => {}; - - isFocused = () => { - return false; - }; - - render() { - return <></>; - } -} - -export default AztecView; diff --git a/test/native/jest.config.js b/test/native/jest.config.js index 8a823af611f70d..2da93faad1859e 100644 --- a/test/native/jest.config.js +++ b/test/native/jest.config.js @@ -62,7 +62,6 @@ module.exports = { haste: { defaultPlatform: rnPlatform, platforms: [ 'android', 'ios', 'native' ], - providesModuleNodeModules: [ 'react-native', 'react-native-svg' ], }, transformIgnorePatterns: [ // This is required for now to have jest transform some of our modules diff --git a/test/native/setup.js b/test/native/setup.js index 5a691f2698b9d2..6e6534d70d8711 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import { NativeModules } from 'react-native'; import 'react-native-gesture-handler/jestSetup'; jest.mock( '@wordpress/element', () => { @@ -106,27 +105,6 @@ jest.mock( '@react-native-community/blur', () => () => 'BlurView', { virtual: true, } ); -// Overwrite some native module mocks from `react-native` jest preset: -// https://github.com/facebook/react-native/blob/HEAD/jest/setup.js -// to fix issue "TypeError: Cannot read property 'Commands' of undefined" -// raised when calling focus or blur on a native component -const mockNativeModules = { - UIManager: { - ...NativeModules.UIManager, - getViewManagerConfig: jest.fn( () => ( { Commands: {} } ) ), - }, -}; - -Object.keys( mockNativeModules ).forEach( ( module ) => { - try { - jest.doMock( module, () => mockNativeModules[ module ] ); // needed by FacebookSDK-test - } catch ( error ) { - jest.doMock( module, () => mockNativeModules[ module ], { - virtual: true, - } ); - } -} ); - jest.mock( 'react-native-reanimated', () => { const Reanimated = require( 'react-native-reanimated/mock' ); @@ -137,5 +115,14 @@ jest.mock( 'react-native-reanimated', () => { return Reanimated; } ); -// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing +// Silence the warning: Animated: `useNativeDriver` is not supported because the +// native animated module is missing. This was added per React Navigation docs. +// https://reactnavigation.org/docs/testing/#mocking-native-modules jest.mock( 'react-native/Libraries/Animated/src/NativeAnimatedHelper' ); + +// We currently reference TextStateInput (a private module) within +// react-native-aztec/src/AztecView. Doing so requires that we mock it via its +// internal path to avoid "TypeError: Cannot read property 'Commands' of +// undefined." The private module referenced could possibly be replaced with +// a React ref instead. We could then remove this internal mock. +jest.mock( 'react-native/Libraries/Components/TextInput/TextInputState' ); diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 70ae8191dc0d8d..346c2f288efd44 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -4,7 +4,7 @@ const glob = require( 'glob' ).sync; // Finds all packages which are transpiled with Babel to force Jest to use their source code. -const transpiledPackageNames = glob( 'packages/*/src/index.js' ).map( +const transpiledPackageNames = glob( 'packages/*/src/index.{js,ts,tsx}' ).map( ( fileName ) => fileName.split( '/' )[ 1 ] ); diff --git a/tsconfig.json b/tsconfig.json index 76591f91a5b43c..72d6dc72c2d39d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,7 @@ { "path": "packages/blob" }, { "path": "packages/block-editor" }, { "path": "packages/components" }, + { "path": "packages/date" }, { "path": "packages/deprecated" }, { "path": "packages/docgen" }, { "path": "packages/dom" },