diff --git a/bin/plugin/commands/common.js b/bin/plugin/commands/common.js index fc540b073a1ff4..59abfa93447ef3 100644 --- a/bin/plugin/commands/common.js +++ b/bin/plugin/commands/common.js @@ -1,72 +1,26 @@ /** * External dependencies */ -const fs = require( 'fs' ); -const rimraf = require( 'rimraf' ); const semver = require( 'semver' ); +const SimpleGit = require( 'simple-git' ); /** * Internal dependencies */ -const { log, formats } = require( '../lib/logger' ); -const { runStep, readJSONFile } = require( '../lib/utils' ); -const git = require( '../lib/git' ); -const config = require( '../config' ); - -/** - * Clone the repository and returns the working directory. - * - * @param {string} abortMessage Abort message. - * - * @return {Promise} Repository local path. - */ -async function runGitRepositoryCloneStep( abortMessage ) { - // Cloning the repository. - let gitWorkingDirectoryPath; - await runStep( 'Cloning the Git repository', abortMessage, async () => { - log( '>> Cloning the Git repository' ); - gitWorkingDirectoryPath = await git.clone( config.gitRepositoryURL ); - log( - '>> The Git repository has been successfully cloned in the following temporary folder: ' + - formats.success( gitWorkingDirectoryPath ) - ); - } ); - - return gitWorkingDirectoryPath; -} - -/** - * Clean the working directories. - * - * @param {string[]} folders Folders to clean. - * @param {string} abortMessage Abort message. - */ -async function runCleanLocalFoldersStep( folders, abortMessage ) { - await runStep( 'Cleaning the temporary folders', abortMessage, async () => { - await Promise.all( - folders.map( async ( directoryPath ) => { - if ( fs.existsSync( directoryPath ) ) { - await rimraf( directoryPath, ( err ) => { - if ( err ) { - throw err; - } - } ); - } - } ) - ); - } ); -} +const { readJSONFile } = require( '../lib/utils' ); /** * Finds the name of the current plugin release branch based on the version in - * the package.json file. + * the package.json file and the latest `trunk` branch in `git`. * * @param {string} gitWorkingDirectoryPath Path to the project's working directory. * * @return {string} Name of the plugin release branch. */ async function findPluginReleaseBranchName( gitWorkingDirectoryPath ) { - await git.checkoutRemoteBranch( gitWorkingDirectoryPath, 'trunk' ); + await SimpleGit( gitWorkingDirectoryPath ) + .fetch( 'origin', 'trunk' ) + .checkout( 'trunk' ); const packageJsonPath = gitWorkingDirectoryPath + '/package.json'; const mainPackageJson = readJSONFile( packageJsonPath ); @@ -141,6 +95,4 @@ function calculateVersionBumpFromChangelog( module.exports = { calculateVersionBumpFromChangelog, findPluginReleaseBranchName, - runGitRepositoryCloneStep, - runCleanLocalFoldersStep, }; diff --git a/bin/plugin/commands/packages.js b/bin/plugin/commands/packages.js index e715ee69345ad0..c085201a235001 100644 --- a/bin/plugin/commands/packages.js +++ b/bin/plugin/commands/packages.js @@ -6,20 +6,24 @@ const path = require( 'path' ); const glob = require( 'fast-glob' ); const fs = require( 'fs' ); const { inc: semverInc } = require( 'semver' ); +const rimraf = require( 'rimraf' ); const readline = require( 'readline' ); +const SimpleGit = require( 'simple-git' ); /** * Internal dependencies */ const { log, formats } = require( '../lib/logger' ); -const { askForConfirmation, runStep, readJSONFile } = require( '../lib/utils' ); +const { + askForConfirmation, + runStep, + readJSONFile, + getRandomTemporaryPath, +} = require( '../lib/utils' ); const { calculateVersionBumpFromChangelog, findPluginReleaseBranchName, - runGitRepositoryCloneStep, - runCleanLocalFoldersStep, } = require( './common' ); -const git = require( '../lib/git' ); const { join } = require( 'path' ); /** @@ -55,6 +59,17 @@ const { join } = require( 'path' ); * @property {ReleaseType} releaseType The selected release type. */ +/** + * Throws if given an error in the node.js callback style. + * + * @param {any|null} error If callback failed, this will hold a value. + */ +const rethrow = ( error ) => { + if ( error ) { + throw error; + } +}; + /** * Checks out the npm release branch. * @@ -64,9 +79,28 @@ async function checkoutNpmReleaseBranch( { gitWorkingDirectoryPath, npmReleaseBranch, } ) { - // Creating the release branch. - await git.checkoutRemoteBranch( gitWorkingDirectoryPath, npmReleaseBranch ); - await git.fetch( gitWorkingDirectoryPath, [ '--depth=100' ] ); + /* + * Create the release branch. + * + * Note that we are grabbing an arbitrary depth of commits + * during the fetch. When `lerna` attempts to determine if + * a package needs an update, it looks at `git` history, + * and if we have pruned that history it will pre-emptively + * publish when it doesn't need to. + * + * We could set a different arbitrary depth if this isn't + * long enough or if it's excessive. We could also try and + * find a way to more specifically fetch what we expect to + * change. For example, if we knew we'll be performing + * updates every two weeks, we might be conservative and + * use `--shallow-since=4.weeks.ago`. + * + * At the time of writing, a depth of 100 pulls in all + * `trunk` commits from within the past week. + */ + await SimpleGit( gitWorkingDirectoryPath ) + .fetch( npmReleaseBranch, [ '--depth=100' ] ) + .checkout( npmReleaseBranch ); log( '>> The local npm release branch ' + formats.success( npmReleaseBranch ) + @@ -105,13 +139,19 @@ async function runNpmReleaseBranchSyncStep( pluginReleaseBranch, config ) { `>> Syncing the latest plugin release to "${ pluginReleaseBranch }".` ); - await git.replaceContentFromRemoteBranch( - gitWorkingDirectoryPath, - pluginReleaseBranch - ); + const repo = SimpleGit( gitWorkingDirectoryPath ); + + /* + * Replace content from remote branch. + * + * @TODO: What is our goal here? Could `git reset --hard origin/${pluginReleaseBranch}` work? + * Why are we manually removing and then adding files back in? + */ + await repo + .raw( 'rm', '-r', '.' ) + .raw( 'checkout', `origin/${ pluginReleaseBranch }`, '--', '.' ); - const commitHash = await git.commit( - gitWorkingDirectoryPath, + const { commit: commitHash } = await repo.commit( `Merge changes published in the Gutenberg plugin "${ pluginReleaseBranch }" branch` ); @@ -223,6 +263,7 @@ async function updatePackages( config ) { '>> Recommended version bumps based on the changes detected in CHANGELOG files:' ); + // e.g. "2022-11-01T00:13:26.102Z" -> "2022-11-01" const publishDate = new Date().toISOString().split( 'T' )[ 0 ]; await Promise.all( packagesToUpdate.map( @@ -234,11 +275,8 @@ async function updatePackages( config ) { version, } ) => { // Update changelog. - const content = await fs.promises.readFile( - changelogPath, - 'utf8' - ); - await fs.promises.writeFile( + const content = fs.readFileSync( changelogPath, 'utf8' ); + fs.writeFileSync( changelogPath, content.replace( '## Unreleased', @@ -280,11 +318,10 @@ async function updatePackages( config ) { ); } - const commitHash = await git.commit( - gitWorkingDirectoryPath, - 'Update changelog files', - [ './*' ] - ); + const { commit: commitHash } = await SimpleGit( gitWorkingDirectoryPath ) + .add( [ './*' ] ) + .commit( 'Update changelog files' ); + if ( commitHash ) { await runPushGitChangesStep( config ); } @@ -313,8 +350,8 @@ async function runPushGitChangesStep( { abortMessage ); } - await git.pushBranchToOrigin( - gitWorkingDirectoryPath, + await SimpleGit( gitWorkingDirectoryPath ).push( + 'origin', npmReleaseBranch ); } ); @@ -345,9 +382,9 @@ async function publishPackagesToNpm( { stdio: 'inherit', } ); - const beforeCommitHash = await git.getLastCommitHash( + const beforeCommitHash = await SimpleGit( gitWorkingDirectoryPath - ); + ).revparse( [ '--short', 'HEAD' ] ); const yesFlag = interactive ? '' : '--yes'; const noVerifyAccessFlag = interactive ? '' : '--no-verify-access'; @@ -403,8 +440,8 @@ async function publishPackagesToNpm( { ); } - const afterCommitHash = await git.getLastCommitHash( - gitWorkingDirectoryPath + const afterCommitHash = await SimpleGit( gitWorkingDirectoryPath ).revparse( + [ '--short', 'HEAD' ] ); if ( afterCommitHash === beforeCommitHash ) { return; @@ -439,18 +476,23 @@ async function backportCommitsToBranch( log( `>> Backporting commits to "${ branchName }".` ); - await git.resetLocalBranchAgainstOrigin( - gitWorkingDirectoryPath, - branchName - ); + const repo = SimpleGit( gitWorkingDirectoryPath ); + + /* + * Reset any local changes and replace them with the origin branch's copy. + * + * Perform an additional fetch to ensure that when we push our changes that + * it's very unlikely that new commits could have appeared at the origin + * HEAD between when we started running this script and now when we're + * pushing our changes back upstream. + */ + await repo.fetch().checkout( branchName ).pull( 'origin', branchName ); + for ( const commitHash of commits ) { - await git.cherrypickCommitIntoBranch( - gitWorkingDirectoryPath, - branchName, - commitHash - ); + await repo.raw( 'cherry-pick', commitHash ); } - await git.pushBranchToOrigin( gitWorkingDirectoryPath, branchName ); + + await repo.push( 'origin', branchName ); log( `>> Backporting successfully finished.` ); } @@ -478,11 +520,20 @@ async function runPackagesRelease( config, customMessages ) { const temporaryFolders = []; if ( ! config.gitWorkingDirectoryPath ) { - // Cloning the Git repository. - config.gitWorkingDirectoryPath = await runGitRepositoryCloneStep( - config.abortMessage + const gitPath = getRandomTemporaryPath(); + config.gitWorkingDirectoryPath = gitPath; + fs.mkdirSync( gitPath, { recursive: true } ); + temporaryFolders.push( gitPath ); + + await runStep( + 'Cloning the Git repository', + config.abortMessage, + async () => { + log( '>> Cloning the Git repository' ); + await SimpleGit( gitPath ).clone( config.gitRepositoryURL ); + log( ` >> successfully clone into: ${ gitPath }` ); + } ); - temporaryFolders.push( config.gitWorkingDirectoryPath ); } let pluginReleaseBranch; @@ -518,7 +569,16 @@ async function runPackagesRelease( config, customMessages ) { } } - await runCleanLocalFoldersStep( temporaryFolders, 'Cleaning failed.' ); + await runStep( + 'Cleaning the temporary folders', + 'Cleaning failed', + async () => + await Promise.all( + temporaryFolders + .filter( ( tempDir ) => fs.existsSync( tempDir ) ) + .map( ( tempDir ) => rimraf( tempDir, rethrow ) ) + ) + ); log( '\n>> 🎉 WordPress packages are now published!\n\n', diff --git a/bin/plugin/commands/performance.js b/bin/plugin/commands/performance.js index 0571a0e72955fe..2aa8a2c9a7f5c6 100644 --- a/bin/plugin/commands/performance.js +++ b/bin/plugin/commands/performance.js @@ -4,6 +4,7 @@ const fs = require( 'fs' ); const path = require( 'path' ); const { mapValues, kebabCase } = require( 'lodash' ); +const SimpleGit = require( 'simple-git' ); /** * Internal dependencies @@ -15,7 +16,6 @@ const { askForConfirmation, getRandomTemporaryPath, } = require( '../lib/utils' ); -const git = require( '../lib/git' ); const config = require( '../config' ); /** @@ -146,24 +146,6 @@ function curateResults( results ) { }; } -/** - * Set up the given branch for testing. - * - * @param {string} branch Branch name. - * @param {string} environmentDirectory Path to the plugin environment's clone. - */ -async function setUpGitBranch( branch, environmentDirectory ) { - // Restore clean working directory (e.g. if `package-lock.json` has local - // changes after install). - await git.discardLocalChanges( environmentDirectory ); - - log( ' >> Fetching the ' + formats.success( branch ) + ' branch' ); - await git.checkoutRemoteBranch( environmentDirectory, branch ); - - log( ' >> Building the ' + formats.success( branch ) + ' branch' ); - await runShellScript( 'npm ci && npm run build', environmentDirectory ); -} - /** * Runs the performance tests on the current branch. * @@ -214,24 +196,47 @@ async function runPerformanceTests( branches, options ) { // 1- Preparing the tests directory. log( '\n>> Preparing the tests directories' ); log( ' >> Cloning the repository' ); - const baseDirectory = await git.clone( config.gitRepositoryURL ); + + /** + * @type {string[]} git refs against which to run tests; + * could be commit SHA, branch name, tag, etc... + */ + if ( branches.length < 2 ) { + throw new Error( `Need at least two git refs to run` ); + } + + const baseDirectory = getRandomTemporaryPath(); + fs.mkdirSync( baseDirectory, { recursive: true } ); + + // @ts-ignore + const git = SimpleGit( baseDirectory ); + await git + .raw( 'init' ) + .raw( 'remote', 'add', 'origin', config.gitRepositoryURL ); + + for ( const branch of branches ) { + await git.raw( 'fetch', '--depth=1', 'origin', branch ); + } + + await git.raw( 'checkout', branches[ 0 ] ); + const rootDirectory = getRandomTemporaryPath(); const performanceTestDirectory = rootDirectory + '/tests'; await runShellScript( 'mkdir -p ' + rootDirectory ); await runShellScript( 'cp -R ' + baseDirectory + ' ' + performanceTestDirectory ); + if ( !! options.testsBranch ) { - log( - ' >> Fetching the test branch: ' + - formats.success( options.testsBranch ) + - ' branch' - ); - await git.checkoutRemoteBranch( - performanceTestDirectory, - options.testsBranch - ); + const branchName = formats.success( options.testsBranch ); + log( ` >> Fetching the test-runner branch: ${ branchName }` ); + + // @ts-ignore + await SimpleGit( performanceTestDirectory ) + .raw( 'fetch', '--depth=1', 'origin', options.testsBranch ) + .raw( 'checkout', options.testsBranch ); } + log( ' >> Installing dependencies and building packages' ); await runShellScript( 'npm ci && npm run build:packages', @@ -244,16 +249,22 @@ async function runPerformanceTests( branches, options ) { log( '\n>> Preparing an environment directory per branch' ); const branchDirectories = {}; for ( const branch of branches ) { - log( ' >> Branch: ' + branch ); + log( ` >> Branch: ${ branch }` ); const environmentDirectory = rootDirectory + '/envs/' + kebabCase( branch ); // @ts-ignore branchDirectories[ branch ] = environmentDirectory; + const buildPath = `${ environmentDirectory }/plugin`; await runShellScript( 'mkdir ' + environmentDirectory ); - await runShellScript( - 'cp -R ' + baseDirectory + ' ' + environmentDirectory + '/plugin' - ); - await setUpGitBranch( branch, environmentDirectory + '/plugin' ); + await runShellScript( `cp -R ${ baseDirectory } ${ buildPath }` ); + + log( ` >> Fetching the ${ formats.success( branch ) } branch` ); + // @ts-ignore + await SimpleGit( buildPath ).reset( 'hard' ).checkout( branch ); + + log( ` >> Building the ${ formats.success( branch ) } branch` ); + await runShellScript( 'npm ci && npm run build', buildPath ); + await runShellScript( 'cp ' + path.resolve( @@ -302,13 +313,9 @@ async function runPerformanceTests( branches, options ) { formats.success( performanceTestDirectory ) ); for ( const branch of branches ) { - log( - '>> Environment Directory (' + - branch + - ') : ' + - // @ts-ignore - formats.success( branchDirectories[ branch ] ) - ); + // @ts-ignore + const envPath = formats.success( branchDirectories[ branch ] ); + log( `>> Environment Directory (${ branch }) : ${ envPath }` ); } // 4- Running the tests. @@ -328,7 +335,7 @@ async function runPerformanceTests( branches, options ) { for ( const branch of branches ) { // @ts-ignore const environmentDirectory = branchDirectories[ branch ]; - log( ' >> Branch: ' + branch + ', Suite: ' + testSuite ); + log( ` >> Branch: ${ branch }, Suite: ${ testSuite }` ); log( ' >> Starting the environment.' ); await runShellScript( '../../tests/node_modules/.bin/wp-env start', diff --git a/bin/plugin/lib/git.js b/bin/plugin/lib/git.js deleted file mode 100644 index 8208efa7164d1a..00000000000000 --- a/bin/plugin/lib/git.js +++ /dev/null @@ -1,202 +0,0 @@ -// @ts-nocheck -/** - * External dependencies - */ -const SimpleGit = require( 'simple-git' ); - -/** - * Internal dependencies - */ -const { getRandomTemporaryPath } = require( './utils' ); - -/** - * Clones a GitHub repository. - * - * @param {string} repositoryUrl - * - * @return {Promise} Repository local Path - */ -async function clone( repositoryUrl ) { - const gitWorkingDirectoryPath = getRandomTemporaryPath(); - const simpleGit = SimpleGit(); - await simpleGit.clone( repositoryUrl, gitWorkingDirectoryPath, [ - '--depth=1', - '--no-single-branch', - ] ); - return gitWorkingDirectoryPath; -} - -/** - * Fetches changes from the repository. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string[]|Object} options Git options to apply. - */ -async function fetch( gitWorkingDirectoryPath, options = [] ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.fetch( options ); -} - -/** - * Commits changes to the repository. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} message Commit message. - * @param {string[]} filesToAdd Files to add. - * - * @return {Promise} Commit Hash - */ -async function commit( gitWorkingDirectoryPath, message, filesToAdd = [] ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.add( filesToAdd ); - const commitData = await simpleGit.commit( message ); - const commitHash = commitData.commit; - - return commitHash; -} - -/** - * Creates a local branch. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} branchName Branch Name - */ -async function createLocalBranch( gitWorkingDirectoryPath, branchName ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.checkoutLocalBranch( branchName ); -} - -/** - * Checkout a local branch. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} branchName Branch Name - */ -async function checkoutRemoteBranch( gitWorkingDirectoryPath, branchName ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.fetch( 'origin', branchName ); - await simpleGit.checkout( branchName ); -} - -/** - * Creates a local tag. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} tagName Tag Name - */ -async function createLocalTag( gitWorkingDirectoryPath, tagName ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.addTag( tagName ); -} - -/** - * Pushes a local branch to the origin. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} branchName Branch Name - */ -async function pushBranchToOrigin( gitWorkingDirectoryPath, branchName ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.push( 'origin', branchName ); -} - -/** - * Pushes tags to the origin. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - */ -async function pushTagsToOrigin( gitWorkingDirectoryPath ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.pushTags( 'origin' ); -} - -/** - * Discard local changes. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - */ -async function discardLocalChanges( gitWorkingDirectoryPath ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.reset( 'hard' ); -} - -/** - * Reset local branch against the origin. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} branchName Branch Name - */ -async function resetLocalBranchAgainstOrigin( - gitWorkingDirectoryPath, - branchName -) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.fetch(); - await simpleGit.checkout( branchName ); - await simpleGit.pull( 'origin', branchName ); -} - -/** - * Gets the commit hash for the last commit in the current branch. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * - * @return {string} Commit hash. - */ -async function getLastCommitHash( gitWorkingDirectoryPath ) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - return await simpleGit.revparse( [ '--short', 'HEAD' ] ); -} - -/** - * Cherry-picks a commit into trunk - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} branchName Branch name. - * @param {string} commitHash Commit hash. - */ -async function cherrypickCommitIntoBranch( - gitWorkingDirectoryPath, - branchName, - commitHash -) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.checkout( branchName ); - await simpleGit.raw( [ 'cherry-pick', commitHash ] ); -} - -/** - * Replaces the local branch's content with the content from another branch. - * - * @param {string} gitWorkingDirectoryPath Local repository path. - * @param {string} sourceBranchName Branch Name - */ -async function replaceContentFromRemoteBranch( - gitWorkingDirectoryPath, - sourceBranchName -) { - const simpleGit = SimpleGit( gitWorkingDirectoryPath ); - await simpleGit.raw( [ 'rm', '-r', '.' ] ); - await simpleGit.raw( [ - 'checkout', - `origin/${ sourceBranchName }`, - '--', - '.', - ] ); -} - -module.exports = { - clone, - commit, - checkoutRemoteBranch, - createLocalBranch, - createLocalTag, - fetch, - pushBranchToOrigin, - pushTagsToOrigin, - discardLocalChanges, - resetLocalBranchAgainstOrigin, - getLastCommitHash, - cherrypickCommitIntoBranch, - replaceContentFromRemoteBranch, -}; diff --git a/bin/tsconfig.json b/bin/tsconfig.json index 86d2a07c742f8c..f3d576c178d0c0 100644 --- a/bin/tsconfig.json +++ b/bin/tsconfig.json @@ -25,7 +25,6 @@ "./plugin/lib/version.js", "./plugin/lib/logger.js", "./plugin/lib/utils.js", - "./plugin/lib/git.js", "./validate-package-lock.js" ] } diff --git a/docs/how-to-guides/javascript/README.md b/docs/how-to-guides/javascript/README.md index b31b0fa4d9989b..4b2d123c8ab136 100644 --- a/docs/how-to-guides/javascript/README.md +++ b/docs/how-to-guides/javascript/README.md @@ -1,6 +1,6 @@ -# Getting Started with JavaScript +# How to use JavaScript with the Block Editor -The purpose of this tutorial is to step through getting started with JavaScript and WordPress, specifically around the new block editor. The Block Editor Handbook contains information on the APIs available for working with this new setup. The goal of this tutorial is to get you comfortable on how to use the API reference and snippets of code found within. +The Block Editor Handbook contains information on the APIs available for working with this new setup. The goal of this tutorial is to get you comfortable using the API reference and snippets of code found within. ### What is JavaScript diff --git a/docs/manifest.json b/docs/manifest.json index 3db21b179331ef..8cc39af57531e4 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -234,7 +234,7 @@ "parent": "how-to-guides" }, { - "title": "Getting Started with JavaScript", + "title": "How to use JavaScript with the Block Editor", "slug": "javascript", "markdown_source": "../docs/how-to-guides/javascript/README.md", "parent": "how-to-guides" diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index a543e36babb11c..65e3cb1f65c8fc 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -50,8 +50,8 @@ Prompt visitors to take action with a button-style link. ([Source](https://githu - **Name:** core/button - **Category:** design -- **Supports:** align, anchor, color (background, gradients, text), spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~reusable~~ -- **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, text, textColor, title, url, width +- **Supports:** anchor, color (background, gradients, text), spacing (padding), typography (fontSize, lineHeight), ~~alignWide~~, ~~align~~, ~~reusable~~ +- **Attributes:** backgroundColor, gradient, linkTarget, placeholder, rel, text, textAlign, textColor, title, url, width ## Buttons @@ -329,7 +329,7 @@ Display a list of your most recent posts. ([Source](https://github.com/WordPress - **Name:** core/latest-posts - **Category:** widgets -- **Supports:** align, spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ +- **Supports:** align, color (background, gradients, link, text), spacing (margin, padding), typography (fontSize, lineHeight), ~~html~~ - **Attributes:** addLinkToFeaturedImage, categories, columns, displayAuthor, displayFeaturedImage, displayPostContent, displayPostContentRadio, displayPostDate, excerptLength, featuredImageAlign, featuredImageSizeHeight, featuredImageSizeSlug, featuredImageSizeWidth, order, orderBy, postLayout, postsToShow, selectedAuthor ## List diff --git a/lib/block-supports/typography.php b/lib/block-supports/typography.php index b1adaf1a679b81..01d223b84281eb 100644 --- a/lib/block-supports/typography.php +++ b/lib/block-supports/typography.php @@ -461,7 +461,6 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty $default_maximum_viewport_width = '1600px'; $default_minimum_viewport_width = '768px'; $default_minimum_font_size_factor = 0.75; - $default_maximum_font_size_factor = 1.5; $default_scale_factor = 1; $default_minimum_font_size_limit = '14px'; @@ -480,21 +479,11 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty // Font sizes. $preferred_size = gutenberg_get_typography_value_and_unit( $preset['size'] ); - // Protect against unsupported units. + // Protects against unsupported units. if ( empty( $preferred_size['unit'] ) ) { return $preset['size']; } - // If no fluid max font size is available, create one using max font size factor. - if ( ! $maximum_font_size_raw ) { - $maximum_font_size_raw = round( $preferred_size['value'] * $default_maximum_font_size_factor, 3 ) . $preferred_size['unit']; - } - - // If no fluid min font size is available, create one using min font size factor. - if ( ! $minimum_font_size_raw ) { - $minimum_font_size_raw = round( $preferred_size['value'] * $default_minimum_font_size_factor, 3 ) . $preferred_size['unit']; - } - // Parses the minimum font size limit, so we can perform checks using it. $minimum_font_size_limit = gutenberg_get_typography_value_and_unit( $default_minimum_font_size_limit, @@ -503,29 +492,35 @@ function gutenberg_get_typography_font_size_value( $preset, $should_use_fluid_ty ) ); - if ( ! empty( $minimum_font_size_limit ) ) { + // Don't enforce minimum font size if a font size has explicitly set a min and max value. + if ( ! empty( $minimum_font_size_limit ) && ( ! $minimum_font_size_raw && ! $maximum_font_size_raw ) ) { /* * If a minimum size was not passed to this function * and the user-defined font size is lower than $minimum_font_size_limit, - * then uses the user-defined font size as the minimum font-size. + * do not calculate a fluid value. */ - if ( ! isset( $fluid_font_size_settings['min'] ) && $preferred_size['value'] < $minimum_font_size_limit['value'] ) { - $minimum_font_size_raw = implode( '', $preferred_size ); + if ( $preferred_size['value'] <= $minimum_font_size_limit['value'] ) { + return $preset['size']; + } + } + + // If no fluid max font size is available use the incoming value. + if ( ! $maximum_font_size_raw ) { + $maximum_font_size_raw = $preferred_size['value'] . $preferred_size['unit']; + } + + /* + * If no minimumFontSize is provided, create one using + * the given font size multiplied by the min font size scale factor. + */ + if ( ! $minimum_font_size_raw ) { + $calculated_minimum_font_size = round( $preferred_size['value'] * $default_minimum_font_size_factor, 3 ); + + // Only use calculated min font size if it's > $minimum_font_size_limit value. + if ( ! empty( $minimum_font_size_limit ) && $calculated_minimum_font_size <= $minimum_font_size_limit['value'] ) { + $minimum_font_size_raw = $minimum_font_size_limit['value'] . $minimum_font_size_limit['unit']; } else { - $minimum_font_size_parsed = gutenberg_get_typography_value_and_unit( - $minimum_font_size_raw, - array( - 'coerce_to' => $preferred_size['unit'], - ) - ); - - /* - * If the passed or calculated minimum font size is lower than $minimum_font_size_limit - * use $minimum_font_size_limit instead. - */ - if ( ! empty( $minimum_font_size_parsed ) && $minimum_font_size_parsed['value'] < $minimum_font_size_limit['value'] ) { - $minimum_font_size_raw = implode( '', $minimum_font_size_limit ); - } + $minimum_font_size_raw = $calculated_minimum_font_size . $preferred_size['unit']; } } diff --git a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php index 45779d395e6291..07b83049cfeaad 100644 --- a/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php +++ b/lib/compat/wordpress-6.0/class-wp-theme-json-resolver-6-0.php @@ -146,80 +146,6 @@ public static function get_style_variations() { return $variations; } - /** - * Returns the custom post type that contains the user's origin config - * for the current theme or a void array if none are found. - * - * This can also create and return a new draft custom post type. - * - * @param WP_Theme $theme The theme object. If empty, it - * defaults to the current theme. - * @param bool $create_post Optional. Whether a new custom post - * type should be created if none are - * found. False by default. - * @param array $post_status_filter Filter Optional. custom post type by - * post status. ['publish'] by default, - * so it only fetches published posts. - * @return array Custom Post Type for the user's origin config. - */ - public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { - if ( ! $theme instanceof WP_Theme ) { - $theme = wp_get_theme(); - } - $user_cpt = array(); - $post_type_filter = 'wp_global_styles'; - $args = array( - 'numberposts' => 1, - 'orderby' => 'date', - 'order' => 'desc', - 'post_type' => $post_type_filter, - 'post_status' => $post_status_filter, - 'tax_query' => array( - array( - 'taxonomy' => 'wp_theme', - 'field' => 'name', - 'terms' => $theme->get_stylesheet(), - ), - ), - ); - - $cache_key = sprintf( 'wp_global_styles_%s', md5( serialize( $args ) ) ); - $post_id = wp_cache_get( $cache_key ); - - if ( (int) $post_id > 0 ) { - return get_post( $post_id, ARRAY_A ); - } - - // Special case: '-1' is a results not found. - if ( -1 === $post_id && ! $create_post ) { - return $user_cpt; - } - - $recent_posts = wp_get_recent_posts( $args ); - if ( is_array( $recent_posts ) && ( count( $recent_posts ) === 1 ) ) { - $user_cpt = $recent_posts[0]; - } elseif ( $create_post ) { - $cpt_post_id = wp_insert_post( - array( - 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', - 'post_status' => 'publish', - 'post_title' => __( 'Custom Styles', 'default' ), - 'post_type' => $post_type_filter, - 'post_name' => 'wp-global-styles-' . urlencode( wp_get_theme()->get_stylesheet() ), - 'tax_input' => array( - 'wp_theme' => array( wp_get_theme()->get_stylesheet() ), - ), - ), - true - ); - $user_cpt = get_post( $cpt_post_id, ARRAY_A ); - } - $cache_expiration = $user_cpt ? DAY_IN_SECONDS : HOUR_IN_SECONDS; - wp_cache_set( $cache_key, $user_cpt ? $user_cpt['ID'] : -1, '', $cache_expiration ); - - return $user_cpt; - } - /** * Returns the user's origin config. * diff --git a/lib/compat/wordpress-6.1/block-template-utils.php b/lib/compat/wordpress-6.1/block-template-utils.php index d1814b6f50e538..088333990a0d5f 100644 --- a/lib/compat/wordpress-6.1/block-template-utils.php +++ b/lib/compat/wordpress-6.1/block-template-utils.php @@ -264,51 +264,65 @@ function gutenberg_get_block_template( $id, $template_type = 'wp_template' ) { function _gutenberg_build_title_and_description_for_single_post_type_block_template( $post_type, $slug, WP_Block_Template $template ) { $post_type_object = get_post_type_object( $post_type ); - $posts = get_posts( - array( - 'name' => $slug, - 'post_type' => $post_type, - ) + $default_args = array( + 'post_type' => $post_type, + 'post_status' => 'publish', + 'posts_per_page' => 1, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, ); - if ( empty( $posts ) ) { + + $args = array( + 'name' => $slug, + ); + $args = wp_parse_args( $args, $default_args ); + + $posts_query = new WP_Query( $args ); + + if ( empty( $posts_query->posts ) ) { $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor referencing a post that was not found, where %1$s is the singular name of a post type and %2$s is the slug of the deleted post, e.g. "Not found: Page(hello)". - __( 'Not found: %1$s(%2$s)', 'gutenberg' ), + /* translators: Custom template title in the Site Editor referencing a post that was not found. 1: Post type singular name, 2: Post type slug. */ + __( 'Not found: %1$s (%2$s)', 'gutenberg' ), $post_type_object->labels->singular_name, $slug ); + return false; } - $post_title = $posts[0]->post_title; + $post_title = $posts_query->posts[0]->post_title; $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor, where %1$s is the singular name of a post type and %2$s is the name of the post, e.g. "Page: Hello". + /* translators: Custom template title in the Site Editor. 1: Post type singular name, 2: Post title. */ __( '%1$s: %2$s', 'gutenberg' ), $post_type_object->labels->singular_name, $post_title ); + $template->description = sprintf( - // translators: Represents the description of a user's custom template in the Site Editor, e.g. "Template for Page: Hello". + /* translators: Custom template description in the Site Editor. %s: Post title. */ __( 'Template for %s', 'gutenberg' ), $post_title ); - $posts_with_same_title = get_posts( - array( - 'title' => $post_title, - 'post_type' => $post_type, - 'post_status' => 'publish', - ) + $args = array( + 'title' => $post_title, ); - if ( count( $posts_with_same_title ) > 1 ) { + $args = wp_parse_args( $args, $default_args ); + + $posts_with_same_title_query = new WP_Query( $args ); + + if ( count( $posts_with_same_title_query->posts ) > 1 ) { $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor, where %1$s is the template title and %2$s is the slug of the post type, e.g. "Project: Hello (project_type)". + /* translators: Custom template title in the Site Editor. 1: Template title, 2: Post type slug. */ __( '%1$s (%2$s)', 'gutenberg' ), $template->title, $slug ); } + return true; } @@ -328,53 +342,66 @@ function _gutenberg_build_title_and_description_for_single_post_type_block_templ function _gutenberg_build_title_and_description_for_taxonomy_block_template( $taxonomy, $slug, WP_Block_Template $template ) { $taxonomy_object = get_taxonomy( $taxonomy ); - $terms = get_terms( - array( - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - 'slug' => $slug, - ) + $default_args = array( + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + 'update_term_meta_cache' => false, ); - if ( empty( $terms ) ) { + $term_query = new WP_Term_Query(); + + $args = array( + 'number' => 1, + 'slug' => $slug, + ); + $args = wp_parse_args( $args, $default_args ); + + $terms_query = $term_query->query( $args ); + + if ( empty( $terms_query ) ) { $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor referencing a taxonomy term that was not found, where %1$s is the singular name of a taxonomy and %2$s is the slug of the deleted term, e.g. "Not found: Category(shoes)". - __( 'Not found: %1$s(%2$s)', 'gutenberg' ), + /* translators: Custom template title in the Site Editor, referencing a taxonomy term that was not found. 1: Taxonomy singular name, 2: Term slug. */ + __( 'Not found: %1$s (%2$s)', 'gutenberg' ), $taxonomy_object->labels->singular_name, $slug ); return false; } - $term_title = $terms[0]->name; + $term_title = $terms_query[0]->name; $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor, where %1$s is the singular name of a taxonomy and %2$s is the name of the term, e.g. "Category: shoes". + /* translators: Custom template title in the Site Editor. 1: Taxonomy singular name, 2: Term title. */ __( '%1$s: %2$s', 'gutenberg' ), $taxonomy_object->labels->singular_name, $term_title ); + $template->description = sprintf( - // translators: Represents the description of a user's custom template in the Site Editor, e.g. "Template for Category: shoes". - __( 'Template for %1$s', 'gutenberg' ), + /* translators: Custom template description in the Site Editor. %s: Term title. */ + __( 'Template for %s', 'gutenberg' ), $term_title ); - $terms_with_same_title = get_terms( - array( - 'taxonomy' => $taxonomy, - 'hide_empty' => false, - 'name' => $term_title, - ) + $term_query = new WP_Term_Query(); + + $args = array( + 'number' => 2, + 'name' => $term_title, ); - if ( count( $terms_with_same_title ) > 1 ) { + $args = wp_parse_args( $args, $default_args ); + + $terms_with_same_title_query = $term_query->query( $args ); + + if ( count( $terms_with_same_title_query ) > 1 ) { $template->title = sprintf( - // translators: Represents the title of a user's custom template in the Site Editor, where %1$s is the template title and %2$s is the slug of the taxonomy, e.g. "Category: shoes (product_tag)". + /* translators: Custom template title in the Site Editor. 1: Template title, 2: Term slug. */ __( '%1$s (%2$s)', 'gutenberg' ), $template->title, $slug ); } + return true; } diff --git a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php index d006f25382bbdc..73e012f33d1c7e 100644 --- a/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php +++ b/lib/compat/wordpress-6.1/class-wp-theme-json-resolver-6-1.php @@ -120,4 +120,72 @@ public static function get_user_data() { return static::$user; } + + /** + * Returns the custom post type that contains the user's origin config + * for the active theme or a void array if none are found. + * + * This can also create and return a new draft custom post type. + * + * @since 5.9.0 + * + * @param WP_Theme $theme The theme object. If empty, it + * defaults to the active theme. + * @param bool $create_post Optional. Whether a new custom post + * type should be created if none are + * found. Default false. + * @param array $post_status_filter Optional. Filter custom post type by + * post status. Default `array( 'publish' )`, + * so it only fetches published posts. + * @return array Custom Post Type for the user's origin config. + */ + public static function get_user_data_from_wp_global_styles( $theme, $create_post = false, $post_status_filter = array( 'publish' ) ) { + if ( ! $theme instanceof WP_Theme ) { + $theme = wp_get_theme(); + } + $user_cpt = array(); + $post_type_filter = 'wp_global_styles'; + $stylesheet = $theme->get_stylesheet(); + $args = array( + 'posts_per_page' => 1, + 'orderby' => 'date', + 'order' => 'desc', + 'post_type' => $post_type_filter, + 'post_status' => $post_status_filter, + 'ignore_sticky_posts' => true, + 'no_found_rows' => true, + 'tax_query' => array( + array( + 'taxonomy' => 'wp_theme', + 'field' => 'name', + 'terms' => $stylesheet, + ), + ), + ); + + $global_style_query = new WP_Query(); + $recent_posts = $global_style_query->query( $args ); + if ( count( $recent_posts ) === 1 ) { + $user_cpt = get_post( $recent_posts[0], ARRAY_A ); + } elseif ( $create_post ) { + $cpt_post_id = wp_insert_post( + array( + 'post_content' => '{"version": ' . WP_Theme_JSON_Gutenberg::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', // Do not make string translatable, see https://core.trac.wordpress.org/ticket/54518. + 'post_type' => $post_type_filter, + 'post_name' => sprintf( 'wp-global-styles-%s', urlencode( $stylesheet ) ), + 'tax_input' => array( + 'wp_theme' => array( $stylesheet ), + ), + ), + true + ); + if ( ! is_wp_error( $cpt_post_id ) ) { + $user_cpt = get_post( $cpt_post_id, ARRAY_A ); + } + } + + return $user_cpt; + } } diff --git a/lib/experimental/class-wp-rest-block-editor-settings-controller.php b/lib/experimental/class-wp-rest-block-editor-settings-controller.php index c0850a8a29f4cf..4a258a70102bb5 100644 --- a/lib/experimental/class-wp-rest-block-editor-settings-controller.php +++ b/lib/experimental/class-wp-rest-block-editor-settings-controller.php @@ -115,7 +115,7 @@ public function get_item_schema() { 'type' => 'object', 'properties' => array( '__unstableEnableFullSiteEditingBlocks' => array( - 'description' => __( 'Enables experimental Full Site Editing blocks', 'gutenberg' ), + 'description' => __( 'Enables experimental Site Editor blocks', 'gutenberg' ), 'type' => 'boolean', 'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ), ), @@ -127,7 +127,7 @@ public function get_item_schema() { ), 'supportsTemplateMode' => array( - 'description' => __( 'Returns if the current theme is full site editing-enabled or not.', 'gutenberg' ), + 'description' => __( 'Indicates whether the current theme supports block-based templates.', 'gutenberg' ), 'type' => 'boolean', 'context' => array( 'post-editor', 'site-editor', 'widgets-editor' ), ), diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 408c5f7aecfa30..b0c05544d6b199 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -73,25 +73,19 @@ function gutenberg_initialize_editor( $editor_name, $editor_script_handle, $sett } /** - * Sets a global JS variable used to trigger the availability of zoomed out view. + * Sets a global JS variable used to trigger the availability of each Gutenberg Experiment. */ -function gutenberg_enable_zoomed_out_view() { +function gutenberg_enable_experiments() { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); if ( $gutenberg_experiments && array_key_exists( 'gutenberg-zoomed-out-view', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableZoomedOutView = true', 'before' ); } -} - -add_action( 'admin_init', 'gutenberg_enable_zoomed_out_view' ); - -/** - * Sets a global JS variable used to trigger the availability of the Navigation List View experiment. - */ -function gutenberg_enable_off_canvas_navigation_editor() { - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-color-randomizer', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableColorRandomizer = true', 'before' ); + } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-off-canvas-navigation-editor', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableOffCanvasNavigationEditor = true', 'before' ); } } -add_action( 'admin_init', 'gutenberg_enable_off_canvas_navigation_editor' ); +add_action( 'admin_init', 'gutenberg_enable_experiments' ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index aa5103f3bb7a11..309612664cb9c9 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -63,6 +63,18 @@ function gutenberg_initialize_experiments_settings() { 'id' => 'gutenberg-off-canvas-navigation-editor', ) ); + add_settings_field( + 'gutenberg-color-randomizer', + __( 'Color randomizer ', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Test the Global Styles color randomizer; a utility that lets you mix the current color palette pseudo-randomly.', 'gutenberg' ), + 'id' => 'gutenberg-color-randomizer', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/package-lock.json b/package-lock.json index f51b791101fc10..ad772d88dd1256 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18057,11 +18057,19 @@ "@wordpress/url": "file:packages/url", "@wordpress/viewport": "file:packages/viewport", "classnames": "^2.3.1", + "colord": "^2.9.2", "downloadjs": "^1.4.7", "history": "^5.1.0", "lodash": "^4.17.21", "react-autosize-textarea": "^7.1.0", "rememo": "^4.0.0" + }, + "dependencies": { + "colord": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz", + "integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ==" + } } }, "@wordpress/edit-widgets": { @@ -18110,6 +18118,7 @@ "@wordpress/data": "file:packages/data", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", + "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/hooks": "file:packages/hooks", "@wordpress/html-entities": "file:packages/html-entities", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index e3d5cab8586f85..54af4cdb6e603d 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Enhancement + +- `BlockLockModal`: Move Icon component out of CheckboxControl label ([#45535](https://github.com/WordPress/gutenberg/pull/45535)) +- Fluid typography: adjust font size min and max rules ([#45536](https://github.com/WordPress/gutenberg/pull/45536)). + ## 10.4.0 (2022-11-02) ### Bug Fix diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 03155e0f4ddd89..5760f1584c5db8 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -428,7 +428,6 @@ _Parameters_ - _args.minimumFontSize_ `?string`: Minimum font size for any clamp() calculation. Optional. - _args.scaleFactor_ `?number`: A scale factor to determine how fast a font scales within boundaries. Optional. - _args.minimumFontSizeFactor_ `?number`: How much to scale defaultFontSize by to derive minimumFontSize. Optional. -- _args.maximumFontSizeFactor_ `?number`: How much to scale defaultFontSize by to derive maximumFontSize. Optional. _Returns_ diff --git a/packages/block-editor/src/components/block-lock/menu-item.js b/packages/block-editor/src/components/block-lock/menu-item.js index 665d9bfbc91a40..65607c646e9684 100644 --- a/packages/block-editor/src/components/block-lock/menu-item.js +++ b/packages/block-editor/src/components/block-lock/menu-item.js @@ -4,7 +4,7 @@ import { __ } from '@wordpress/i18n'; import { useReducer } from '@wordpress/element'; import { MenuItem } from '@wordpress/components'; -import { lock, unlock } from '@wordpress/icons'; +import { lockOutline, unlock } from '@wordpress/icons'; /** * Internal dependencies @@ -28,7 +28,10 @@ export default function BlockLockMenuItem( { clientId } ) { return ( <> - + { label } { isModalOpen && ( diff --git a/packages/block-editor/src/components/block-lock/modal.js b/packages/block-editor/src/components/block-lock/modal.js index fec05139360810..00429c3a9b6992 100644 --- a/packages/block-editor/src/components/block-lock/modal.js +++ b/packages/block-editor/src/components/block-lock/modal.js @@ -136,18 +136,7 @@ export default function BlockLockModal( { clientId, onClose } ) {
  • - { __( 'Restrict editing' ) } - - - } + label={ __( 'Restrict editing' ) } checked={ !! lock.edit } onChange={ ( edit ) => setLock( ( prevLock ) => ( { @@ -156,23 +145,16 @@ export default function BlockLockModal( { clientId, onClose } ) { } ) ) } /> +
  • ) }
  • - { __( 'Disable movement' ) } - - - } + label={ __( 'Disable movement' ) } checked={ lock.move } onChange={ ( move ) => setLock( ( prevLock ) => ( { @@ -181,22 +163,15 @@ export default function BlockLockModal( { clientId, onClose } ) { } ) ) } /> +
  • - { __( 'Prevent removal' ) } - - - } + label={ __( 'Prevent removal' ) } checked={ lock.remove } onChange={ ( remove ) => setLock( ( prevLock ) => ( { @@ -205,6 +180,10 @@ export default function BlockLockModal( { clientId, onClose } ) { } ) ) } /> +
  • { hasTemplateLock && ( diff --git a/packages/block-editor/src/components/block-lock/style.scss b/packages/block-editor/src/components/block-lock/style.scss index 4152168e3e8154..b833ec72d95a59 100644 --- a/packages/block-editor/src/components/block-lock/style.scss +++ b/packages/block-editor/src/components/block-lock/style.scss @@ -25,24 +25,17 @@ } } .block-editor-block-lock-modal__checklist-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: $grid-unit-15; margin-bottom: 0; padding: $grid-unit-15 0 $grid-unit-15 $grid-unit-40; - .components-base-control__field { - align-items: center; - display: flex; - } - - .components-checkbox-control__label { - display: flex; - align-items: center; - justify-content: space-between; - flex-grow: 1; - - svg { - margin-right: $grid-unit-15; - fill: $gray-900; - } + .block-editor-block-lock-modal__lock-icon { + flex-shrink: 0; + margin-right: $grid-unit-15; + fill: $gray-900; } &:hover { diff --git a/packages/block-editor/src/components/block-mover/style.scss b/packages/block-editor/src/components/block-mover/style.scss index eba7bbed683045..cf4061b3789dfb 100644 --- a/packages/block-editor/src/components/block-mover/style.scss +++ b/packages/block-editor/src/components/block-mover/style.scss @@ -15,7 +15,6 @@ // Focus style. &::before { height: calc(100% - 4px); - width: calc(100% - 4px); } } diff --git a/packages/block-editor/src/components/block-styles/utils.js b/packages/block-editor/src/components/block-styles/utils.js index 7f738ab0fa923c..f60a23a4287b21 100644 --- a/packages/block-editor/src/components/block-styles/utils.js +++ b/packages/block-editor/src/components/block-styles/utils.js @@ -11,7 +11,7 @@ import { _x } from '@wordpress/i18n'; /** * Returns the active style from the given className. * - * @param {Array} styles Block style variations. + * @param {Array} styles Block styles. * @param {string} className Class name * * @return {Object?} The active style. @@ -59,7 +59,7 @@ export function replaceActiveStyle( className, activeStyle, newStyle ) { * act as a fallback for when there is no active style applied to a block. The default item also serves * as a switch on the frontend to deactivate non-default styles. * - * @param {Array} styles Block style variations. + * @param {Array} styles Block styles. * * @return {Array} The style collection. */ @@ -83,7 +83,7 @@ export function getRenderedStyles( styles ) { /** * Returns a style object from a collection of styles where that style object is the default block style. * - * @param {Array} styles Block style variations. + * @param {Array} styles Block styles. * * @return {Object?} The default style object, if found. */ diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index ea6cba19639cdb..e301859c165491 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -175,6 +175,12 @@ &:active { color: $white; } + + // Make sure the button has no hover style when it's disabled. + &[aria-disabled="true"]:hover { + color: $white; + } + display: flex; } .block-selection-button_select-button.components-button { diff --git a/packages/block-editor/src/components/font-sizes/fluid-utils.js b/packages/block-editor/src/components/font-sizes/fluid-utils.js index 2c6540f89d0494..de8a27e3014e88 100644 --- a/packages/block-editor/src/components/font-sizes/fluid-utils.js +++ b/packages/block-editor/src/components/font-sizes/fluid-utils.js @@ -9,7 +9,6 @@ const DEFAULT_MAXIMUM_VIEWPORT_WIDTH = '1600px'; const DEFAULT_MINIMUM_VIEWPORT_WIDTH = '768px'; const DEFAULT_SCALE_FACTOR = 1; const DEFAULT_MINIMUM_FONT_SIZE_FACTOR = 0.75; -const DEFAULT_MAXIMUM_FONT_SIZE_FACTOR = 1.5; const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px'; /** @@ -41,7 +40,6 @@ const DEFAULT_MINIMUM_FONT_SIZE_LIMIT = '14px'; * @param {?string} args.minimumFontSize Minimum font size for any clamp() calculation. Optional. * @param {?number} args.scaleFactor A scale factor to determine how fast a font scales within boundaries. Optional. * @param {?number} args.minimumFontSizeFactor How much to scale defaultFontSize by to derive minimumFontSize. Optional. - * @param {?number} args.maximumFontSizeFactor How much to scale defaultFontSize by to derive maximumFontSize. Optional. * * @return {string|null} A font-size value using clamp(). */ @@ -53,15 +51,8 @@ export function getComputedFluidTypographyValue( { maximumViewPortWidth = DEFAULT_MAXIMUM_VIEWPORT_WIDTH, scaleFactor = DEFAULT_SCALE_FACTOR, minimumFontSizeFactor = DEFAULT_MINIMUM_FONT_SIZE_FACTOR, - maximumFontSizeFactor = DEFAULT_MAXIMUM_FONT_SIZE_FACTOR, minimumFontSizeLimit = DEFAULT_MINIMUM_FONT_SIZE_LIMIT, } ) { - /* - * Caches minimumFontSize in minimumFontSizeValue - * so we can check if minimumFontSize exists later. - */ - let minimumFontSizeValue = minimumFontSize; - /* * Calculates missing minimumFontSize and maximumFontSize from * defaultFontSize if provided. @@ -75,15 +66,6 @@ export function getComputedFluidTypographyValue( { return null; } - // If no minimumFontSize is provided, derive using min scale factor. - if ( ! minimumFontSizeValue ) { - minimumFontSizeValue = - roundToPrecision( - fontSizeParsed.value * minimumFontSizeFactor, - 3 - ) + fontSizeParsed.unit; - } - // Parses the minimum font size limit, so we can perform checks using it. const minimumFontSizeLimitParsed = getTypographyValueAndUnit( minimumFontSizeLimit, @@ -92,57 +74,51 @@ export function getComputedFluidTypographyValue( { } ); - if ( !! minimumFontSizeLimitParsed?.value ) { + // Don't enforce minimum font size if a font size has explicitly set a min and max value. + if ( + !! minimumFontSizeLimitParsed?.value && + ! minimumFontSize && + ! maximumFontSize + ) { /* * If a minimum size was not passed to this function - * and the user-defined font size is lower than `minimumFontSizeLimit`, - * then uses the user-defined font size as the minimum font-size. + * and the user-defined font size is lower than $minimum_font_size_limit, + * do not calculate a fluid value. */ - if ( - ! minimumFontSize && - fontSizeParsed?.value < minimumFontSizeLimitParsed?.value - ) { - minimumFontSizeValue = `${ fontSizeParsed.value }${ fontSizeParsed.unit }`; - } else { - const minimumFontSizeParsed = getTypographyValueAndUnit( - minimumFontSizeValue, - { - coerceTo: fontSizeParsed.unit, - } - ); - - /* - * Otherwise, if the passed or calculated minimum font size is lower than `minimumFontSizeLimit` - * use `minimumFontSizeLimit` instead. - */ - if ( - !! minimumFontSizeParsed?.value && - minimumFontSizeParsed.value < - minimumFontSizeLimitParsed.value - ) { - minimumFontSizeValue = `${ minimumFontSizeLimitParsed.value }${ minimumFontSizeLimitParsed.unit }`; - } + if ( fontSizeParsed?.value <= minimumFontSizeLimitParsed?.value ) { + return null; } } - // If no maximumFontSize is provided, derive using max scale factor. + // If no fluid max font size is available use the incoming value. if ( ! maximumFontSize ) { - maximumFontSize = - roundToPrecision( - fontSizeParsed.value * maximumFontSizeFactor, - 3 - ) + fontSizeParsed.unit; + maximumFontSize = `${ fontSizeParsed.value }${ fontSizeParsed.unit }`; } - } - // Return early if one of the provided inputs is not provided. - if ( ! minimumFontSizeValue || ! maximumFontSize ) { - return null; + /* + * If no minimumFontSize is provided, create one using + * the given font size multiplied by the min font size scale factor. + */ + if ( ! minimumFontSize ) { + const calculatedMinimumFontSize = roundToPrecision( + fontSizeParsed.value * minimumFontSizeFactor, + 3 + ); + + // Only use calculated min font size if it's > $minimum_font_size_limit value. + if ( + !! minimumFontSizeLimitParsed?.value && + calculatedMinimumFontSize < minimumFontSizeLimitParsed?.value + ) { + minimumFontSize = `${ minimumFontSizeLimitParsed.value }${ minimumFontSizeLimitParsed.unit }`; + } else { + minimumFontSize = `${ calculatedMinimumFontSize }${ fontSizeParsed.unit }`; + } + } } // Grab the minimum font size and normalize it in order to use the value for calculations. - const minimumFontSizeParsed = - getTypographyValueAndUnit( minimumFontSizeValue ); + const minimumFontSizeParsed = getTypographyValueAndUnit( minimumFontSize ); // We get a 'preferred' unit to keep units consistent when calculating, // otherwise the result will not be accurate. @@ -159,12 +135,9 @@ export function getComputedFluidTypographyValue( { } // Uses rem for accessible fluid target font scaling. - const minimumFontSizeRem = getTypographyValueAndUnit( - minimumFontSizeValue, - { - coerceTo: 'rem', - } - ); + const minimumFontSizeRem = getTypographyValueAndUnit( minimumFontSize, { + coerceTo: 'rem', + } ); // Viewport widths defined for fluid typography. Normalize units const maximumViewPortWidthParsed = getTypographyValueAndUnit( @@ -205,7 +178,7 @@ export function getComputedFluidTypographyValue( { ); const fluidTargetFontSize = `${ minimumFontSizeRem.value }${ minimumFontSizeRem.unit } + ((1vw - ${ viewPortWidthOffset }) * ${ linearFactorScaled })`; - return `clamp(${ minimumFontSizeValue }, ${ fluidTargetFontSize }, ${ maximumFontSize })`; + return `clamp(${ minimumFontSize }, ${ fluidTargetFontSize }, ${ maximumFontSize })`; } /** diff --git a/packages/block-editor/src/components/font-sizes/test/fluid-utils.js b/packages/block-editor/src/components/font-sizes/test/fluid-utils.js index aa268d04d7f1f2..15805431a959fb 100644 --- a/packages/block-editor/src/components/font-sizes/test/fluid-utils.js +++ b/packages/block-editor/src/components/font-sizes/test/fluid-utils.js @@ -33,7 +33,7 @@ describe( 'getComputedFluidTypographyValue()', () => { fontSize: '30px', } ); expect( fluidTypographyValues ).toBe( - 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 2.704), 45px)' + 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 0.901), 30px)' ); } ); @@ -42,7 +42,7 @@ describe( 'getComputedFluidTypographyValue()', () => { fontSize: '30px', } ); expect( fluidTypographyValues ).toBe( - 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 2.704), 45px)' + 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 0.901), 30px)' ); } ); @@ -53,7 +53,7 @@ describe( 'getComputedFluidTypographyValue()', () => { maximumViewPortWidth: '1000px', } ); expect( fluidTypographyValues ).toBe( - 'clamp(22.5px, 1.406rem + ((1vw - 5px) * 4.5), 45px)' + 'clamp(22.5px, 1.406rem + ((1vw - 5px) * 1.5), 30px)' ); } ); @@ -63,7 +63,7 @@ describe( 'getComputedFluidTypographyValue()', () => { scaleFactor: '2', } ); expect( fluidTypographyValues ).toBe( - 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 5.409), 45px)' + 'clamp(22.5px, 1.406rem + ((1vw - 7.68px) * 1.803), 30px)' ); } ); @@ -74,7 +74,7 @@ describe( 'getComputedFluidTypographyValue()', () => { maximumFontSizeFactor: '2', } ); expect( fluidTypographyValues ).toBe( - 'clamp(15px, 0.938rem + ((1vw - 7.68px) * 5.409), 60px)' + 'clamp(15px, 0.938rem + ((1vw - 7.68px) * 1.803), 30px)' ); } ); diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 8afc6482eb33fa..c03a8616fed657 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -150,7 +150,8 @@ const ForwardedInnerBlocks = forwardRef( ( props, ref ) => { * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inner-blocks/README.md */ export function useInnerBlocksProps( props = {}, options = {} ) { - const { __unstableDisableLayoutClassNames } = options; + const { __unstableDisableLayoutClassNames, __unstableDisableDropZone } = + options; const { clientId, __unstableLayoutClassNames: layoutClassNames = '' } = useBlockEditContext(); const isSmallScreen = useViewportMatch( 'medium', '<' ); @@ -187,11 +188,13 @@ export function useInnerBlocksProps( props = {}, options = {} ) { [ clientId, isSmallScreen ] ); + const blockDropZoneRef = useBlockDropZone( { + rootClientId: clientId, + } ); + const ref = useMergeRefs( [ props.ref, - useBlockDropZone( { - rootClientId: clientId, - } ), + __unstableDisableDropZone ? null : blockDropZoneRef, ] ); const innerBlocksProps = { diff --git a/packages/block-editor/src/components/inserter/reusable-blocks-tab.js b/packages/block-editor/src/components/inserter/reusable-blocks-tab.js index 2fbd5e49ae8c06..9505dd77f3b94d 100644 --- a/packages/block-editor/src/components/inserter/reusable-blocks-tab.js +++ b/packages/block-editor/src/components/inserter/reusable-blocks-tab.js @@ -4,6 +4,7 @@ import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { addQueryArgs } from '@wordpress/url'; +import { Button } from '@wordpress/components'; /** * Internal dependencies @@ -59,14 +60,15 @@ export function ReusableBlocksTab( { rootClientId, onInsert, onHover } ) { rootClientId={ rootClientId } /> ); diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 210f2db211c00a..785b9a414a5d79 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -174,9 +174,13 @@ $block-inserter-tabs-height: 44px; text-align: right; } +.block-editor-inserter__manage-reusable-blocks-container { + margin: auto $grid-unit-20 $grid-unit-20; +} + .block-editor-inserter__manage-reusable-blocks { - display: inline-block; - margin: $grid-unit-20; + justify-content: center; + width: 100%; } .block-editor-inserter__no-results { @@ -341,10 +345,6 @@ $block-inserter-tabs-height: 44px; position: relative; // prevents overscroll when block library is open } -.block-editor-inserter__manage-reusable-blocks-container { - padding: $grid-unit-20; -} - .block-editor-inserter__quick-inserter { width: 100%; @@ -442,7 +442,8 @@ $block-inserter-tabs-height: 44px; } &__list { - margin-left: $sidebar-width - $grid-unit-40; + margin-left: $sidebar-width; + padding: $grid-unit-30 0 $grid-unit-40; } .block-editor-block-patterns-list { diff --git a/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.js b/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.js index 90042d1df5d410..ce47db3d7f4079 100644 --- a/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.js +++ b/packages/block-editor/src/components/inserter/test/reusable-blocks-tab.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { render, fireEvent } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * WordPress dependencies */ import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -22,35 +21,6 @@ jest.mock( '../hooks/use-block-types-state', () => { return mock; } ); -jest.mock( '@wordpress/data/src/components/use-select', () => { - // This allows us to tweak the returned value on each test. - const mock = jest.fn(); - return mock; -} ); - -jest.mock( '@wordpress/data/src/components/use-dispatch', () => { - return { - useDispatch: () => ( {} ), - }; -} ); - -const debouncedSpeak = jest.fn(); - -function InserterBlockList( props ) { - return ; -} - -const initializeAllClosedMenuState = ( propOverrides ) => { - const { container } = render( ); - const activeTabs = container.querySelectorAll( - '.components-panel__body.is-opened button.components-panel__body-toggle' - ); - activeTabs.forEach( ( tab ) => { - fireEvent.click( tab ); - } ); - return container; -}; - describe( 'InserterMenu', () => { beforeAll( () => { registerBlockType( 'core/block', { @@ -59,19 +29,17 @@ describe( 'InserterMenu', () => { edit: () => {}, } ); } ); + afterAll( () => { unregisterBlockType( 'core/block' ); } ); - beforeEach( () => { - debouncedSpeak.mockClear(); + beforeEach( () => { useBlockTypesState.mockImplementation( () => [ items, categories, collections, ] ); - - useSelect.mockImplementation( () => false ); } ); it( 'should show nothing if there are no items', () => { @@ -81,36 +49,25 @@ describe( 'InserterMenu', () => { categories, collections, ] ); - const { container } = render( - - ); - const visibleBlocks = container.querySelector( - '.block-editor-block-types-list__item' - ); - expect( visibleBlocks ).toBe( null ); + render( ); + + expect( screen.queryByRole( 'option' ) ).not.toBeInTheDocument(); } ); it( 'should list reusable blocks', () => { - const container = initializeAllClosedMenuState(); - const blocks = container.querySelectorAll( - '.block-editor-block-types-list__item-title' - ); + render( ); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ] ).toHaveTextContent( 'My reusable block' ); + expect( + screen.getByRole( 'option', { name: 'My reusable block' } ) + ).toBeVisible(); } ); it( 'should trim whitespace of search terms', () => { - const { container } = render( - - ); - - const blocks = container.querySelectorAll( - '.block-editor-block-types-list__item-title' - ); + render( ); - expect( blocks ).toHaveLength( 1 ); - expect( blocks[ 0 ] ).toHaveTextContent( 'My reusable block' ); + expect( + screen.getByRole( 'option', { name: 'My reusable block' } ) + ).toBeVisible(); } ); } ); diff --git a/packages/block-editor/src/components/link-control/index.js b/packages/block-editor/src/components/link-control/index.js index ee8b8db855acd6..52048cdc25b056 100644 --- a/packages/block-editor/src/components/link-control/index.js +++ b/packages/block-editor/src/components/link-control/index.js @@ -20,6 +20,7 @@ import LinkControlSettingsDrawer from './settings-drawer'; import LinkControlSearchInput from './search-input'; import LinkPreview from './link-preview'; import useCreatePage from './use-create-page'; +import useInternalInputValue from './use-internal-input-value'; import { ViewerFill } from './viewer-slot'; import { DEFAULT_LINK_SETTINGS } from './constants'; @@ -132,22 +133,19 @@ function LinkControl( { const isMounting = useRef( true ); const wrapperNode = useRef(); const textInputRef = useRef(); + const isEndingEditWithFocus = useRef( false ); + + const [ internalUrlInputValue, setInternalUrlInputValue ] = + useInternalInputValue( value?.url || '' ); + + const [ internalTextInputValue, setInternalTextInputValue ] = + useInternalInputValue( value?.title || '' ); - const [ internalInputValue, setInternalInputValue ] = useState( - value?.url || '' - ); - const [ internalTextValue, setInternalTextValue ] = useState( - value?.title || '' - ); - const currentInputValue = propInputValue || internalInputValue; const [ isEditingLink, setIsEditingLink ] = useState( forceIsEditingLink !== undefined ? forceIsEditingLink : ! value || ! value.url ); - const isEndingEditWithFocus = useRef( false ); - - const currentInputIsEmpty = ! currentInputValue?.trim()?.length; const { createPage, isCreatingPage, errorMessage } = useCreatePage( createSuggestion ); @@ -191,53 +189,35 @@ function LinkControl( { isEndingEditWithFocus.current = false; }, [ isEditingLink, isCreatingPage ] ); - useEffect( () => { - /** - * If the value's `text` property changes then sync this - * back up with state. - */ - if ( value?.title && value.title !== internalTextValue ) { - setInternalTextValue( value.title ); - } - - /** - * Update the state value internalInputValue if the url value changes - * for example when clicking on another anchor - */ - if ( value?.url ) { - setInternalInputValue( value.url ); - } - }, [ value ] ); - /** * Cancels editing state and marks that focus may need to be restored after * the next render, if focus was within the wrapper when editing finished. */ - function stopEditing() { + const stopEditing = () => { isEndingEditWithFocus.current = !! wrapperNode.current?.contains( wrapperNode.current.ownerDocument.activeElement ); setIsEditingLink( false ); - } + }; const handleSelectSuggestion = ( updatedValue ) => { onChange( { ...updatedValue, - title: internalTextValue || updatedValue?.title, + title: internalTextInputValue || updatedValue?.title, } ); stopEditing(); }; const handleSubmit = () => { if ( - currentInputValue !== value?.url || - internalTextValue !== value?.title + currentUrlInputValue !== value?.url || + internalTextInputValue !== value?.title ) { onChange( { ...value, - url: currentInputValue, - title: internalTextValue, + url: currentUrlInputValue, + title: internalTextInputValue, } ); } stopEditing(); @@ -254,6 +234,10 @@ function LinkControl( { } }; + const currentUrlInputValue = propInputValue || internalUrlInputValue; + + const currentInputIsEmpty = ! currentUrlInputValue?.trim()?.length; + const shownUnlinkControl = onRemove && value && ! isEditingLink && ! isCreatingPage; @@ -289,8 +273,8 @@ function LinkControl( { ref={ textInputRef } className="block-editor-link-control__field block-editor-link-control__text-content" label="Text" - value={ internalTextValue } - onChange={ setInternalTextValue } + value={ internalTextInputValue } + onChange={ setInternalTextInputValue } onKeyDown={ handleSubmitWithEnter } /> ) } @@ -299,10 +283,10 @@ function LinkControl( { currentLink={ value } className="block-editor-link-control__field block-editor-link-control__search-input" placeholder={ searchInputPlaceholder } - value={ currentInputValue } + value={ currentUrlInputValue } withCreateSuggestion={ withCreateSuggestion } onCreateSuggestion={ createPage } - onChange={ setInternalInputValue } + onChange={ setInternalUrlInputValue } onSelect={ handleSelectSuggestion } showInitialSuggestions={ showInitialSuggestions } allowDirectEntry={ ! noDirectEntry } diff --git a/packages/block-editor/src/components/link-control/search-input.js b/packages/block-editor/src/components/link-control/search-input.js index 5d4e9328f283e6..cabe52f907469e 100644 --- a/packages/block-editor/src/components/link-control/search-input.js +++ b/packages/block-editor/src/components/link-control/search-input.js @@ -111,7 +111,7 @@ const LinkControlSearchInput = forwardRef( allowDirectEntry || ( suggestion && Object.keys( suggestion ).length >= 1 ) ) { - const { id, url, ...restLinkProps } = currentLink; + const { id, url, ...restLinkProps } = currentLink ?? {}; onSelect( // Some direct entries don't have types or IDs, and we still need to clear the previous ones. { ...restLinkProps, ...suggestion }, diff --git a/packages/block-editor/src/components/link-control/use-internal-input-value.js b/packages/block-editor/src/components/link-control/use-internal-input-value.js new file mode 100644 index 00000000000000..c69c8d06bcb2f5 --- /dev/null +++ b/packages/block-editor/src/components/link-control/use-internal-input-value.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { useState, useEffect } from '@wordpress/element'; + +export default function useInternalInputValue( value ) { + const [ internalInputValue, setInternalInputValue ] = useState( + value || '' + ); + + useEffect( () => { + /** + * If the value's `text` property changes then sync this + * back up with state. + */ + if ( value?.title && value.title !== internalInputValue ) { + setInternalInputValue( value.title ); + } + }, [ value ] ); + + return [ internalInputValue, setInternalInputValue ]; +} diff --git a/packages/block-editor/src/hooks/color-panel.js b/packages/block-editor/src/hooks/color-panel.js index b1bf5b8206a79b..d8b804c300eb04 100644 --- a/packages/block-editor/src/hooks/color-panel.js +++ b/packages/block-editor/src/hooks/color-panel.js @@ -29,7 +29,19 @@ export default function ColorPanel( { const definedColors = settings.filter( ( setting ) => setting?.colorValue ); useEffect( () => { - if ( ! enableContrastChecking || ! definedColors.length ) { + if ( ! enableContrastChecking ) { + return; + } + if ( ! definedColors.length ) { + if ( detectedBackgroundColor ) { + setDetectedBackgroundColor(); + } + if ( detectedColor ) { + setDetectedColor(); + } + if ( detectedLinkColor ) { + setDetectedColor(); + } return; } diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index ad6a14028d3f28..593eea881e3107 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -459,6 +459,8 @@ export function ColorEdit( props ) { Platform.OS === 'web' && ! gradient && ! style?.color?.gradient && + hasBackgroundColor && + ( hasLinkColor || hasTextColor ) && // Contrast checking is enabled by default. // Deactivating it requires `enableContrastChecker` to have // an explicit value of `false`. diff --git a/packages/block-editor/src/hooks/test/use-typography-props.js b/packages/block-editor/src/hooks/test/use-typography-props.js index 52f7bf97328ee3..00557881467ca8 100644 --- a/packages/block-editor/src/hooks/test/use-typography-props.js +++ b/packages/block-editor/src/hooks/test/use-typography-props.js @@ -42,7 +42,7 @@ describe( 'getTypographyClassesAndStyles', () => { style: { letterSpacing: '22px', fontSize: - 'clamp(1.5rem, 1.5rem + ((1vw - 0.48rem) * 2.885), 3rem)', + 'clamp(1.5rem, 1.5rem + ((1vw - 0.48rem) * 0.962), 2rem)', textTransform: 'uppercase', }, } ); diff --git a/packages/block-editor/src/layouts/flex.js b/packages/block-editor/src/layouts/flex.js index 57dbf1005d5283..948eceb6624d60 100644 --- a/packages/block-editor/src/layouts/flex.js +++ b/packages/block-editor/src/layouts/flex.js @@ -10,7 +10,14 @@ import { arrowRight, arrowDown, } from '@wordpress/icons'; -import { Button, ToggleControl, Flex, FlexItem } from '@wordpress/components'; +import { + Button, + ToggleControl, + Flex, + FlexItem, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon, +} from '@wordpress/components'; /** * Internal dependencies @@ -289,22 +296,23 @@ function FlexLayoutJustifyContentControl( { } return ( -
    - { __( 'Justification' ) } -
    - { justificationOptions.map( ( { value, icon, label } ) => { - return ( -
    -
    + + { justificationOptions.map( ( { value, icon, label } ) => { + return ( + + ); + } ) } + ); } @@ -327,30 +335,27 @@ function FlexWrapControl( { layout, onChange } ) { function OrientationControl( { layout, onChange } ) { const { orientation = 'horizontal' } = layout; return ( -
    - { __( 'Orientation' ) } -
    + ); } diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index 6076db9b8feec3..f34437f74b573e 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -9,6 +9,9 @@ "keywords": [ "link" ], "textdomain": "default", "attributes": { + "textAlign": { + "type": "string" + }, "url": { "type": "string", "source": "attribute", @@ -56,7 +59,7 @@ }, "supports": { "anchor": true, - "align": true, + "align": false, "alignWide": false, "color": { "__experimentalSkipSerialization": true, diff --git a/packages/block-library/src/button/deprecated.js b/packages/block-library/src/button/deprecated.js index 2da17df69cde51..1ec6fde3fe4a48 100644 --- a/packages/block-library/src/button/deprecated.js +++ b/packages/block-library/src/button/deprecated.js @@ -51,6 +51,20 @@ const migrateBorderRadius = ( attributes ) => { }; }; +function migrateAlign( attributes ) { + if ( ! attributes.align ) { + return attributes; + } + const { align, ...otherAttributes } = attributes; + return { + ...otherAttributes, + className: classnames( + otherAttributes.className, + `align${ attributes.align }` + ), + }; +} + const migrateCustomColorsAndGradients = ( attributes ) => { if ( ! attributes.customTextColor && @@ -780,10 +794,12 @@ const deprecated = [ isEligible: ( attributes ) => !! attributes.customTextColor || !! attributes.customBackgroundColor || - !! attributes.customGradient, + !! attributes.customGradient || + !! attributes.align, migrate: compose( migrateBorderRadius, - migrateCustomColorsAndGradients + migrateCustomColorsAndGradients, + migrateAlign ), save( { attributes } ) { const { diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index c222b55943c1b4..fd38379636faf3 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -17,6 +17,7 @@ import { Popover, } from '@wordpress/components'; import { + AlignmentControl, BlockControls, InspectorControls, RichText, @@ -76,7 +77,7 @@ function ButtonEdit( props ) { onReplace, mergeBlocks, } = props; - const { linkTarget, placeholder, rel, style, text, url, width } = + const { textAlign, linkTarget, placeholder, rel, style, text, url, width } = attributes; function onToggleOpenInNewTab( value ) { @@ -170,6 +171,7 @@ function ButtonEdit( props ) { colorProps.className, borderProps.className, { + [ `has-text-align-${ textAlign }` ]: textAlign, // For backwards compatibility add style that isn't // provided via block support. 'no-border-radius': style?.border?.radius === 0, @@ -193,6 +195,12 @@ function ButtonEdit( props ) { /> + { + setAttributes( { textAlign: nextAlign } ); + } } + /> { ! isURLSet && ( + innerBlocks.flatMap( ( innerBlock ) => innerBlock.innerBlocks ), + }, + ], }; export default transforms; diff --git a/packages/block-library/src/cover/edit/index.js b/packages/block-library/src/cover/edit/index.js index 4d7aba03dc7fa3..88f97297a5d93d 100644 --- a/packages/block-library/src/cover/edit/index.js +++ b/packages/block-library/src/cover/edit/index.js @@ -188,7 +188,9 @@ function CoverEdit( { className: 'wp-block-cover__inner-container', }, { - template: innerBlocksTemplate, + // Avoid template sync when the `templateLock` value is `all` or `contentOnly`. + // See: https://github.com/WordPress/gutenberg/pull/45632 + template: ! hasInnerBlocks ? innerBlocksTemplate : undefined, templateInsertUpdatesSelection: true, allowedBlocks, templateLock, diff --git a/packages/block-library/src/latest-posts/block.json b/packages/block-library/src/latest-posts/block.json index cef1a64967f1b2..13a01280c4d1d2 100644 --- a/packages/block-library/src/latest-posts/block.json +++ b/packages/block-library/src/latest-posts/block.json @@ -85,6 +85,15 @@ "supports": { "align": true, "html": false, + "color": { + "gradients": true, + "link": true, + "__experimentalDefaultControls": { + "background": true, + "text": true, + "link": true + } + }, "spacing": { "margin": true, "padding": true diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 5168dbeb60cfef..738a8ab397adf3 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -67,6 +67,7 @@ export default function ListItemEdit( { const innerBlocksProps = useInnerBlocksProps( blockProps, { allowedBlocks: [ 'core/list' ], renderAppender: false, + __unstableDisableDropZone: true, } ); const useEnterRef = useEnter( { content, clientId } ); const useSpaceRef = useSpace( clientId ); diff --git a/packages/block-library/src/navigation-link/edit.js b/packages/block-library/src/navigation-link/edit.js index 1b1b530dcdcdfd..ccd0aae57bcbbf 100644 --- a/packages/block-library/src/navigation-link/edit.js +++ b/packages/block-library/src/navigation-link/edit.js @@ -716,6 +716,14 @@ export default function NavigationLinkEdit( { + { + setAttributes( { url: urlValue } ); + } } + label={ __( 'URL' ) } + autoComplete="off" + /> { diff --git a/packages/block-library/src/navigation/edit/navigation-menu-selector.js b/packages/block-library/src/navigation/edit/navigation-menu-selector.js index 8103dfeef3d1ba..9817934c3a2b16 100644 --- a/packages/block-library/src/navigation/edit/navigation-menu-selector.js +++ b/packages/block-library/src/navigation/edit/navigation-menu-selector.js @@ -170,7 +170,11 @@ function NavigationMenuSelector( { : 'wp-block-navigation__navigation-selector' } label={ selectorLabel } - text={ isOffCanvasNavigationEditorEnabled ? '' : selectorLabel } + text={ + + { isOffCanvasNavigationEditorEnabled ? '' : selectorLabel } + + } icon={ isOffCanvasNavigationEditorEnabled ? moreVertical : null } toggleProps={ isOffCanvasNavigationEditorEnabled diff --git a/packages/block-library/src/navigation/editor.scss b/packages/block-library/src/navigation/editor.scss index 68802b67adf812..8b2dd93e3010be 100644 --- a/packages/block-library/src/navigation/editor.scss +++ b/packages/block-library/src/navigation/editor.scss @@ -639,6 +639,17 @@ body.editor-styles-wrapper width: 100%; } +.wp-block-navigation__navigation-selector-button__icon { + flex: 0 0 auto; +} + +.wp-block-navigation__navigation-selector-button__label { + flex: 0 1 auto; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .wp-block-navigation__navigation-selector-button--createnew { border: 1px solid; margin-bottom: $grid-unit-20; diff --git a/packages/block-library/src/navigation/style.scss b/packages/block-library/src/navigation/style.scss index 27638cd5d84cc7..59622a7039f491 100644 --- a/packages/block-library/src/navigation/style.scss +++ b/packages/block-library/src/navigation/style.scss @@ -676,6 +676,14 @@ button.wp-block-navigation-item__content { top: 0; right: 0; z-index: 2; // Needs to be above the modal z index itself. + + // When set to collapse into a text button, it should inherit the parent font. + // This needs specificity to override inherited properties by the button element and component. + &.wp-block-navigation__responsive-container-close.wp-block-navigation__responsive-container-close { + font-family: inherit; + font-weight: inherit; + font-size: inherit; + } } // The menu adds wrapping containers. diff --git a/packages/block-library/src/post-author/edit.js b/packages/block-library/src/post-author/edit.js index ee8f0722e82bf1..1fc13ea42add9f 100644 --- a/packages/block-library/src/post-author/edit.js +++ b/packages/block-library/src/post-author/edit.js @@ -13,11 +13,23 @@ import { RichText, useBlockProps, } from '@wordpress/block-editor'; -import { PanelBody, SelectControl, ToggleControl } from '@wordpress/components'; +import { + ComboboxControl, + PanelBody, + SelectControl, + ToggleControl, +} from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { store as coreStore } from '@wordpress/core-data'; +const minimumUsersForCombobox = 25; + +const AUTHORS_QUERY = { + who: 'authors', + per_page: 100, +}; + function PostAuthorEdit( { isSelected, context: { postType, postId, queryId }, @@ -38,7 +50,7 @@ function PostAuthorEdit( { return { authorId: _authorId, authorDetails: _authorId ? getUser( _authorId ) : null, - authors: getUsers( { who: 'authors' } ), + authors: getUsers( AUTHORS_QUERY ), }; }, [ postType, postId ] @@ -65,34 +77,46 @@ function PostAuthorEdit( { } ), } ); + const authorOptions = authors?.length + ? authors.map( ( { id, name } ) => { + return { + value: id, + label: name, + }; + } ) + : []; + + const handleSelect = ( nextAuthorId ) => { + editEntityRecord( 'postType', postType, postId, { + author: nextAuthorId, + } ); + }; + + const showCombobox = authorOptions.length >= minimumUsersForCombobox; + return ( <> { !! postId && ! isDescendentOfQueryLoop && - !! authors?.length && ( + authorOptions.length && + ( ( showCombobox && ( + + ) ) || ( { - return { - value: id, - label: name, - }; - } ) } - onChange={ ( nextAuthorId ) => { - editEntityRecord( - 'postType', - postType, - postId, - { - author: nextAuthorId, - } - ); - } } + options={ authorOptions } + onChange={ handleSelect } /> - ) } + ) ) } .wp-block-query-pagination-next:last-child { + > .wp-block-query-pagination-next:last-of-type { margin-inline-start: auto; } > .wp-block-query-pagination-previous:first-child { diff --git a/packages/block-library/src/query/editor.scss b/packages/block-library/src/query/editor.scss index e6eead11f152c0..f6de35bc04f69b 100644 --- a/packages/block-library/src/query/editor.scss +++ b/packages/block-library/src/query/editor.scss @@ -3,7 +3,7 @@ } .wp-block-query__create-new-link { - padding: 0 $grid-unit-20 $grid-unit-20 56px; + padding: 0 $grid-unit-20 $grid-unit-20 52px; } .block-library-query__pattern-selection-content .block-editor-block-patterns-list { diff --git a/packages/block-library/src/read-more/index.php b/packages/block-library/src/read-more/index.php index 93013b2517d141..7bfd22e6d4c4df 100644 --- a/packages/block-library/src/read-more/index.php +++ b/packages/block-library/src/read-more/index.php @@ -19,15 +19,22 @@ function render_block_core_read_more( $attributes, $content, $block ) { } $post_ID = $block->context['postId']; + $post_title = get_the_title( $post_ID ); + $screen_reader_text = sprintf( + /* translators: %s is either the post title or post ID to describe the link for screen readers. */ + __( ': %s' ), + '' !== $post_title ? $post_title : __( 'untitled post ' ) . $post_ID + ); $justify_class_name = empty( $attributes['justifyContent'] ) ? '' : "is-justified-{$attributes['justifyContent']}"; $wrapper_attributes = get_block_wrapper_attributes( array( 'class' => $justify_class_name ) ); $more_text = ! empty( $attributes['content'] ) ? wp_kses_post( $attributes['content'] ) : __( 'Read more' ); return sprintf( - '%4s', + '%4s%5s', $wrapper_attributes, get_the_permalink( $post_ID ), esc_attr( $attributes['linkTarget'] ), - $more_text + $more_text, + $screen_reader_text ); } diff --git a/packages/block-library/src/table/theme.scss b/packages/block-library/src/table/theme.scss index 81558143627876..3adc4d3e452ba7 100644 --- a/packages/block-library/src/table/theme.scss +++ b/packages/block-library/src/table/theme.scss @@ -1,5 +1,5 @@ .wp-block-table { - margin: "0 0 1em 0"; + margin: 0 0 1em 0; thead { border-bottom: 3px solid; diff --git a/packages/block-library/src/template-part/variations.js b/packages/block-library/src/template-part/variations.js index d39b3e5e8a6bc5..866cf15d56c125 100644 --- a/packages/block-library/src/template-part/variations.js +++ b/packages/block-library/src/template-part/variations.js @@ -40,6 +40,10 @@ export function enhanceTemplatePartVariations( settings, name ) { 'wp_template_part', `${ theme }//${ slug }` ); + + if ( entity?.slug ) { + return entity.slug === variationAttributes.slug; + } return entity?.area === variationAttributes.area; }; diff --git a/packages/blocks/README.md b/packages/blocks/README.md index 20930df1a3a9f5..f78783f7283392 100644 --- a/packages/blocks/README.md +++ b/packages/blocks/README.md @@ -692,7 +692,7 @@ _Parameters_ ### registerBlockStyle -Registers a new block style variation for the given block. +Registers a new block style for the given block. For more information on connecting the styles with CSS [the official documentation](/docs/reference-guides/block-api/block-styles.md#styles) @@ -970,7 +970,7 @@ _Returns_ ### unregisterBlockStyle -Unregisters a block style variation for the given block. +Unregisters a block style for the given block. _Usage_ diff --git a/packages/blocks/src/api/registration.js b/packages/blocks/src/api/registration.js index 903d9e7e00e32a..f8f5ac996173b6 100644 --- a/packages/blocks/src/api/registration.js +++ b/packages/blocks/src/api/registration.js @@ -656,7 +656,7 @@ export const hasChildBlocksWithInserterSupport = ( blockName ) => { }; /** - * Registers a new block style variation for the given block. + * Registers a new block style for the given block. * * For more information on connecting the styles with CSS [the official documentation](/docs/reference-guides/block-api/block-styles.md#styles) * @@ -691,7 +691,7 @@ export const registerBlockStyle = ( blockName, styleVariation ) => { }; /** - * Unregisters a block style variation for the given block. + * Unregisters a block style for the given block. * * @param {string} blockName Name of block (example: “core/latest-posts”). * @param {string} styleVariationName Name of class applied to the block. diff --git a/packages/blocks/src/store/reducer.js b/packages/blocks/src/store/reducer.js index 242876476f14ff..6770dd6b67f26b 100644 --- a/packages/blocks/src/store/reducer.js +++ b/packages/blocks/src/store/reducer.js @@ -105,7 +105,7 @@ export function blockTypes( state = {}, action ) { } /** - * Reducer managing the block style variations. + * Reducer managing the block styles. * * @param {Object} state Current state. * @param {Object} action Dispatched action. diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 848f00a8e270be..11bae78e235e2c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,15 +8,26 @@ ### Bug Fix +- `FormTokenField`: Fix duplicate input in IME composition ([#45607](https://github.com/WordPress/gutenberg/pull/45607)). +- `Autocomplete`: Check key events more strictly in IME composition ([#45626](https://github.com/WordPress/gutenberg/pull/45626)). - `Autocomplete`: Fix unexpected block insertion during IME composition ([#45510](https://github.com/WordPress/gutenberg/pull/45510)). +- `Icon`: Making size prop work for icon components using dash icon strings ([#45593](https://github.com/WordPress/gutenberg/pull/45593)) +- `ToolsPanelItem`: Prevent unintended calls to onDeselect when parent panel is remounted and item is rendered via SlotFill ([#45673](https://github.com/WordPress/gutenberg/pull/45673)) +- `ColorPicker`: Prevent all number fields from becoming "0" when one of them is an empty string ([#45649](https://github.com/WordPress/gutenberg/pull/45649)). ### Internal +- `ToolsPanel`: Update to fix `exhaustive-deps` eslint rule ([#45715](https://github.com/WordPress/gutenberg/pull/45715)). - `PaletteEditListView`: Update to ignore `exhaustive-deps` eslint rule ([#45467](https://github.com/WordPress/gutenberg/pull/45467)). +- `Popover`: Update to pass `exhaustive-deps` eslint rule ([#45656](https://github.com/WordPress/gutenberg/pull/45656)). - `Flex`: Update to pass `exhaustive-deps` eslint rule ([#45528](https://github.com/WordPress/gutenberg/pull/45528)). - `withNotices`: Update to pass `exhaustive-deps` eslint rule ([#45530](https://github.com/WordPress/gutenberg/pull/45530)). - `ItemGroup`: Update to pass `exhaustive-deps` eslint rule ([#45531](https://github.com/WordPress/gutenberg/pull/45531)). +- `TabPanel`: Update to pass `exhaustive-deps` eslint rule ([#45660](https://github.com/WordPress/gutenberg/pull/45660)). +- `NavigatorScreen`: Update to pass `exhaustive-deps` eslint rule ([#45648](https://github.com/WordPress/gutenberg/pull/45648)). - `Draggable`: Convert to TypeScript ([#45471](https://github.com/WordPress/gutenberg/pull/45471)). +- `MenuGroup`: Convert to TypeScript ([#45617](https://github.com/WordPress/gutenberg/pull/45617)). +- `useCx`: fix story to satisfy the `react-hooks/exhaustive-deps` eslint rule ([#45614](https://github.com/WordPress/gutenberg/pull/45614)) ### Experimental @@ -89,8 +100,8 @@ - `NumberControl`: Replace `hideHTMLArrows` prop with `spinControls` prop. Allow custom spin controls via `spinControls="custom"` ([#45333](https://github.com/WordPress/gutenberg/pull/45333)). ### Experimental -- Theming: updated Components package to utilize the new `accent` prop of the experimental `Theme` component. +- Theming: updated Components package to utilize the new `accent` prop of the experimental `Theme` component. ## 21.3.0 (2022-10-19) diff --git a/packages/components/src/autocomplete/index.js b/packages/components/src/autocomplete/index.js index 862dc4225ecd1a..f96bd01750c01d 100644 --- a/packages/components/src/autocomplete/index.js +++ b/packages/components/src/autocomplete/index.js @@ -220,7 +220,16 @@ function useAutocomplete( { if ( filteredOptions.length === 0 ) { return; } - if ( event.defaultPrevented ) { + + if ( + event.defaultPrevented || + // Ignore keydowns from IMEs + event.isComposing || + // Workaround for Mac Safari where the final Enter/Backspace of an IME composition + // is `isComposing=false`, even though it's technically still part of the composition. + // These can only be detected by keyCode. + event.keyCode === 229 + ) { return; } switch ( event.key ) { diff --git a/packages/components/src/base-field/test/__snapshots__/index.js.snap b/packages/components/src/base-field/test/__snapshots__/index.js.snap index 273234b79c1f9a..99582f8d40879f 100644 --- a/packages/components/src/base-field/test/__snapshots__/index.js.snap +++ b/packages/components/src/base-field/test/__snapshots__/index.js.snap @@ -131,9 +131,11 @@ exports[`base field should render correctly 1`] = ` box-shadow: 0 0 0 0.5px var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); } -
    +
    +
    +
    `; diff --git a/packages/components/src/base-field/test/index.js b/packages/components/src/base-field/test/index.js index 8204163c2b6c40..871bd8b3f595ee 100644 --- a/packages/components/src/base-field/test/index.js +++ b/packages/components/src/base-field/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -15,33 +15,45 @@ const TestField = ( props ) => { describe( 'base field', () => { it( 'should render correctly', () => { - const base = render( ).container; - expect( base.firstChild ).toMatchSnapshot(); + const { container } = render( ); + expect( container ).toMatchSnapshot(); } ); describe( 'props', () => { it( 'should render error styles', () => { - const base = render( ).container; - const { container } = render( ); - expect( container.firstChild ).toMatchStyleDiffSnapshot( - base.firstChild + render( + <> + + + ); + expect( + screen.getByTestId( 'base-field-error' ) + ).toMatchStyleDiffSnapshot( screen.getByTestId( 'base-field' ) ); } ); it( 'should render inline styles', () => { - const base = render( ).container; - const { container } = render( ); - expect( container.firstChild ).toMatchStyleDiffSnapshot( - base.firstChild + render( + <> + + + ); + expect( + screen.getByTestId( 'base-field-inline' ) + ).toMatchStyleDiffSnapshot( screen.getByTestId( 'base-field' ) ); } ); it( 'should render subtle styles', () => { - const base = render( ).container; - const { container } = render( ); - expect( container.firstChild ).toMatchStyleDiffSnapshot( - base.firstChild + render( + <> + + + ); + expect( + screen.getByTestId( 'base-field-subtle' ) + ).toMatchStyleDiffSnapshot( screen.getByTestId( 'base-field' ) ); } ); } ); diff --git a/packages/components/src/color-picker/input-with-slider.tsx b/packages/components/src/color-picker/input-with-slider.tsx index 8e4e16030ccae3..be09e789d7902e 100644 --- a/packages/components/src/color-picker/input-with-slider.tsx +++ b/packages/components/src/color-picker/input-with-slider.tsx @@ -25,6 +25,18 @@ export const InputWithSlider = ( { onChange, value, }: InputWithSliderProps ) => { + const onNumberControlChange = ( newValue?: number | string ) => { + if ( ! newValue ) { + onChange( 0 ); + return; + } + if ( typeof newValue === 'string' ) { + onChange( parseInt( newValue, 10 ) ); + return; + } + onChange( newValue ); + }; + return ( & OwnProps} Props */ @@ -10,7 +11,8 @@ * @param {Props} props * @return {JSX.Element} Element */ -function Dashicon( { icon, className, ...extraProps } ) { + +function Dashicon( { icon, className, size = 20, style = {}, ...extraProps } ) { const iconClass = [ 'dashicon', 'dashicons', @@ -20,7 +22,24 @@ function Dashicon( { icon, className, ...extraProps } ) { .filter( Boolean ) .join( ' ' ); - return ; + // For retro-compatibility reasons (for example if people are overriding icon size with CSS), we add inline styles just if the size is different to the default + const sizeStyles = + // using `!=` to catch both 20 and "20" + // eslint-disable-next-line eqeqeq + 20 != size + ? { + fontSize: `${ size }px`, + width: `${ size }px`, + height: `${ size }px`, + } + : {}; + + const styles = { + ...sizeStyles, + ...style, + }; + + return ; } export default Dashicon; diff --git a/packages/components/src/date-time/types.ts b/packages/components/src/date-time/types.ts index 5e4f6ec5339a6a..ae99a7082dcd27 100644 --- a/packages/components/src/date-time/types.ts +++ b/packages/components/src/date-time/types.ts @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import type { Moment } from 'moment'; - export type TimePickerProps = { /** * The initial current time the time picker should render. @@ -30,20 +25,6 @@ export type DatePickerEvent = { date: Date; }; -export type DatePickerDayProps = { - /** - * The day to display. - */ - day: Moment; - - /** - * List of events to show on this day. - * - * @default [] - */ - events?: DatePickerEvent[]; -}; - export type DatePickerProps = { /** * The current date and time at initialization. Optionally pass in a `null` diff --git a/packages/components/src/focal-point-picker/focal-point.tsx b/packages/components/src/focal-point-picker/focal-point.tsx index fbc90ab113c67f..21a25f5a832f34 100644 --- a/packages/components/src/focal-point-picker/focal-point.tsx +++ b/packages/components/src/focal-point-picker/focal-point.tsx @@ -1,12 +1,7 @@ /** * Internal dependencies */ -import { - FocalPointWrapper, - PointerIconPathFill, - PointerIconPathOutline, - PointerIconSVG, -} from './styles/focal-point-style'; +import { PointerCircle } from './styles/focal-point-style'; /** * External dependencies @@ -26,22 +21,5 @@ export default function FocalPoint( { const style = { left, top }; - return ( - - - - - - - ); + return ; } diff --git a/packages/components/src/focal-point-picker/styles/focal-point-style.ts b/packages/components/src/focal-point-picker/styles/focal-point-style.ts index 0f46d7c5738574..201924ec6f6c6e 100644 --- a/packages/components/src/focal-point-picker/styles/focal-point-style.ts +++ b/packages/components/src/focal-point-picker/styles/focal-point-style.ts @@ -3,45 +3,21 @@ */ import styled from '@emotion/styled'; -/** - * WordPress dependencies - */ -import { Path, SVG } from '@wordpress/primitives'; - -/** - * Internal dependencies - */ -import { COLORS } from '../../utils'; - -export const FocalPointWrapper = styled.div` +export const PointerCircle = styled.div` background-color: transparent; cursor: grab; - height: 30px; - margin: -15px 0 0 -15px; - opacity: 0.8; + height: 48px; + margin: -24px 0 0 -24px; position: absolute; user-select: none; - width: 30px; + width: 48px; will-change: transform; z-index: 10000; + background: rgba( 255, 255, 255, 0.6 ); + border-radius: 50%; + backdrop-filter: blur( 4px ); + box-shadow: rgb( 0 0 0 / 20% ) 0px 0px 10px; ${ ( { isDragging }: { isDragging: boolean } ) => isDragging && 'cursor: grabbing;' } `; - -export const PointerIconSVG = styled( SVG )` - display: block; - height: 100%; - left: 0; - position: absolute; - top: 0; - width: 100%; -`; - -export const PointerIconPathOutline = styled( Path )` - fill: white; -`; - -export const PointerIconPathFill = styled( Path )` - fill: ${ COLORS.ui.theme }; -`; diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index cca98fd5e6d827..a7c8e9d272f8e3 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -169,10 +169,18 @@ export function FormTokenField( props: FormTokenFieldProps ) { function onKeyDown( event: KeyboardEvent ) { let preventDefault = false; - if ( event.defaultPrevented ) { + if ( + event.defaultPrevented || + // Ignore keydowns from IMEs + event.nativeEvent.isComposing || + // Workaround for Mac Safari where the final Enter/Backspace of an IME composition + // is `isComposing=false`, even though it's technically still part of the composition. + // These can only be detected by keyCode. + event.keyCode === 229 + ) { return; } - switch ( event.code ) { + switch ( event.key ) { case 'Backspace': preventDefault = handleDeleteKey( deleteTokenBeforeInput ); break; @@ -213,9 +221,9 @@ export function FormTokenField( props: FormTokenFieldProps ) { function onKeyPress( event: KeyboardEvent ) { let preventDefault = false; - // TODO: replace to event.code; - switch ( event.charCode ) { - case 44: // Comma. + + switch ( event.key ) { + case ',': preventDefault = handleCommaKey(); break; default: diff --git a/packages/components/src/icon/index.tsx b/packages/components/src/icon/index.tsx index 504a30444e16ed..15b41063fdb1fc 100644 --- a/packages/components/src/icon/index.tsx +++ b/packages/components/src/icon/index.tsx @@ -33,7 +33,7 @@ interface BaseProps< P > { /** * The size (width and height) of the icon. * - * @default 24 + * @default `20` when a Dashicon is rendered, `24` for all other icons. */ size?: number; } @@ -48,13 +48,14 @@ export type Props< P > = BaseProps< P > & AdditionalProps< IconType< P > >; function Icon< P >( { icon = null, - size = 24, + size = 'string' === typeof icon ? 20 : 24, ...additionalProps }: Props< P > ) { if ( 'string' === typeof icon ) { return ( ) } /> ); diff --git a/packages/components/src/icon/test/index.js b/packages/components/src/icon/test/index.js index 5fc897576db1c7..9faff9e74116df 100644 --- a/packages/components/src/icon/test/index.js +++ b/packages/components/src/icon/test/index.js @@ -32,6 +32,16 @@ describe( 'Icon', () => { ); } ); + it( 'renders a dashicon with custom size', () => { + render( + + ); + + expect( screen.getByTestId( testId ) ).toHaveStyle( 'width:10px' ); + expect( screen.getByTestId( testId ) ).toHaveStyle( 'height:10px' ); + expect( screen.getByTestId( testId ) ).toHaveStyle( 'font-size:10px' ); + } ); + it( 'renders a function', () => { render( } /> ); diff --git a/packages/components/src/menu-group/index.js b/packages/components/src/menu-group/index.tsx similarity index 60% rename from packages/components/src/menu-group/index.js rename to packages/components/src/menu-group/index.tsx index 6054bfcb3e4e29..00938928969507 100644 --- a/packages/components/src/menu-group/index.js +++ b/packages/components/src/menu-group/index.tsx @@ -1,4 +1,3 @@ -// @ts-nocheck /** * External dependencies */ @@ -10,7 +9,27 @@ import classnames from 'classnames'; import { Children } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; -export function MenuGroup( props ) { +/** + * Internal dependencies + */ +import type { MenuGroupProps } from './types'; + +/** + * `MenuGroup` wraps a series of related `MenuItem` components into a common + * section. + * + * ```jsx + * import { MenuGroup, MenuItem } from '@wordpress/components'; + * + * const MyMenuGroup = () => ( + * + * Setting 1 + * Setting 2 + * + * ); + * ``` + */ +export function MenuGroup( props: MenuGroupProps ) { const { children, className = '', label, hideSeparator } = props; const instanceId = useInstanceId( MenuGroup ); @@ -34,7 +53,7 @@ export function MenuGroup( props ) { { label }
    ) } -
    +
    { children }
    diff --git a/packages/components/src/menu-group/test/index.js b/packages/components/src/menu-group/test/index.tsx similarity index 100% rename from packages/components/src/menu-group/test/index.js rename to packages/components/src/menu-group/test/index.tsx diff --git a/packages/components/src/menu-group/types.ts b/packages/components/src/menu-group/types.ts new file mode 100644 index 00000000000000..e73538eea68f24 --- /dev/null +++ b/packages/components/src/menu-group/types.ts @@ -0,0 +1,23 @@ +/** + * External dependencies + */ +import type { ReactNode } from 'react'; + +export type MenuGroupProps = { + /** + * A CSS `class` to give to the container element. + */ + className?: string; + /** + * Hide the top border on the container. + */ + hideSeparator?: boolean; + /** + * Text to be displayed as the menu group header. + */ + label?: string; + /** + * The children elements. + */ + children?: ReactNode; +}; diff --git a/packages/components/src/navigator/navigator-screen/component.tsx b/packages/components/src/navigator/navigator-screen/component.tsx index 7365bf9817e81f..266bd553e0a8d2 100644 --- a/packages/components/src/navigator/navigator-screen/component.tsx +++ b/packages/components/src/navigator/navigator-screen/component.tsx @@ -75,6 +75,12 @@ function UnconnectedNavigatorScreen( [ className, cx ] ); + const locationRef = useRef( location ); + + useEffect( () => { + locationRef.current = location; + }, [ location ] ); + // Focus restoration const isInitialLocation = location.isInitial && ! location.isBack; useEffect( () => { @@ -87,7 +93,7 @@ function UnconnectedNavigatorScreen( isInitialLocation || ! isMatch || ! wrapperRef.current || - location.hasRestoredFocus + locationRef.current.hasRestoredFocus ) { return; } @@ -119,12 +125,11 @@ function UnconnectedNavigatorScreen( elementToFocus = firstTabbable ?? wrapperRef.current; } - location.hasRestoredFocus = true; + locationRef.current.hasRestoredFocus = true; elementToFocus.focus(); }, [ isInitialLocation, isMatch, - location.hasRestoredFocus, location.isBack, previousLocation?.focusTargetSelector, ] ); diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index b960edef57c3c6..1d3b2af1dec1ae 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -379,6 +379,17 @@ const UnforwardedPopover = ( // When any of the possible anchor "sources" change, // recompute the reference element (real or virtual) and its owner document. + + const anchorRefTop = ( anchorRef as PopoverAnchorRefTopBottom | undefined ) + ?.top; + const anchorRefBottom = ( + anchorRef as PopoverAnchorRefTopBottom | undefined + )?.bottom; + const anchorRefStartContainer = ( anchorRef as Range | undefined ) + ?.startContainer; + const anchorRefCurrent = ( anchorRef as PopoverAnchorRefReference ) + ?.current; + useLayoutEffect( () => { const resultingReferenceOwnerDoc = getReferenceOwnerDocument( { anchor, @@ -401,11 +412,11 @@ const UnforwardedPopover = ( setReferenceOwnerDocument( resultingReferenceOwnerDoc ); }, [ anchor, - anchorRef as Element | undefined, - ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.top, - ( anchorRef as PopoverAnchorRefTopBottom | undefined )?.bottom, - ( anchorRef as Range | undefined )?.startContainer, - ( anchorRef as PopoverAnchorRefReference )?.current, + anchorRef, + anchorRefTop, + anchorRefBottom, + anchorRefStartContainer, + anchorRefCurrent, anchorRect, getAnchorRect, fallbackReferenceElement, @@ -420,7 +431,7 @@ const UnforwardedPopover = ( // Reference and root documents are the same. referenceOwnerDocument === document || // Reference and floating are in the same document. - referenceOwnerDocument === refs?.floating?.current?.ownerDocument || + referenceOwnerDocument === refs.floating.current?.ownerDocument || // The reference's document has no view (i.e. window) // or frame element (ie. it's not an iframe). ! referenceOwnerDocument?.defaultView?.frameElement @@ -442,7 +453,7 @@ const UnforwardedPopover = ( return () => { defaultView.removeEventListener( 'resize', updateFrameOffset ); }; - }, [ referenceOwnerDocument, update ] ); + }, [ referenceOwnerDocument, update, refs.floating ] ); const mergedFloatingRef = useMergeRefs( [ floating, diff --git a/packages/components/src/tab-panel/index.tsx b/packages/components/src/tab-panel/index.tsx index 4e0b0394bd7409..206c2b8aab0a71 100644 --- a/packages/components/src/tab-panel/index.tsx +++ b/packages/components/src/tab-panel/index.tsx @@ -7,7 +7,7 @@ import { find } from 'lodash'; /** * WordPress dependencies */ -import { useState, useEffect } from '@wordpress/element'; +import { useState, useEffect, useCallback } from '@wordpress/element'; import { useInstanceId } from '@wordpress/compose'; /** @@ -85,10 +85,13 @@ export function TabPanel( { const instanceId = useInstanceId( TabPanel, 'tab-panel' ); const [ selected, setSelected ] = useState< string >(); - const handleTabSelection = ( tabKey: string ) => { - setSelected( tabKey ); - onSelect?.( tabKey ); - }; + const handleTabSelection = useCallback( + ( tabKey: string ) => { + setSelected( tabKey ); + onSelect?.( tabKey ); + }, + [ onSelect ] + ); const onNavigate = ( _childIndex: number, child: HTMLButtonElement ) => { child.click(); @@ -100,7 +103,7 @@ export function TabPanel( { if ( ! selectedTab?.name && tabs.length > 0 ) { handleTabSelection( initialTabName || tabs[ 0 ].name ); } - }, [ tabs, selectedTab?.name, initialTabName ] ); + }, [ tabs, selectedTab?.name, initialTabName, handleTabSelection ] ); return (
    diff --git a/packages/components/src/toggle-group-control/test/index.tsx b/packages/components/src/toggle-group-control/test/index.tsx index 361cbb789f64c7..28b3812fe8dafa 100644 --- a/packages/components/src/toggle-group-control/test/index.tsx +++ b/packages/components/src/toggle-group-control/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render, fireEvent, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** @@ -73,7 +73,10 @@ describe( 'ToggleGroupControl', () => { expect( container ).toMatchSnapshot(); } ); } ); - it( 'should call onChange with proper value', () => { + it( 'should call onChange with proper value', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); const mockOnChange = jest.fn(); render( @@ -86,13 +89,15 @@ describe( 'ToggleGroupControl', () => { ); - const firstRadio = screen.getByRole( 'radio', { name: 'R' } ); - - fireEvent.click( firstRadio ); + await user.click( screen.getByRole( 'radio', { name: 'R' } ) ); expect( mockOnChange ).toHaveBeenCalledWith( 'rigas' ); } ); - it( 'should render tooltip where `showTooltip` === `true`', () => { + + it( 'should render tooltip where `showTooltip` === `true`', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); render( { optionsWithTooltip } @@ -103,14 +108,19 @@ describe( 'ToggleGroupControl', () => { 'Click for Delicious Gnocchi' ); - fireEvent.focus( firstRadio ); + await user.hover( firstRadio ); - expect( - screen.getByText( 'Click for Delicious Gnocchi' ) - ).toBeInTheDocument(); + await waitFor( () => + expect( + screen.getByText( 'Click for Delicious Gnocchi' ) + ).toBeVisible() + ); } ); - it( 'should not render tooltip', () => { + it( 'should not render tooltip', async () => { + const user = userEvent.setup( { + advanceTimers: jest.advanceTimersByTime, + } ); render( { optionsWithTooltip } @@ -121,11 +131,13 @@ describe( 'ToggleGroupControl', () => { 'Click for Sumptuous Caponata' ); - fireEvent.focus( secondRadio ); + await user.hover( secondRadio ); - expect( - screen.queryByText( 'Click for Sumptuous Caponata' ) - ).not.toBeInTheDocument(); + await waitFor( () => + expect( + screen.queryByText( 'Click for Sumptuous Caponata' ) + ).not.toBeInTheDocument() + ); } ); describe( 'isDeselectable', () => { @@ -208,7 +220,7 @@ describe( 'ToggleGroupControl', () => { name: 'R', pressed: false, } ) - ).toBeInTheDocument(); + ).toBeVisible(); } ); it( 'should tab to the next option button', async () => { diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.ts b/packages/components/src/tools-panel/tools-panel-item/hook.ts index 582ff88113dba5..dcc6c6416e0ce9 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-item/hook.ts @@ -108,11 +108,15 @@ export function useToolsPanelItem( const menuGroup = isShownByDefault ? 'default' : 'optional'; const isMenuItemChecked = menuItems?.[ menuGroup ]?.[ label ]; const wasMenuItemChecked = usePrevious( isMenuItemChecked ); + const isRegistered = menuItems?.[ menuGroup ]?.[ label ] !== undefined; // Determine if the panel item's corresponding menu is being toggled and // trigger appropriate callback if it is. useEffect( () => { - if ( isResetting || ! hasMatchingPanel ) { + // We check whether this item is currently registered as items rendered + // via fills can persist through the parent panel being remounted. + // See: https://github.com/WordPress/gutenberg/pull/45673 + if ( ! isRegistered || isResetting || ! hasMatchingPanel ) { return; } @@ -126,6 +130,7 @@ export function useToolsPanelItem( }, [ hasMatchingPanel, isMenuItemChecked, + isRegistered, isResetting, isValueSet, wasMenuItemChecked, diff --git a/packages/components/src/truncate/test/index.tsx b/packages/components/src/truncate/test/index.tsx index 366f1980238c8d..082b6aa232d477 100644 --- a/packages/components/src/truncate/test/index.tsx +++ b/packages/components/src/truncate/test/index.tsx @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -10,34 +10,34 @@ import { Truncate } from '..'; describe( 'props', () => { test( 'should render correctly', () => { - const { container } = render( Lorem ipsum. ); - expect( container.firstChild ).toHaveTextContent( 'Lorem ipsum.' ); + render( Lorem ipsum. ); + expect( screen.getByText( 'Lorem ipsum.' ) ).toBeVisible(); } ); test( 'should render limit', () => { - const { container } = render( + render( Lorem ipsum. ); - expect( container.firstChild ).toHaveTextContent( 'L…' ); + expect( screen.getByText( 'L…' ) ).toBeVisible(); } ); test( 'should render custom ellipsis', () => { - const { container } = render( + render( Lorem ipsum. ); - expect( container.firstChild ).toHaveTextContent( 'Lorem!!!' ); + expect( screen.getByText( 'Lorem!!!' ) ).toBeVisible(); } ); test( 'should render custom ellipsizeMode', () => { - const { container } = render( + render( Lorem ipsum. ); - expect( container.firstChild ).toHaveTextContent( 'Lo!!!m.' ); + expect( screen.getByText( 'Lo!!!m.' ) ).toBeVisible(); } ); } ); diff --git a/packages/components/src/ui/form-group/test/index.js b/packages/components/src/ui/form-group/test/index.js index a9c0edb70efadb..b6bfcd4d8f2110 100644 --- a/packages/components/src/ui/form-group/test/index.js +++ b/packages/components/src/ui/form-group/test/index.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * Internal dependencies @@ -20,44 +20,43 @@ const TextInput = ( { id: idProp, ...props } ) => { /* eslint-disable no-restricted-syntax */ describe( 'props', () => { test( 'should render correctly', () => { - const { container } = render( + render( ); - const label = container.querySelector( 'label' ); - expect( label ).toHaveAttribute( 'for', 'fname' ); - expect( label ).toContainHTML( 'First name' ); - - const input = container.querySelector( 'input' ); - expect( input ).toHaveAttribute( 'id', 'fname' ); + expect( + screen.getByRole( 'textbox', { name: 'First name' } ) + ).toBeVisible(); } ); test( 'should render label without prop correctly', () => { - const { container } = render( + render( First name ); - const label = container.querySelector( 'label' ); - expect( label ).toHaveAttribute( 'for', 'fname' ); - expect( label ).toContainHTML( 'First name' ); + expect( + screen.getByRole( 'textbox', { name: 'First name' } ) + ).toBeVisible(); } ); test( 'should render labelHidden', () => { - const { container } = render( + render( ); - const label = container.querySelector( 'label' ); - expect( label ).toContainHTML( 'First name' ); - // @todo: Refactor this after adding next VisuallyHidden. - expect( label ).toHaveClass( 'components-visually-hidden' ); + expect( + screen.getByRole( 'textbox', { name: 'First name' } ) + ).toBeVisible(); + expect( screen.getByText( 'First name' ) ).toHaveClass( + 'components-visually-hidden' + ); } ); test( 'should render alignLabel', () => { diff --git a/packages/components/src/utils/hooks/stories/use-cx.js b/packages/components/src/utils/hooks/stories/use-cx.js index b8a85f0834dbd7..e7054a8f53952b 100644 --- a/packages/components/src/utils/hooks/stories/use-cx.js +++ b/packages/components/src/utils/hooks/stories/use-cx.js @@ -32,9 +32,9 @@ const Example = ( { serializedStyles, children } ) => { const ExampleWithUseMemoWrong = ( { serializedStyles, children } ) => { const cx = useCx(); // Wrong: using 'useMemo' without adding 'cx' to the dependency list. - // eslint-disable-next-line react-hooks/exhaustive-deps const classes = useMemo( () => cx( serializedStyles ), + // eslint-disable-next-line react-hooks/exhaustive-deps [ serializedStyles ] ); return { children }; diff --git a/packages/components/src/view/index.js b/packages/components/src/view/index.ts similarity index 100% rename from packages/components/src/view/index.js rename to packages/components/src/view/index.ts diff --git a/packages/create-block/README.md b/packages/create-block/README.md index 36d0d632cc391b..d6e77b9576388b 100644 --- a/packages/create-block/README.md +++ b/packages/create-block/README.md @@ -1,18 +1,26 @@ # Create Block -Create Block is an officially supported tool for scaffolding WordPress plugins with blocks. It generates PHP, JS, CSS code, and everything you need to start the project. It integrates a modern build setup with no configuration. +Create Block is an **officially supported tool for scaffolding a WordPress plugin that registers a block**. It generates PHP, JS, CSS code, and everything you need to start the project. It also integrates a modern build setup with no configuration. -It is largely inspired by [create-react-app](https://create-react-app.dev/docs/getting-started). Major kudos to [@gaearon](https://github.com/gaearon), the whole Facebook team, and the React community. +_It is largely inspired by [create-react-app](https://create-react-app.dev/docs/getting-started). Major kudos to [@gaearon](https://github.com/gaearon), the whole Facebook team, and the React community._ -## Description +> **Blocks are the fundamental elements of modern WordPress sites**. Introduced in [WordPress 5.0](https://wordpress.org/news/2018/12/bebo/), they allow [page and post builder-like functionality](https://wordpress.org/gutenberg/) to every up-to-date WordPress website. -Blocks are the fundamental element of the WordPress block editor. They are the primary way in which plugins can register their functionality and extend the editor's capabilities. +> _Learn more about the [Block API at the Gutenberg HandBook](https://developer.wordpress.org/block-editor/developers/block-api/block-registration/)._ -Visit the [Gutenberg handbook](https://developer.wordpress.org/block-editor/developers/block-api/block-registration/) to learn more about Block API. +## Table of Contents + +- [Quick start](#quick-start) +- [Usage](#usage) + - [Interactive Mode](#interactive-mode) + - [`slug`](#slug) + - [`options`](#options) +- [Available Commands](#available-commands) +- [External Project Templates](#external-project-templates) +- [Contributing to this package](#contributing-to-this-package) -## Quick start -You only need to provide the `slug` – the target location for scaffolded plugin files and the internal block name. +## Quick start ```bash $ npx @wordpress/create-block todo-list @@ -20,15 +28,17 @@ $ cd todo-list $ npm start ``` +The `slug` provided (`todo-list` in the example) defines the folder name for the scaffolded plugin and the internal block name. The WordPress plugin generated must [be installed manually](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). + + _(requires `node` version `14.0.0` or above, and `npm` version `6.14.4` or above)_ -It creates a WordPress plugin that you need to [install manually](https://wordpress.org/support/article/managing-plugins/#manual-plugin-installation). -[Watch a video introduction to create-block on Learn.wordpress.org](https://learn.wordpress.org/tutorial/using-the-create-block-tool/) +> [Watch a video introduction to create-block on Learn.wordpress.org](https://learn.wordpress.org/tutorial/using-the-create-block-tool/) ## Usage -The following command generates a project with PHP, JS, and CSS code for registering a block with a WordPress plugin. +The `create-block` command generates a project with PHP, JS, and CSS code for registering a block with a WordPress plugin. ```bash $ npx @wordpress/create-block [options] [slug] @@ -36,9 +46,28 @@ $ npx @wordpress/create-block [options] [slug] ![Demo](https://user-images.githubusercontent.com/699132/103872910-4de15f00-50cf-11eb-8c74-67ca91a8c1a4.gif) -`[slug]` is optional. When provided, it triggers the quick mode where it is used as the block slug used for its identification, the output location for scaffolded files, and the name of the WordPress plugin. The rest of the configuration is set to all default values unless overridden with some options listed below. +> The name for a block is a unique string that identifies a block. Block Names are structured as `namespace`/`slug`, where namespace is the name of your plugin or theme. + +> In most cases, we recommended pairing blocks with WordPress plugins rather than themes, because only using plugin ensures that all blocks still work when your theme changes. + +### Interactive Mode + +When no `slug` is provided, the script will run in interactive mode and will start prompting for the input required (`slug`, title, namespace...) to scaffold the project. + + +### `slug` + +The use of `slug` is optional. + +When provided it triggers the _quick mode_, where this `slug` is used: +- as the block slug (required for its identification) +- as the output location (folder name) for scaffolded files +- as the name of the WordPress plugin. + +The rest of the configuration is set to all default values unless overridden with some options listed below. + +### `options` -Options: ```bash -V, --version output the version number @@ -55,94 +84,61 @@ Options: --variant choose a block variant as defined by the template ``` -More examples: - -1. Interactive mode - without giving a project name, the script will run in interactive mode giving a chance to customize the important options before generating the files. - -```bash -$ npx @wordpress/create-block -``` +#### `--template` -2. External npm package – it is also possible to select an external npm package as a template. +This argument specifies an _external npm package_ as a template. ```bash $ npx @wordpress/create-block --template my-template-package ``` -3. Local template directory – it is also possible to pick a local directory as a template. +This argument also allows to pick a _local directory_ as a template. ```bash $ npx @wordpress/create-block --template ./path/to/template-directory ``` -4. Generating a dynamic block based on the built-in template. +#### `--variant` -```bash -$ npx @wordpress/create-block --variant dynamic -``` - -5. Help – you need to use `npx` to output usage information. +With this argument, `create-block` will generate a [dynamic block](https://developer.wordpress.org/block-editor/explanations/glossary/#dynamic-block) based on the built-in template. ```bash -$ npx @wordpress/create-block --help -``` - -5. No plugin mode – it is also possible to scaffold only block files into the current directory. - -```bash -$ npx @wordpress/create-block --no-plugin +$ npx @wordpress/create-block --variant dynamic ``` -When you scaffold a block, you must provide at least a `slug` name, the `namespace` which usually corresponds to either the `theme` or `plugin` name. In most cases, we recommended pairing blocks with WordPress plugins rather than themes, because only using plugin ensures that all blocks still work when your theme changes. - -## Available Commands +#### `--help` -When bootstrapped with the `static` template (or any other project template with `wpScripts` flag enabled), you can run several commands inside the directory: +With this argument, the `create-block` package outputs usage information. ```bash -$ npm start -``` - -Starts the build for development. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#start). - -```bash -$ npm run build -``` - -Builds the code for production. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#build). - -```bash -$ npm run format +$ npx @wordpress/create-block --help ``` -Formats files. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#format). +#### `--no-plugin` -```bash -$ npm run lint:css -``` - -Lints CSS files. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#lint-style). +With this argument, the `create-block` package runs in _No plugin mode_ which only scaffolds block files into the current directory. ```bash -$ npm run lint:js +$ npx @wordpress/create-block --no-plugin ``` +#### `--wp-env` -Lints JavaScript files. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#lint-js). +With this argument, the `create-block` package will add to the generated plugin the configuration and the script to run [`wp-env` package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/) within the plugin. This will allow you to easily set up a local WordPress environment (via Docker) for building and testing the generated plugin. ```bash -$ npm run plugin-zip +$ npx @wordpress/create-block --wp-env ``` -Creates a zip file for a WordPress plugin. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#plugin-zip). +## Available commands in the scaffolded project -```bash -$ npm run packages-update -``` +The plugin folder created when executing this command, is a node package with a modern build setup that requires no configuration. -Updates WordPress packages to the latest version. [Learn more](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#packages-update). +A set of scripts is available from inside that folder (provided by the `scripts` package) to make your work easier. [Click here](https://github.com/WordPress/gutenberg/tree/HEAD/packages/scripts#available-scripts) for a full description of these commands. _Note: You don’t need to install or configure tools like [webpack](https://webpack.js.org), [Babel](https://babeljs.io) or [ESLint](https://eslint.org) yourself. They are preconfigured and hidden so that you can focus on coding._ +For example, running the `start` script from inside the generated folder (`npm start`) would automatically start the build for development. + ## External Project Templates [Click here](https://github.com/WordPress/gutenberg/tree/HEAD/packages/create-block/docs/external-template.md) for information on External Project Templates diff --git a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts index 2760a8ff69cefb..552a0cef37233e 100644 --- a/packages/e2e-test-utils-playwright/src/editor/publish-post.ts +++ b/packages/e2e-test-utils-playwright/src/editor/publish-post.ts @@ -12,7 +12,7 @@ import type { Editor } from './index'; export async function publishPost( this: Editor ) { await this.page.click( 'role=button[name="Publish"i]' ); const publishEditorPanel = this.page.locator( - 'role=region[name="Publish editor"i]' + 'role=region[name="Editor publish"i]' ); const isPublishEditorVisible = await publishEditorPanel.isVisible(); diff --git a/packages/e2e-test-utils/src/list-view.js b/packages/e2e-test-utils/src/list-view.js index 3ac16475bd9541..58b857652f5899 100644 --- a/packages/e2e-test-utils/src/list-view.js +++ b/packages/e2e-test-utils/src/list-view.js @@ -1,13 +1,15 @@ async function toggleListView() { + // selector .edit-post-header-toolbar__list-view-toggle is still required because the performance tests also execute against older versions that still use that selector. await page.click( - '.edit-post-header-toolbar__list-view-toggle, .edit-site-header-edit-mode__list-view-toggle, .edit-widgets-header-toolbar__list-view-toggle' + '.edit-post-header-toolbar__document-overview-toggle, .edit-post-header-toolbar__list-view-toggle, .edit-site-header-edit-mode__list-view-toggle, .edit-widgets-header-toolbar__list-view-toggle' ); } async function isListViewOpen() { return await page.evaluate( () => { + // selector .edit-post-header-toolbar__list-view-toggle is still required because the performance tests also execute against older versions that still use that selector. return !! document.querySelector( - '.edit-post-header-toolbar__list-view-toggle.is-pressed, .edit-site-header-edit-mode__list-view-toggle.is-pressed, .edit-widgets-header-toolbar__list-view-toggle.is-pressed' + '.edit-post-header-toolbar__document-overview-toggle.is-pressed, .edit-post-header-toolbar__list-view-toggle.is-pressed, .edit-site-header-edit-mode__list-view-toggle.is-pressed, .edit-widgets-header-toolbar__list-view-toggle.is-pressed' ); } ); } diff --git a/packages/e2e-tests/specs/editor/blocks/cover.test.js b/packages/e2e-tests/specs/editor/blocks/cover.test.js index 715ed555631a49..4366bad799bc21 100644 --- a/packages/e2e-tests/specs/editor/blocks/cover.test.js +++ b/packages/e2e-tests/specs/editor/blocks/cover.test.js @@ -127,7 +127,9 @@ describe( 'Cover', () => { ); // Select the cover block.By default the child paragraph gets selected. - await page.click( '.edit-post-header-toolbar__list-view-toggle' ); + await page.click( + '.edit-post-header-toolbar__document-overview-toggle' + ); await page.click( '.block-editor-list-view-block__contents-container a' ); 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 ea16ec75ffda2b..fef0f11eca67a9 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 @@ -49,7 +49,9 @@ describe( 'Navigating the block hierarchy', () => { await page.keyboard.type( 'First column' ); // Navigate to the columns blocks. - await page.click( '.edit-post-header-toolbar__list-view-toggle' ); + await page.click( + '.edit-post-header-toolbar__document-overview-toggle' + ); const firstColumnsBlockMenuItem = ( await getListViewBlocks( 'Columns' ) @@ -183,7 +185,9 @@ describe( 'Navigating the block hierarchy', () => { await page.click( '.editor-post-title' ); // Try selecting the group block using the Outline. - await page.click( '.edit-post-header-toolbar__list-view-toggle' ); + await page.click( + '.edit-post-header-toolbar__document-overview-toggle' + ); const groupMenuItem = ( await getListViewBlocks( 'Group' ) )[ 0 ]; await groupMenuItem.click(); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 488878796586e3..5cf850cdc4bcdd 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Enhancement + +- ` BlockTypesChecklist`: Move BlockIcon component out of CheckboxControl label ([#45535](https://github.com/WordPress/gutenberg/pull/45535)) + ## 6.18.0 (2022-11-02) ## 6.17.0 (2022-10-19) diff --git a/packages/edit-post/src/components/block-manager/checklist.js b/packages/edit-post/src/components/block-manager/checklist.js index 02481f0995e2e0..aa21fefb1c8180 100644 --- a/packages/edit-post/src/components/block-manager/checklist.js +++ b/packages/edit-post/src/components/block-manager/checklist.js @@ -14,17 +14,13 @@ function BlockTypesChecklist( { blockTypes, value, onItemChange } ) { > - { blockType.title } - - - } + label={ blockType.title } checked={ value.includes( blockType.name ) } onChange={ ( ...args ) => onItemChange( blockType.name, ...args ) } /> + ) ) } diff --git a/packages/edit-post/src/components/block-manager/style.scss b/packages/edit-post/src/components/block-manager/style.scss index 17e98948ac7c28..d8f9b78fe5a391 100644 --- a/packages/edit-post/src/components/block-manager/style.scss +++ b/packages/edit-post/src/components/block-manager/style.scss @@ -51,29 +51,19 @@ .edit-post-block-manager__category-title, .edit-post-block-manager__checklist-item { border-bottom: 1px solid $gray-300; - - .components-base-control__field { - align-items: center; - display: flex; - } } .edit-post-block-manager__checklist-item { + display: flex; + justify-content: space-between; + align-items: center; margin-bottom: 0; - padding-left: $grid-unit-20; + padding: $grid-unit-10 0 $grid-unit-10 $grid-unit-20; .components-modal__content &.components-checkbox-control__input-container { margin: 0 $grid-unit-10; } - .components-checkbox-control__label { - display: flex; - align-items: center; - justify-content: space-between; - flex-grow: 1; - padding: $grid-unit-10 0; - } - .block-editor-block-icon { margin-right: 10px; fill: $gray-900; diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index bf22dec0822887..b4d30adc20244a 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -78,12 +78,12 @@ function HeaderToolbar() { <> .edit-post-header__toolbar .edit-post-header-toolbar__inserter-toggle, - & > .edit-post-header__toolbar .edit-post-header-toolbar__list-view-toggle, + & > .edit-post-header__toolbar .edit-post-header-toolbar__document-overview-toggle, & > .edit-post-header__settings > .block-editor-post-preview__dropdown, & > .edit-post-header__settings > .interface-pinned-items { display: none; diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js index 7e098a87be2cd9..fd11d14e08cde0 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/config.js @@ -36,4 +36,14 @@ export const textFormattingShortcuts = [ keyCombination: { modifier: 'access', character: 'x' }, description: __( 'Make the selected text inline code.' ), }, + { + keyCombination: { modifier: 'access', character: '0' }, + description: __( 'Convert the current heading to a paragraph.' ), + }, + { + keyCombination: { modifier: 'access', character: '1-6' }, + description: __( + 'Convert the current paragraph or heading to a heading of level 1 to 6.' + ), + }, ]; diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index 7cc2c124e6fbbd..7b2ba852ad5aab 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -821,6 +821,76 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ
    +
  • +
    + Convert the current heading to a paragraph. +
    +
    + + + Shift + + + + + Alt + + + + + 0 + + +
    +
  • +
  • +
    + Convert the current paragraph or heading to a heading of level 1 to 6. +
    +
    + + + Shift + + + + + Alt + + + + + 1-6 + + +
    +
  • diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index df5205de1c94a7..d3d6569e398a45 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -12,6 +12,7 @@ import { store as editorStore } from '@wordpress/editor'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { store as preferencesStore } from '@wordpress/preferences'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -53,6 +54,35 @@ function KeyboardShortcuts() { closeGeneralSidebar(); }; + const { replaceBlocks } = useDispatch( blockEditorStore ); + const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = + useSelect( blockEditorStore ); + + const handleTextLevelShortcut = ( event, level ) => { + event.preventDefault(); + const destinationBlockName = + level === 0 ? 'core/paragraph' : 'core/heading'; + const currentClientId = getSelectedBlockClientId(); + if ( currentClientId === null ) { + return; + } + const blockName = getBlockName( currentClientId ); + if ( blockName !== 'core/paragraph' && blockName !== 'core/heading' ) { + return; + } + const currentAttributes = getBlockAttributes( currentClientId ); + const { content: currentContent, align: currentAlign } = + currentAttributes; + replaceBlocks( + currentClientId, + createBlock( destinationBlockName, { + level, + content: currentContent, + align: currentAlign, + } ) + ); + }; + useEffect( () => { registerShortcut( { name: 'core/edit-post/toggle-mode', @@ -149,6 +179,28 @@ function KeyboardShortcuts() { character: 'h', }, } ); + + registerShortcut( { + name: `core/block-editor/transform-heading-to-paragraph`, + category: 'block-library', + description: __( 'Transform heading to paragraph.' ), + keyCombination: { + modifier: 'access', + character: `0`, + }, + } ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + registerShortcut( { + name: `core/block-editor/transform-paragraph-to-heading-${ level }`, + category: 'block-library', + description: __( 'Transform paragraph to heading.' ), + keyCombination: { + modifier: 'access', + character: `${ level }`, + }, + } ); + } ); }, [] ); useShortcut( @@ -202,6 +254,21 @@ function KeyboardShortcuts() { setIsListViewOpened( ! isListViewOpened() ) ); + useShortcut( + 'core/block-editor/transform-heading-to-paragraph', + ( event ) => handleTextLevelShortcut( event, 0 ) + ); + + [ 1, 2, 3, 4, 5, 6 ].forEach( ( level ) => { + //the loop is based off on a constant therefore + //the hook will execute the same way every time + //eslint-disable-next-line react-hooks/rules-of-hooks + useShortcut( + `core/block-editor/transform-paragraph-to-heading-${ level }`, + ( event ) => handleTextLevelShortcut( event, level ) + ); + } ); + return null; } diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 3385d2624c4513..7ab7be65e54f26 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -164,7 +164,7 @@ function Layout( { styles } ) { ); const secondarySidebarLabel = isListViewOpened - ? __( 'List View' ) + ? __( 'Document Overview' ) : __( 'Block Library' ); const secondarySidebar = () => { diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index 56f6fce6f17bf1..447fcbd1a70db2 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -43,17 +43,17 @@ export default function ListViewSidebar() { return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions
    + ) } ); } diff --git a/packages/edit-site/src/components/global-styles/test/typography-utils.js b/packages/edit-site/src/components/global-styles/test/typography-utils.js index e0c29a37ea9811..647b02cb4be1fb 100644 --- a/packages/edit-site/src/components/global-styles/test/typography-utils.js +++ b/packages/edit-site/src/components/global-styles/test/typography-utils.js @@ -7,7 +7,7 @@ describe( 'typography utils', () => { describe( 'getTypographyFontSizeValue', () => { [ { - message: 'Default return non-fluid value.', + message: 'returns value when fluid typography is deactivated', preset: { size: '28px', }, @@ -16,7 +16,7 @@ describe( 'typography utils', () => { }, { - message: 'Default return value where font size is 0.', + message: 'returns value where font size is 0', preset: { size: 0, }, @@ -25,7 +25,7 @@ describe( 'typography utils', () => { }, { - message: "Default return value where font size is '0'.", + message: "returns value where font size is '0'", preset: { size: '0', }, @@ -34,17 +34,16 @@ describe( 'typography utils', () => { }, { - message: - 'Default return non-fluid value where `size` is undefined.', + message: 'returns value where `size` is `null`.', preset: { - size: undefined, + size: null, }, - typographySettings: undefined, - expected: undefined, + typographySettings: null, + expected: null, }, { - message: 'return non-fluid value when fluid is `false`.', + message: 'returns value when fluid is `false`', preset: { size: '28px', fluid: false, @@ -56,84 +55,94 @@ describe( 'typography utils', () => { }, { - message: - 'Should coerce integer to `px` and return fluid value.', + message: 'returns already clamped value', preset: { - size: 33, - fluid: true, + size: 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + fluid: false, }, typographySettings: { fluid: true, }, expected: - 'clamp(24.75px, 1.547rem + ((1vw - 7.68px) * 2.975), 49.5px)', + 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', }, { - message: 'coerce float to `px` and return fluid value.', + message: 'returns value with unsupported unit', preset: { - size: 100.23, + size: '1000%', + fluid: false, + }, + typographySettings: { fluid: true, }, + expected: '1000%', + }, + + { + message: 'returns clamp value with rem min and max units', + preset: { + size: '1.75rem', + }, typographySettings: { fluid: true, }, expected: - 'clamp(75.173px, 4.698rem + ((1vw - 7.68px) * 9.035), 150.345px)', + 'clamp(1.313rem, 1.313rem + ((1vw - 0.48rem) * 0.84), 1.75rem)', }, { - message: 'return incoming value when already clamped.', + message: 'returns clamp value with eem min and max units', preset: { - size: 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', - fluid: false, + size: '1.75em', }, typographySettings: { fluid: true, }, expected: - 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + 'clamp(1.313em, 1.313rem + ((1vw - 0.48em) * 0.84), 1.75em)', }, { - message: 'return incoming value with unsupported unit.', + message: 'returns clamp value for floats', preset: { - size: '1000%', - fluid: false, + size: '100.175px', }, typographySettings: { fluid: true, }, - expected: '1000%', + expected: + 'clamp(75.131px, 4.696rem + ((1vw - 7.68px) * 3.01), 100.175px)', }, { - message: 'return fluid value.', + message: 'coerces integer to `px` and returns clamp value', preset: { - size: '1.75rem', + size: 33, + fluid: true, }, typographySettings: { fluid: true, }, expected: - 'clamp(1.313rem, 1.313rem + ((1vw - 0.48rem) * 2.523), 2.625rem)', + 'clamp(24.75px, 1.547rem + ((1vw - 7.68px) * 0.992), 33px)', }, { - message: 'return fluid value for floats with units.', + message: 'coerces float to `px` and returns clamp value', preset: { - size: '100.175px', + size: 100.23, + fluid: true, }, typographySettings: { fluid: true, }, expected: - 'clamp(75.131px, 4.696rem + ((1vw - 7.68px) * 9.03), 150.263px)', + 'clamp(75.173px, 4.698rem + ((1vw - 7.68px) * 3.012), 100.23px)', }, { - message: - 'Should return default fluid values with empty fluid array.', + message: 'returns clamp value when `fluid` is empty array', preset: { size: '28px', fluid: [], @@ -142,11 +151,11 @@ describe( 'typography utils', () => { fluid: true, }, expected: - 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 0.841), 28px)', }, { - message: 'return default fluid values with null value.', + message: 'returns clamp value when `fluid` is `null`', preset: { size: '28px', fluid: null, @@ -155,12 +164,12 @@ describe( 'typography utils', () => { fluid: true, }, expected: - 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 0.841), 28px)', }, { message: - 'return clamped value if min font size is greater than max.', + 'returns clamp value if min font size is greater than max', preset: { size: '3rem', fluid: { @@ -176,7 +185,7 @@ describe( 'typography utils', () => { }, { - message: 'return size with invalid fluid units.', + message: 'returns value with invalid min/max fluid units', preset: { size: '10em', fluid: { @@ -192,20 +201,30 @@ describe( 'typography utils', () => { { message: - 'return clamped using font size where no min is given and size is less than default min size.', + 'returns value when size is < lower bounds and no fluid min/max set', preset: { size: '3px', }, typographySettings: { fluid: true, }, - expected: - 'clamp(3px, 0.188rem + ((1vw - 7.68px) * 0.18), 4.5px)', + expected: '3px', }, { message: - 'return fluid clamp value with different min max units.', + 'returns value when size is equal to lower bounds and no fluid min/max set', + preset: { + size: '14px', + }, + typographySettings: { + fluid: true, + }, + expected: '14px', + }, + + { + message: 'returns clamp value with different min max units', preset: { size: '28px', fluid: { @@ -219,10 +238,9 @@ describe( 'typography utils', () => { expected: 'clamp(20px, 1.25rem + ((1vw - 7.68px) * 93.75), 50rem)', }, - // + { - message: - ' Should return clamp value with default fluid max value.', + message: 'returns clamp value where no fluid max size is set', preset: { size: '28px', fluid: { @@ -233,12 +251,11 @@ describe( 'typography utils', () => { fluid: true, }, expected: - 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * 0.048), 42px)', + 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * -1.635), 28px)', }, { - message: - 'Should return clamp value with default fluid min value.', + message: 'returns clamp value where no fluid min size is set', preset: { size: '28px', fluid: { @@ -253,90 +270,57 @@ describe( 'typography utils', () => { }, { - message: 'adjust computed min in px to min limit.', - preset: { - size: '14px', - }, - typographySettings: { - fluid: true, - }, - expected: - 'clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.841), 21px)', - }, - - { - message: 'adjust computed min in rem to min limit.', - preset: { - size: '1.1rem', - }, - typographySettings: { - fluid: true, - }, - expected: - 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 1.49), 1.65rem)', - }, - - { - message: 'adjust computed min in em to min limit.', - preset: { - size: '1.1em', - }, - typographySettings: { - fluid: true, - }, - expected: - 'clamp(0.875em, 0.875rem + ((1vw - 0.48em) * 1.49), 1.65em)', - }, - - { - message: 'adjust fluid min value in px to min limit', + message: + 'should not apply lower bound test when fluid values are set', preset: { - size: '20px', + size: '1.5rem', fluid: { - min: '12px', + min: '0.5rem', + max: '5rem', }, }, typographySettings: { fluid: true, }, expected: - 'clamp(14px, 0.875rem + ((1vw - 7.68px) * 1.923), 30px)', + 'clamp(0.5rem, 0.5rem + ((1vw - 0.48rem) * 8.654), 5rem)', }, { - message: 'adjust fluid min value in rem to min limit.', + message: + 'should not apply lower bound test when only fluid min is set', preset: { - size: '1.5rem', + size: '20px', fluid: { - min: '0.5rem', + min: '12px', }, }, typographySettings: { fluid: true, }, expected: - 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 2.644), 2.25rem)', + 'clamp(12px, 0.75rem + ((1vw - 7.68px) * 0.962), 20px)', }, { - message: 'adjust fluid min value but honor max value.', + message: + 'should not apply lower bound test when only fluid max is set', preset: { - size: '1.5rem', + size: '0.875rem', fluid: { - min: '0.5rem', - max: '5rem', + max: '20rem', }, }, typographySettings: { fluid: true, }, expected: - 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 7.933), 5rem)', + 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 36.779), 20rem)', }, { message: - 'return a fluid font size a min and max font sizes are equal.', + 'returns clamp value when min and max font sizes are equal', preset: { size: '4rem', fluid: { diff --git a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js index a6bd55ff86ec8f..a26221247dc9b9 100644 --- a/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/edit-site/src/components/global-styles/test/use-global-styles-output.js @@ -711,7 +711,7 @@ describe( 'global styles renderer', () => { }, typography: { fontFamily: 'sans-serif', - fontSize: '14px', + fontSize: '15px', }, }; @@ -725,7 +725,7 @@ describe( 'global styles renderer', () => { '--wp--style--root--padding-left: 33px', 'background-color: var(--wp--preset--color--light-green-cyan)', 'font-family: sans-serif', - 'font-size: 14px', + 'font-size: 15px', ] ); } ); @@ -739,7 +739,7 @@ describe( 'global styles renderer', () => { 'padding-bottom: 33px', 'padding-left: 33px', 'font-family: sans-serif', - 'font-size: 14px', + 'font-size: 15px', ] ); } ); @@ -757,7 +757,7 @@ describe( 'global styles renderer', () => { 'padding-bottom: 33px', 'padding-left: 33px', 'font-family: sans-serif', - 'font-size: 14px', + 'font-size: 15px', ] ); } ); @@ -782,7 +782,7 @@ describe( 'global styles renderer', () => { 'padding-bottom: 33px', 'padding-left: 33px', 'font-family: sans-serif', - 'font-size: clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.841), 21px)', + 'font-size: clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.12), 15px)', ] ); } ); @@ -807,7 +807,7 @@ describe( 'global styles renderer', () => { 'padding-bottom: 33px', 'padding-left: 33px', 'font-family: sans-serif', - 'font-size: 14px', + 'font-size: 15px', ] ); } ); } ); diff --git a/packages/edit-site/src/components/global-styles/typography-panel.js b/packages/edit-site/src/components/global-styles/typography-panel.js index 3e53486af2121c..82f7dc796d92f1 100644 --- a/packages/edit-site/src/components/global-styles/typography-panel.js +++ b/packages/edit-site/src/components/global-styles/typography-panel.js @@ -7,6 +7,7 @@ import { __experimentalFontAppearanceControl as FontAppearanceControl, __experimentalLetterSpacingControl as LetterSpacingControl, __experimentalTextTransformControl as TextTransformControl, + __experimentalTextDecorationControl as TextDecorationControl, } from '@wordpress/block-editor'; import { FontSizePicker, @@ -101,6 +102,13 @@ function useHasTextTransformControl( name, element ) { return supports.includes( 'textTransform' ); } +function useHasTextDecorationControl( name, element ) { + // This is an exception for link elements. + // We shouldn't allow other blocks or elements to set textDecoration + // because this will be inherited by their children. + return ! name && element === 'link'; +} + function useStyleWithReset( path, blockName ) { const [ style, setStyle ] = useStyle( path, blockName ); const [ userStyle ] = useStyle( path, blockName, 'user' ); @@ -190,6 +198,10 @@ export default function TypographyPanel( { name, element, headingLevel } ) { const appearanceControlLabel = useAppearanceControlLabel( name ); const hasLetterSpacingControl = useHasLetterSpacingControl( name, element ); const hasTextTransformControl = useHasTextTransformControl( name, element ); + const hasTextDecorationControl = useHasTextDecorationControl( + name, + element + ); /* Disable font size controls when the option to style all headings is selected. */ let hasFontSizeEnabled = supports.includes( 'fontSize' ); @@ -223,6 +235,12 @@ export default function TypographyPanel( { name, element, headingLevel } ) { hasTextTransform, resetTextTransform, ] = useStyleWithReset( prefix + 'typography.textTransform', name ); + const [ + textDecoration, + setTextDecoration, + hasTextDecoration, + resetTextDecoration, + ] = useStyleWithReset( prefix + 'typography.textDecoration', name ); const resetAll = () => { resetFontFamily(); @@ -347,6 +365,22 @@ export default function TypographyPanel( { name, element, headingLevel } ) { /> ) } + { hasTextDecorationControl && ( + + + + ) } ); } diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js index c822dda69560ff..2bf1ba13a36a1e 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js @@ -24,6 +24,7 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; import { store as preferencesStore } from '@wordpress/preferences'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -147,7 +148,7 @@ export default function DocumentActions() { entityLabel ) } - { entityTitle } + { decodeEntities( entityTitle ) } - { template.description } + { decodeEntities( template.description ) } diff --git a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/__snapshots__/index.js.snap b/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/__snapshots__/index.js.snap deleted file mode 100644 index f08a738cdcaedf..00000000000000 --- a/packages/edit-site/src/components/navigation-sidebar/navigation-toggle/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,41 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`NavigationToggle when in full screen mode should display a default site icon if no user uploaded site icon exists 1`] = ` -
    -
    - -
    -
    -`; 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 159a48b2d7989e..9888593a8eca92 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 @@ -15,55 +15,51 @@ import NavigationToggle from '..'; jest.mock( '@wordpress/data/src/components/use-select', () => { // This allows us to tweak the returned value on each test. - const mock = jest.fn(); - return mock; + return jest.fn(); } ); -jest.mock( '@wordpress/data/src/components/use-dispatch', () => ( { - useDispatch: () => ( { setNavigationPanelActiveMenu: jest.fn() } ), -} ) ); - -jest.mock( '@wordpress/core-data' ); describe( 'NavigationToggle', () => { describe( 'when in full screen mode', () => { it( 'should display a user uploaded site icon if it exists', () => { - useSelect.mockImplementation( ( cb ) => { - return cb( () => ( { - getCurrentTemplateNavigationPanelSubMenu: () => 'root', + useSelect.mockImplementation( ( cb ) => + cb( () => ( { getEntityRecord: () => ( { site_icon_url: 'https://fakeUrl.com', } ), isResolving: () => false, isNavigationOpened: () => false, - } ) ); - } ); + } ) ) + ); - render( ); + const { unmount } = render( ); const siteIcon = screen.getByAltText( 'Site Icon' ); - expect( siteIcon ).toBeVisible(); + + // Unmount the UI synchronously so that any async effects, like the on-mount focus + // that shows and positions a tooltip, are cancelled right away and never run. + unmount(); } ); it( 'should display a default site icon if no user uploaded site icon exists', () => { - useSelect.mockImplementation( ( cb ) => { - return cb( () => ( { - getCurrentTemplateNavigationPanelSubMenu: () => 'root', + useSelect.mockImplementation( ( cb ) => + cb( () => ( { getEntityRecord: () => ( { site_icon_url: '', } ), isResolving: () => false, isNavigationOpened: () => false, - } ) ); - } ); + } ) ) + ); - const { container } = render( ); + const { unmount } = render( ); - expect( - screen.queryByAltText( 'Site Icon' ) - ).not.toBeInTheDocument(); + const siteIcon = screen.queryByAltText( 'Site Icon' ); + expect( siteIcon ).not.toBeInTheDocument(); - expect( container ).toMatchSnapshot(); + // Unmount the UI synchronously so that any async effects, like the on-mount focus + // that shows and positions a tooltip, are cancelled right away and never run. + unmount(); } ); } ); } ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js index eec45cba850adc..96333e3ab01f26 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-card/index.js @@ -5,6 +5,7 @@ import { useSelect } from '@wordpress/data'; import { Icon } from '@wordpress/components'; import { store as editorStore } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -42,12 +43,12 @@ export default function TemplateCard() {

    - { title } + { decodeEntities( title ) }

    - { description } + { decodeEntities( description ) }
    diff --git a/packages/edit-site/src/components/template-details/index.js b/packages/edit-site/src/components/template-details/index.js index 7b9326deaab797..94dda53f75b22f 100644 --- a/packages/edit-site/src/components/template-details/index.js +++ b/packages/edit-site/src/components/template-details/index.js @@ -11,6 +11,7 @@ import { } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; +import { decodeEntities } from '@wordpress/html-entities'; /** * Internal dependencies @@ -65,7 +66,7 @@ export default function TemplateDetails( { template, onClose } ) { className="edit-site-template-details__title" as="p" > - { title } + { decodeEntities( title ) }
    ) } @@ -75,7 +76,7 @@ export default function TemplateDetails( { template, onClose } ) { className="edit-site-template-details__description" as="p" > - { description } + { decodeEntities( description ) } ) } diff --git a/packages/editor/package.json b/packages/editor/package.json index 0fb5506b6e05c2..288a0d427b582f 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -42,6 +42,7 @@ "@wordpress/data": "file:../data", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", + "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/hooks": "file:../hooks", "@wordpress/html-entities": "file:../html-entities", diff --git a/packages/editor/src/components/post-publish-button/index.js b/packages/editor/src/components/post-publish-button/index.js index 86d026c8798b2a..73254e99e9556e 100644 --- a/packages/editor/src/components/post-publish-button/index.js +++ b/packages/editor/src/components/post-publish-button/index.js @@ -34,12 +34,22 @@ export class PostPublishButton extends Component { entitiesSavedStatesCallback: false, }; } + componentDidMount() { if ( this.props.focusOnMount ) { - this.buttonNode.current.focus(); + // This timeout is necessary to make sure the `useEffect` hook of + // `useFocusReturn` gets the correct element (the button that opens the + // PostPublishPanel) otherwise it will get this button. + this.timeoutID = setTimeout( () => { + this.buttonNode.current.focus(); + }, 0 ); } } + componentWillUnmount() { + clearTimeout( this.timeoutID ); + } + createOnClick( callback ) { return ( ...args ) => { const { hasNonPostEntityChanges, setEntitiesSavedStatesCallback } = diff --git a/packages/editor/src/components/post-title/index.js b/packages/editor/src/components/post-title/index.js index 1092ee5959c0bd..b9143a29ff3c05 100644 --- a/packages/editor/src/components/post-title/index.js +++ b/packages/editor/src/components/post-title/index.js @@ -26,6 +26,7 @@ import { insert, } from '@wordpress/rich-text'; import { useMergeRefs } from '@wordpress/compose'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -166,7 +167,7 @@ function PostTitle( _, forwardedRef ) { ( firstBlock.name === 'core/heading' || firstBlock.name === 'core/paragraph' ) ) { - onUpdate( firstBlock.attributes.content ); + onUpdate( stripHTML( firstBlock.attributes.content ) ); onInsertBlockAfter( content.slice( 1 ) ); } else { onInsertBlockAfter( content ); @@ -176,7 +177,10 @@ function PostTitle( _, forwardedRef ) { ...create( { html: title } ), ...selection, }; - const newValue = insert( value, create( { html: content } ) ); + const newValue = insert( + value, + create( { html: stripHTML( content ) } ) + ); onUpdate( toHTMLString( { value: newValue } ) ); setSelection( { start: newValue.start, diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index f5071f80d1a1e3..c7f891a5292dbf 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -125,6 +125,7 @@ export { default as list } from './library/list'; export { default as listItem } from './library/list-item'; export { default as listView } from './library/list-view'; export { default as lock } from './library/lock'; +export { default as lockOutline } from './library/lock-outline'; export { default as login } from './library/login'; export { default as loop } from './library/loop'; export { default as mapMarker } from './library/map-marker'; diff --git a/packages/icons/src/library/lock-outline.js b/packages/icons/src/library/lock-outline.js new file mode 100644 index 00000000000000..dfc21768f55740 --- /dev/null +++ b/packages/icons/src/library/lock-outline.js @@ -0,0 +1,12 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const lockOutline = ( + + + +); + +export default lockOutline; diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index 4d902f4ba4e278..5bb776127d95e7 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -237,7 +237,7 @@ export const displayShortcutList = mapValues( modifiers, ( modifier ) => { // so override the rule to allow symbols used for shortcuts. // see: https://github.com/blakeembrey/change-case#options const capitalizedCharacter = capitalCase( character, { - stripRegexp: /[^A-Z0-9`,\.\\]/gi, + stripRegexp: /[^A-Z0-9`,\.\\\-]/gi, } ); return [ ...modifierKeys, capitalizedCharacter ]; diff --git a/packages/rich-text/src/component/use-anchor.js b/packages/rich-text/src/component/use-anchor.js index dd6172a1661db3..075515c1521cfc 100644 --- a/packages/rich-text/src/component/use-anchor.js +++ b/packages/rich-text/src/component/use-anchor.js @@ -45,13 +45,18 @@ export function useAnchor( { editableContentElement, value, settings = {} } ) { return; } + const selectionWithinEditableContentElement = + editableContentElement?.contains( selection?.anchorNode ); + const range = selection.getRangeAt( 0 ); if ( ! activeFormat ) { return { ownerDocument: range.startContainer.ownerDocument, getBoundingClientRect() { - return range.getBoundingClientRect(); + return selectionWithinEditableContentElement + ? range.getBoundingClientRect() + : editableContentElement.getBoundingClientRect(); }, }; } diff --git a/phpunit/block-supports/typography-test.php b/phpunit/block-supports/typography-test.php index 6ecac215b0f61c..63fd98d1e524ff 100644 --- a/phpunit/block-supports/typography-test.php +++ b/phpunit/block-supports/typography-test.php @@ -315,7 +315,7 @@ public function test_gutenberg_get_typography_font_size_value( $font_size, $shou */ public function data_generate_font_size_preset_fixtures() { return array( - 'default_return_value' => array( + 'returns value when fluid typography is deactivated' => array( 'font_size' => array( 'size' => '28px', ), @@ -323,7 +323,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => '28px', ), - 'size: int 0' => array( + 'returns value where font size is 0' => array( 'font_size' => array( 'size' => 0, ), @@ -331,7 +331,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 0, ), - 'size: string 0' => array( + "returns value where font size is '0'" => array( 'font_size' => array( 'size' => '0', ), @@ -339,7 +339,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => '0', ), - 'default_return_value_when_size_is_undefined' => array( + 'returns value where `size` is `null`' => array( 'font_size' => array( 'size' => null, ), @@ -347,7 +347,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => null, ), - 'default_return_value_when_fluid_is_false' => array( + 'returns value when fluid is `false`' => array( 'font_size' => array( 'size' => '28px', 'fluid' => false, @@ -356,7 +356,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => '28px', ), - 'default_return_value_when_value_is_already_clamped' => array( + 'returns already clamped value' => array( 'font_size' => array( 'size' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', 'fluid' => false, @@ -365,7 +365,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', ), - 'default_return_value_with_unsupported_unit' => array( + 'returns value with unsupported unit' => array( 'font_size' => array( 'size' => '1000%', 'fluid' => false, @@ -374,57 +374,65 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => '1000%', ), - 'return_fluid_value' => array( + 'returns clamp value with rem min and max units' => array( 'font_size' => array( 'size' => '1.75rem', ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(1.313rem, 1.313rem + ((1vw - 0.48rem) * 2.523), 2.625rem)', + 'expected_output' => 'clamp(1.313rem, 1.313rem + ((1vw - 0.48rem) * 0.84), 1.75rem)', ), - 'return_fluid_value_with_floats_with_units' => array( + 'returns clamp value with em min and max units' => array( + 'font_size' => array( + 'size' => '1.75em', + ), + 'should_use_fluid_typography' => true, + 'expected_output' => 'clamp(1.313em, 1.313rem + ((1vw - 0.48em) * 0.84), 1.75em)', + ), + + 'returns clamp value for floats' => array( 'font_size' => array( 'size' => '100.175px', ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(75.131px, 4.696rem + ((1vw - 7.68px) * 9.03), 150.263px)', + 'expected_output' => 'clamp(75.131px, 4.696rem + ((1vw - 7.68px) * 3.01), 100.175px)', ), - 'return_fluid_value_with_integer_coerced_to_px' => array( + 'coerces integer to `px` and returns clamp value' => array( 'font_size' => array( 'size' => 33, ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(24.75px, 1.547rem + ((1vw - 7.68px) * 2.975), 49.5px)', + 'expected_output' => 'clamp(24.75px, 1.547rem + ((1vw - 7.68px) * 0.992), 33px)', ), - 'return_fluid_value_with_float_coerced_to_px' => array( + 'coerces float to `px` and returns clamp value' => array( 'font_size' => array( 'size' => 100.23, ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(75.173px, 4.698rem + ((1vw - 7.68px) * 9.035), 150.345px)', + 'expected_output' => 'clamp(75.173px, 4.698rem + ((1vw - 7.68px) * 3.012), 100.23px)', ), - 'return_default_fluid_values_with_empty_fluid_array' => array( + 'returns clamp value when `fluid` is empty array' => array( 'font_size' => array( 'size' => '28px', 'fluid' => array(), ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 0.841), 28px)', ), - 'return_default_fluid_values_with_null_value' => array( + 'returns clamp value when `fluid` is `null`' => array( 'font_size' => array( 'size' => '28px', 'fluid' => null, ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 2.524), 42px)', + 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 0.841), 28px)', ), - 'return_clamped_value_if_min_font_size_is_greater_than_max' => array( + 'returns clamp value if min font size is greater than max' => array( 'font_size' => array( 'size' => '3rem', 'fluid' => array( @@ -436,7 +444,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 'clamp(5rem, 5rem + ((1vw - 0.48rem) * -5.769), 32px)', ), - 'return_size_with_invalid_fluid_units' => array( + 'returns value with invalid min/max fluid units' => array( 'font_size' => array( 'size' => '10em', 'fluid' => array( @@ -448,15 +456,23 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => '10em', ), - 'return_clamped_size_where_no_min_is_given_and_less_than_default_min_size' => array( + 'returns value when size is < lower bounds and no fluid min/max set' => array( 'font_size' => array( 'size' => '3px', ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(3px, 0.188rem + ((1vw - 7.68px) * 0.18), 4.5px)', + 'expected_output' => '3px', ), - 'return_fluid_clamp_value_with_different_min_max_units' => array( + 'returns value when size is equal to lower bounds and no fluid min/max set' => array( + 'font_size' => array( + 'size' => '14px', + ), + 'should_use_fluid_typography' => true, + 'expected_output' => '14px', + ), + + 'returns clamp value with different min max units' => array( 'font_size' => array( 'size' => '28px', 'fluid' => array( @@ -468,7 +484,7 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 'clamp(20px, 1.25rem + ((1vw - 7.68px) * 93.75), 50rem)', ), - 'return_clamp_value_with_default_fluid_max_value' => array( + 'returns clamp value where no fluid max size is set' => array( 'font_size' => array( 'size' => '28px', 'fluid' => array( @@ -476,10 +492,10 @@ public function data_generate_font_size_preset_fixtures() { ), ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * 0.048), 42px)', + 'expected_output' => 'clamp(2.6rem, 2.6rem + ((1vw - 0.48rem) * -1.635), 28px)', ), - 'default_return_clamp_value_with_default_fluid_min_value' => array( + 'returns clamp value where no fluid min size is set' => array( 'font_size' => array( 'size' => '28px', 'fluid' => array( @@ -490,65 +506,41 @@ public function data_generate_font_size_preset_fixtures() { 'expected_output' => 'clamp(21px, 1.313rem + ((1vw - 7.68px) * 7.091), 80px)', ), - 'should_adjust_computed_min_in_px_to_min_limit' => array( - 'font_size' => array( - 'size' => '14px', - ), - 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(14px, 0.875rem + ((1vw - 7.68px) * 0.841), 21px)', - ), - - 'should_adjust_computed_min_in_rem_to_min_limit' => array( + 'should not apply lower bound test when fluid values are set' => array( 'font_size' => array( - 'size' => '1.1rem', - ), - 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 1.49), 1.65rem)', - ), - - 'default_return_clamp_value_with_replaced_fluid_min_value_in_em' => array( - 'font_size' => array( - 'size' => '1.1em', - ), - 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(0.875em, 0.875rem + ((1vw - 0.48em) * 1.49), 1.65em)', - ), - - 'should_adjust_fluid_min_value_in_px_to_min_limit' => array( - 'font_size' => array( - 'size' => '20px', + 'size' => '1.5rem', 'fluid' => array( - 'min' => '12px', + 'min' => '0.5rem', + 'max' => '5rem', ), ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(14px, 0.875rem + ((1vw - 7.68px) * 1.923), 30px)', + 'expected_output' => 'clamp(0.5rem, 0.5rem + ((1vw - 0.48rem) * 8.654), 5rem)', ), - 'should_adjust_fluid_min_value_in_rem_to_min_limit' => array( + 'should not apply lower bound test when only fluid min is set' => array( 'font_size' => array( - 'size' => '1.5rem', + 'size' => '20px', 'fluid' => array( - 'min' => '0.5rem', + 'min' => '12px', ), ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 2.644), 2.25rem)', + 'expected_output' => 'clamp(12px, 0.75rem + ((1vw - 7.68px) * 0.962), 20px)', ), - 'should_adjust_fluid_min_value_but_honor_max_value' => array( + 'should not apply lower bound test when only fluid max is set' => array( 'font_size' => array( - 'size' => '1.5rem', + 'size' => '0.875rem', 'fluid' => array( - 'min' => '0.5rem', - 'max' => '5rem', + 'max' => '20rem', ), ), 'should_use_fluid_typography' => true, - 'expected_output' => 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 7.933), 5rem)', + 'expected_output' => 'clamp(0.875rem, 0.875rem + ((1vw - 0.48rem) * 36.779), 20rem)', ), - 'should_return_fluid_value_when_min_and_max_font_sizes_are_equal' => array( + 'returns clamp value when min and max font sizes are equal' => array( 'font_size' => array( 'size' => '4rem', 'fluid' => array( @@ -630,7 +622,7 @@ public function data_generate_block_supports_font_size_fixtures() { 'return_value_with_fluid_typography' => array( 'font_size_value' => '50px', 'should_use_fluid_typography' => true, - 'expected_output' => 'font-size:clamp(37.5px, 2.344rem + ((1vw - 7.68px) * 4.507), 75px);', + 'expected_output' => 'font-size:clamp(37.5px, 2.344rem + ((1vw - 7.68px) * 1.502), 50px);', ), ); } @@ -688,7 +680,7 @@ public function data_generate_replace_inline_font_styles_with_fluid_values_fixtu 'block_content' => '', 'font_size_value' => '4rem', 'should_use_fluid_typography' => true, - 'expected_output' => '', + 'expected_output' => '', ), 'return_content_if_no_inline_font_size_found' => array( 'block_content' => '

    A paragraph inside a group

    ', @@ -706,13 +698,13 @@ public function data_generate_replace_inline_font_styles_with_fluid_values_fixtu 'block_content' => '

    A paragraph inside a group

    ', 'font_size_value' => '20px', 'should_use_fluid_typography' => true, - 'expected_output' => '

    A paragraph inside a group

    ', + 'expected_output' => '

    A paragraph inside a group

    ', ), 'return_content_with_first_match_replace_only' => array( 'block_content' => "
    \n \n

    A paragraph inside a group

    ", 'font_size_value' => '1.5em', 'should_use_fluid_typography' => true, - 'expected_output' => "
    \n \n

    A paragraph inside a group

    ", + 'expected_output' => "
    \n \n

    A paragraph inside a group

    ", ), ); } diff --git a/phpunit/class-wp-theme-json-resolver-test.php b/phpunit/class-wp-theme-json-resolver-test.php index 3746d158f20169..153db8c5606791 100644 --- a/phpunit/class-wp-theme-json-resolver-test.php +++ b/phpunit/class-wp-theme-json-resolver-test.php @@ -8,6 +8,60 @@ class WP_Theme_JSON_Resolver_Gutenberg_Test extends WP_UnitTestCase { + /** + * Administrator ID. + * + * @var int + */ + protected static $administrator_id; + + /** + * WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var ReflectionProperty + */ + private static $property_blocks_cache; + + /** + * Original value of the WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var array + */ + private static $property_blocks_cache_orig_value; + + /** + * WP_Theme_JSON_Resolver::$core property. + * + * @var ReflectionProperty + */ + private static $property_core; + + /** + * Original value of the WP_Theme_JSON_Resolver::$core property. + * + * @var WP_Theme_JSON + */ + private static $property_core_orig_value; + + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$administrator_id = self::factory()->user->create( + array( + 'role' => 'administrator', + 'user_email' => 'administrator@example.com', + ) + ); + + static::$property_blocks_cache = new ReflectionProperty( WP_Theme_JSON_Resolver_Gutenberg::class, 'blocks_cache' ); + static::$property_blocks_cache->setAccessible( true ); + static::$property_blocks_cache_orig_value = static::$property_blocks_cache->getValue(); + + static::$property_core = new ReflectionProperty( WP_Theme_JSON_Resolver_Gutenberg::class, 'core' ); + static::$property_core->setAccessible( true ); + static::$property_core_orig_value = static::$property_core->getValue(); + } + public function set_up() { parent::set_up(); $this->theme_root = realpath( __DIR__ . '/data/themedir1' ); @@ -256,28 +310,52 @@ public function test_merges_child_theme_json_into_parent_theme_json() { } public function test_get_user_data_from_wp_global_styles_does_not_use_uncached_queries() { + // Switch to a theme that does have support. + switch_theme( 'block-theme' ); + wp_set_current_user( self::$administrator_id ); + $theme = wp_get_theme(); + WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); add_filter( 'query', array( $this, 'filter_db_query' ) ); $query_count = count( $this->queries ); for ( $i = 0; $i < 3; $i++ ) { - WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( wp_get_theme() ); + WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); } $query_count = count( $this->queries ) - $query_count; - $this->assertEquals( 1, $query_count, 'Only one SQL query should be peformed for multiple invocations of WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles()' ); + $this->assertSame( 0, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type prior to creation.' ); - $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( wp_get_theme() ); - $this->assertEmpty( $user_cpt ); + $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); + $this->assertEmpty( $user_cpt, 'User CPT is expected to be empty.' ); - $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( wp_get_theme(), true ); - $this->assertNotEmpty( $user_cpt ); + $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertNotEmpty( $user_cpt, 'User CPT is expected not to be empty.' ); + $query_count = count( $this->queries ); + for ( $i = 0; $i < 3; $i ++ ) { + $new_user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); + WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); + $this->assertSameSets( $user_cpt, $new_user_cpt, "User CPTs do not match on run {$i}." ); + } + $query_count = count( $this->queries ) - $query_count; + $this->assertSame( 1, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type after creation.' ); + } + + /** + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_does_not_use_uncached_queries_for_logged_out_users() { + $theme = wp_get_theme(); + WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); + add_filter( 'query', array( $this, 'filter_db_query' ) ); $query_count = count( $this->queries ); for ( $i = 0; $i < 3; $i++ ) { - WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( wp_get_theme() ); + WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); WP_Theme_JSON_Resolver_Gutenberg::clean_cached_data(); } $query_count = count( $this->queries ) - $query_count; - $this->assertEquals( 0, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type' ); - remove_filter( 'query', array( $this, 'filter_db_query' ) ); + $this->assertSame( 0, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type prior to creation.' ); + + $user_cpt = WP_Theme_JSON_Resolver_Gutenberg::get_user_data_from_wp_global_styles( $theme ); + $this->assertEmpty( $user_cpt, 'User CPT is expected to be empty.' ); } } diff --git a/readme.txt b/readme.txt index dcc9e7c3598a52..c87aff308e1c52 100644 --- a/readme.txt +++ b/readme.txt @@ -1,6 +1,6 @@ === Gutenberg === Contributors: matveb, joen, karmatosed -Tested up to: 6.0 +Tested up to: 6.1 Stable tag: V.V.V License: GPLv2 or later License URI: http://www.gnu.org/licenses/gpl-2.0.html diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js index fce8991892e146..7fb6b6cf9230ab 100644 --- a/test/e2e/specs/editor/blocks/columns.spec.js +++ b/test/e2e/specs/editor/blocks/columns.spec.js @@ -21,7 +21,7 @@ test.describe( 'Columns', () => { await page.locator( '[aria-label="Two columns; equal split"]' ).click(); // Open List view toggle - await page.locator( 'role=button[name="List View"i]' ).click(); + await page.locator( 'role=button[name="Document Overview"i]' ).click(); // block column add await page diff --git a/test/e2e/specs/editor/various/publish-panel.spec.js b/test/e2e/specs/editor/various/publish-panel.spec.js new file mode 100644 index 00000000000000..1ea72d7eb11a22 --- /dev/null +++ b/test/e2e/specs/editor/various/publish-panel.spec.js @@ -0,0 +1,60 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Post publish panel', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test( 'should move focus back to the Publish panel toggle button when canceling', async ( { + editor, + page, + } ) => { + // Add a paragraph block. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Dummy text' }, + } ); + + // Find and click the Publish panel toggle button. + const publishPanelToggleButton = page.locator( + 'role=region[name="Editor top bar"i] >> role=button[name="Publish"i]' + ); + await publishPanelToggleButton.click(); + + // Click the Cancel button. + await page.click( + 'role=region[name="Editor publish"i] >> role=button[name="Cancel"i]' + ); + + // Test focus is moved back to the Publish panel toggle button. + await expect( publishPanelToggleButton ).toBeFocused(); + } ); + + test( 'should move focus back to the Publish panel toggle button after publishing and closing the panel', async ( { + editor, + page, + } ) => { + // Insert a paragraph block. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Dummy text' }, + } ); + + await editor.publishPost(); + + // Close the publish panel. + await page.click( + 'role=region[name="Editor publish"i] >> role=button[name="Close panel"i]' + ); + + // Test focus is moved back to the Publish panel toggle button. + await expect( + page.locator( + 'role=region[name="Editor top bar"i] >> role=button[name="Update"i]' + ) + ).toBeFocused(); + } ); +} ); diff --git a/test/integration/fixtures/blocks/core__button__center__deprecated.json b/test/integration/fixtures/blocks/core__button__center__deprecated.json index 7c18fd6db96a90..458eebb841b877 100644 --- a/test/integration/fixtures/blocks/core__button__center__deprecated.json +++ b/test/integration/fixtures/blocks/core__button__center__deprecated.json @@ -5,7 +5,7 @@ "attributes": { "url": "https://github.com/WordPress/gutenberg", "text": "Help build Gutenberg", - "align": "center" + "className": "aligncenter" }, "innerBlocks": [] } diff --git a/test/integration/fixtures/blocks/core__button__center__deprecated.serialized.html b/test/integration/fixtures/blocks/core__button__center__deprecated.serialized.html index a923cbd5100dcd..761810b735a721 100644 --- a/test/integration/fixtures/blocks/core__button__center__deprecated.serialized.html +++ b/test/integration/fixtures/blocks/core__button__center__deprecated.serialized.html @@ -1,3 +1,3 @@ - +