diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e606dc21e8074f..4a7a5af6e15205 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -82,8 +82,7 @@ /packages/block-editor/src/components/rich-text @youknowriad @aduth @ellatrix @jorgefilipecosta @daniloercoli @sergioestevao # PHP -/lib @youknowriad @gziolo @aduth -*-controller.php @youknowriad @gziolo @aduth @timothybjacobs +/lib @youknowriad @gziolo @aduth @timothybjacobs # Native (Unowned) *.native.js @ghost diff --git a/.github/actions/milestone-it/Dockerfile b/.github/actions/milestone-it/Dockerfile new file mode 100644 index 00000000000000..af20456bcc34e5 --- /dev/null +++ b/.github/actions/milestone-it/Dockerfile @@ -0,0 +1,18 @@ +FROM debian:stable-slim + +LABEL "name"="Milestone It" +LABEL "maintainer"="The WordPress Contributors" +LABEL "version"="1.0.0" + +LABEL "com.github.actions.name"="Milestone It" +LABEL "com.github.actions.description"="Assigns a pull request to the next milestone" +LABEL "com.github.actions.icon"="flag" +LABEL "com.github.actions.color"="green" + +RUN apt-get update && \ + apt-get install --no-install-recommends -y jq curl ca-certificates && \ + apt-get clean -y + +COPY entrypoint.sh /entrypoint.sh + +ENTRYPOINT [ "/entrypoint.sh" ] diff --git a/.github/actions/milestone-it/entrypoint.sh b/.github/actions/milestone-it/entrypoint.sh new file mode 100755 index 00000000000000..835875dc61a1b4 --- /dev/null +++ b/.github/actions/milestone-it/entrypoint.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -e + +# 1. Determine if milestone already exists (don't replace one which has already +# been assigned). + +pr=$(jq -r '.number' $GITHUB_EVENT_PATH) + +current_milestone=$( + curl \ + --silent \ + -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$pr" \ + | jq '.milestone' +) + +if [ "$current_milestone" != 'null' ]; then + echo 'Milestone already applied. Aborting.' + exit 1; +fi + +# 2. Read current version. + +version=$(jq -r '.version' package.json) + +IFS='.' read -ra parts <<< "$version" +major=${parts[0]} +minor=${parts[1]} + +# 3. Determine next milestone. + +if [[ $minor == 9* ]]; then + major=$((major+1)) + minor="0" +else + minor=$((minor+1)) +fi + +milestone="Gutenberg $major.$minor" + +# 4. Calculate next milestone due date, using a static reference of an earlier +# release (v5.0) as a reference point for the biweekly release schedule. + +reference_major=5 +reference_minor=0 +reference_date=1549238400 +num_versions_elapsed=$(((major-reference_major)*10+(minor-reference_minor))) +weeks=$((num_versions_elapsed*2)) +due=$(date -u --iso-8601=seconds -d "$(date -d @$(echo $reference_date)) + $(echo $weeks) weeks") + +# 5. Create milestone. This may fail for duplicates, which is expected and +# ignored. + +curl \ + --silent \ + -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"title\":\"$milestone\",\"due_on\":\"$due\",\"description\":\"Tasks to be included in the $milestone plugin release.\"}" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/milestones" > /dev/null + +# 6. Find milestone number. This could be improved to allow for non-open status +# or paginated results. + +number=$( + curl \ + --silent \ + -H "Authorization: token $GITHUB_TOKEN" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/milestones" \ + | jq ".[] | select(.title == \"$milestone\") | .number" +) + +# 7. Assign pull request to milestone. + +curl \ + --silent \ + -X POST \ + -H "Authorization: token $GITHUB_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"milestone\":$number}" \ + "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$pr" > /dev/null diff --git a/.github/main.workflow b/.github/main.workflow new file mode 100644 index 00000000000000..a714ca269ada60 --- /dev/null +++ b/.github/main.workflow @@ -0,0 +1,15 @@ +workflow "Milestone merged pull requests" { + on = "pull_request" + resolves = ["Milestone It"] +} + +action "Filter merged" { + uses = "actions/bin/filter@3c0b4f0e63ea54ea5df2914b4fabf383368cd0da" + args = "merged true" +} + +action "Milestone It" { + uses = "./.github/actions/milestone-it" + needs = ["Filter merged"] + secrets = ["GITHUB_TOKEN"] +} diff --git a/.travis.yml b/.travis.yml index 7ff3c9b0445020..60a40cec454eb7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,11 @@ jobs: - name: Build artifacts install: - - npm ci + # A "full" install is executed, since `npm ci` does not always exit + # with an error status code if the lock file is inaccurate. + # + # See: https://github.com/WordPress/gutenberg/issues/16157 + - npm install script: - npm run check-local-changes @@ -81,39 +85,39 @@ jobs: script: - ./bin/run-wp-unit-tests.sh - - name: E2E tests (Admin with plugins) (1/4) - env: WP_VERSION=latest SCRIPT_DEBUG=false POPULAR_PLUGINS=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= + - name: E2E tests (Admin) (1/4) + env: WP_VERSION=latest SCRIPT_DEBUG=false PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 0' < ~/.jest-e2e-tests ) - - name: E2E tests (Admin with plugins) (2/4) - env: WP_VERSION=latest SCRIPT_DEBUG=false POPULAR_PLUGINS=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= + - name: E2E tests (Admin) (2/4) + env: WP_VERSION=latest SCRIPT_DEBUG=false PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 1' < ~/.jest-e2e-tests ) - - name: E2E tests (Admin with plugins) (3/4) - env: WP_VERSION=latest SCRIPT_DEBUG=false POPULAR_PLUGINS=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= + - name: E2E tests (Admin) (3/4) + env: WP_VERSION=latest SCRIPT_DEBUG=false PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 2' < ~/.jest-e2e-tests ) - - name: E2E tests (Admin with plugins) (4/4) - env: WP_VERSION=latest SCRIPT_DEBUG=false POPULAR_PLUGINS=true PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= + - name: E2E tests (Admin) (4/4) + env: WP_VERSION=latest SCRIPT_DEBUG=false PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh script: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 3' < ~/.jest-e2e-tests ) - - name: E2E tests (Author without plugins) (1/4) + - name: E2E tests (Author) (1/4) env: WP_VERSION=latest SCRIPT_DEBUG=false E2E_ROLE=author PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh @@ -121,7 +125,7 @@ jobs: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 0' < ~/.jest-e2e-tests ) - - name: E2E tests (Author without plugins) (2/4) + - name: E2E tests (Author) (2/4) env: WP_VERSION=latest SCRIPT_DEBUG=false E2E_ROLE=author PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh @@ -129,7 +133,7 @@ jobs: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 1' < ~/.jest-e2e-tests ) - - name: E2E tests (Author without plugins) (3/4) + - name: E2E tests (Author) (3/4) env: WP_VERSION=latest SCRIPT_DEBUG=false E2E_ROLE=author PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh @@ -137,7 +141,7 @@ jobs: - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --listTests > ~/.jest-e2e-tests - $( npm bin )/wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" --runTestsByPath $( awk 'NR % 4 == 2' < ~/.jest-e2e-tests ) - - name: E2E tests (Author without plugins) (4/4) + - name: E2E tests (Author) (4/4) env: WP_VERSION=latest SCRIPT_DEBUG=false E2E_ROLE=author PUPPETEER_SKIP_CHROMIUM_DOWNLOAD= install: - ./bin/setup-travis-e2e-tests.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 521f17e5b90798..001086875af3c8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -16,7 +16,7 @@ Please see the [Developer Contributions section](/docs/contributors/develop.md) ## How Can Designers Contribute? -If you'd like to contribute to the design or front-end, feel free to contribute to tickets labelled [Needs Design](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs+Design%22) or [Needs Design Feedback](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A"Needs+Design+Feedback%22). We could use your thoughtful replies, mockups, animatics, sketches, doodles. Proposed changes are best done as minimal and specific iterations on the work that precedes it so we can compare. If you use Sketch, you can grab the source file for the mockups (updated April 6th). +If you'd like to contribute to the design or front-end, feel free to contribute to tickets labelled [Needs Design](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A%22Needs+Design%22) or [Needs Design Feedback](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+label%3A"Needs+Design+Feedback%22). We could use your thoughtful replies, mockups, animatics, sketches, doodles. Proposed changes are best done as minimal and specific iterations on the work that precedes it so we can compare. The [WordPress Design team](http://make.wordpress.org/design/) uses [Figma](https://www.figma.com/) to collaborate and share work. If you'd like to contribute, join the [#design channel](http://wordpress.slack.com/messages/design/) in [Slack](https://make.wordpress.org/chat/) and ask the team to set you up with a free Figma account. This will give you access to a helpful [library of components](https://www.figma.com/file/ZtN5xslEVYgzU7Dd5CxgGZwq/WordPress-Components?node-id=0%3A1) used in WordPress. ## Contribute to the Documentation diff --git a/assets/stylesheets/_animations.scss b/assets/stylesheets/_animations.scss index f856c0bf812d60..9466001700e036 100644 --- a/assets/stylesheets/_animations.scss +++ b/assets/stylesheets/_animations.scss @@ -5,5 +5,5 @@ @mixin edit-post__fade-in-animation($speed: 0.2s, $delay: 0s) { animation: edit-post__fade-in-animation $speed ease-out $delay; animation-fill-mode: forwards; - @include reduce-motion; + @include reduce-motion("animation"); } diff --git a/assets/stylesheets/_mixins.scss b/assets/stylesheets/_mixins.scss index 8551c934d6edb9..d72c0b5b3ecbfc 100644 --- a/assets/stylesheets/_mixins.scss +++ b/assets/stylesheets/_mixins.scss @@ -176,6 +176,7 @@ transition: box-shadow 0.1s linear; border-radius: $radius-round-rectangle; border: $border-width solid $dark-gray-150; + @include reduce-motion("transition"); } @mixin input-style__focus() { @@ -337,10 +338,27 @@ * Allows users to opt-out of animations via OS-level preferences. */ -@mixin reduce-motion { - @media (prefers-reduced-motion: reduce) { - animation-duration: 1ms !important; +@mixin reduce-motion($property: "") { + + @if $property == "transition" { + @media (prefers-reduced-motion: reduce) { + transition-duration: 0s; + } + } + + @else if $property == "animation" { + @media (prefers-reduced-motion: reduce) { + animation-duration: 1ms; + } } + + @else { + @media (prefers-reduced-motion: reduce) { + transition-duration: 0s; + animation-duration: 1ms; + } + } + } /** diff --git a/assets/stylesheets/_z-index.scss b/assets/stylesheets/_z-index.scss index 92a044a7b899a3..12f9b16de20037 100644 --- a/assets/stylesheets/_z-index.scss +++ b/assets/stylesheets/_z-index.scss @@ -9,7 +9,6 @@ $z-layers: ( ".block-library-classic__toolbar": 10, ".block-editor-block-list__layout .reusable-block-indicator": 1, ".block-editor-block-list__breadcrumb": 2, - ".editor-inner-blocks .block-editor-block-list__breadcrumb": 22, ".components-form-toggle__input": 1, ".components-panel__header.edit-post-sidebar__panel-tabs": -1, ".edit-post-sidebar .components-panel": -2, @@ -19,7 +18,6 @@ $z-layers: ( ".components-modal__header": 10, ".edit-post-meta-boxes-area.is-loading::before": 1, ".edit-post-meta-boxes-area .spinner": 5, - ".block-editor-block-contextual-toolbar": 21, ".components-popover__close": 5, ".block-editor-block-list__insertion-point": 6, ".block-editor-inserter-with-shortcuts": 5, @@ -51,16 +49,21 @@ $z-layers: ( ".components-drop-zone": 100, ".components-drop-zone__content": 110, - // The block mover, particularly in nested contexts, - // should overlap most block content. - ".block-editor-block-list__block.is-{selected,hovered} .block-editor-block-mover": 80, - // The block mover for floats should overlap the controls of adjacent blocks. ".block-editor-block-list__block {core/image aligned left or right}": 81, // Small screen inner blocks overlay must be displayed above drop zone, // settings menu, and movers. - ".block-editor-inner-blocks__small-screen-overlay:after": 120, + ".block-editor-inner-blocks.has-overlay::after": 120, + + // The toolbar, when contextual, should be above any adjacent nested block click overlays. + ".block-editor-block-list__layout .reusable-block-edit-panel": 121, + ".block-editor-block-contextual-toolbar": 121, + ".editor-inner-blocks .block-editor-block-list__breadcrumb": 122, + + // The block mover, particularly in nested contexts, + // should overlap most block content. + ".block-editor-block-list__block.is-{selected,hovered} .block-editor-block-mover": 121, // Show sidebar above wp-admin navigation bar for mobile viewports: // #wpadminbar { z-index: 99999 } diff --git a/bin/commander.js b/bin/commander.js index 3412492ce062f4..9eeae01834ac55 100755 --- a/bin/commander.js +++ b/bin/commander.js @@ -278,6 +278,7 @@ async function runReleaseBranchCreationStep( abortMessage ) { return { version, versionLabel, + releaseBranch, }; } @@ -320,6 +321,7 @@ async function runReleaseBranchCheckoutStep( abortMessage ) { return { version, versionLabel: version, + releaseBranch, }; } @@ -415,9 +417,10 @@ async function runCreateGitTagStep( version, abortMessage ) { /** * Push the local Git Changes and Tags to the remote repository. * - * @param {string} abortMessage Abort message. + * @param {string} releaseBranch Release branch name. + * @param {string} abortMessage Abort message. */ -async function runPushGitChangesStep( abortMessage ) { +async function runPushGitChangesStep( releaseBranch, abortMessage ) { await runStep( 'Pushing the release branch and the tag', abortMessage, async () => { const simpleGit = SimpleGit( gitWorkingDirectoryPath ); await askForConfirmationToContinue( @@ -425,7 +428,7 @@ async function runPushGitChangesStep( abortMessage ) { true, abortMessage ); - await simpleGit.push( 'origin' ); + await simpleGit.push( 'origin', releaseBranch ); await simpleGit.pushTags( 'origin' ); } ); } @@ -539,7 +542,7 @@ async function releasePlugin( isRC = true ) { await runGitRepositoryCloneStep( abortMessage ); // Creating the release branch - const { version, versionLabel } = isRC ? + const { version, versionLabel, releaseBranch } = isRC ? await runReleaseBranchCreationStep( abortMessage ) : await runReleaseBranchCheckoutStep( abortMessage ); @@ -553,7 +556,7 @@ async function releasePlugin( isRC = true ) { await runCreateGitTagStep( version, abortMessage ); // Push the local changes - await runPushGitChangesStep( abortMessage ); + await runPushGitChangesStep( releaseBranch, abortMessage ); abortMessage = 'Aborting! Make sure to ' + isRC ? 'remove' : 'reset' + ' the remote release branch and remove the git tag.'; // Creating the GitHub Release diff --git a/bin/install-docker.sh b/bin/install-docker.sh index d6a10c498f0420..f485753b9cc4c6 100755 --- a/bin/install-docker.sh +++ b/bin/install-docker.sh @@ -10,7 +10,7 @@ set -e # Check that Docker is installed. if ! command_exists "docker"; then - echo -e $(error_message "Docker doesn't seem to be installed. Please head on over to the Docker site to download it: $(action_format "https://www.docker.com/community-edition#/download")") + echo -e $(error_message "Docker doesn't seem to be installed. Please head on over to the Docker site to download it: $(action_format "https://www.docker.com/products/docker-desktop")") exit 1 fi diff --git a/bin/install-wordpress.sh b/bin/install-wordpress.sh index 2b7b1ebf84b393..551e8bd0dc77e1 100755 --- a/bin/install-wordpress.sh +++ b/bin/install-wordpress.sh @@ -90,11 +90,6 @@ fi echo -e $(status_message "Activating Gutenberg...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI plugin activate gutenberg --quiet -if [ "$POPULAR_PLUGINS" == "true" ]; then - echo -e $(status_message "Activating popular plugins...") - docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm -u 33 $CLI plugin install advanced-custom-fields jetpack wpforms-lite --activate --quiet -fi - # Install a dummy favicon to avoid 404 errors. echo -e $(status_message "Installing a dummy favicon...") docker-compose $DOCKER_COMPOSE_FILE_OPTIONS run --rm $CONTAINER touch /var/www/html/favicon.ico diff --git a/bin/packages/build-worker.js b/bin/packages/build-worker.js new file mode 100644 index 00000000000000..7e3e636c013a1d --- /dev/null +++ b/bin/packages/build-worker.js @@ -0,0 +1,159 @@ +/** + * External dependencies + */ +const { promisify } = require( 'util' ); +const fs = require( 'fs' ); +const path = require( 'path' ); +const babel = require( '@babel/core' ); +const makeDir = require( 'make-dir' ); +const sass = require( 'node-sass' ); +const postcss = require( 'postcss' ); + +/** + * Internal dependencies + */ +const getBabelConfig = require( './get-babel-config' ); + +/** + * Path to packages directory. + * + * @type {string} + */ +const PACKAGES_DIR = path.resolve( __dirname, '../../packages' ); + +/** + * Mapping of JavaScript environments to corresponding build output. + * + * @type {Object} + */ +const JS_ENVIRONMENTS = { + main: 'build', + module: 'build-module', +}; + +/** + * Promisified fs.readFile. + * + * @type {Function} + */ +const readFile = promisify( fs.readFile ); + +/** + * Promisified fs.writeFile. + * + * @type {Function} + */ +const writeFile = promisify( fs.writeFile ); + +/** + * Promisified sass.render. + * + * @type {Function} + */ +const renderSass = promisify( sass.render ); + +/** + * Get the package name for a specified file + * + * @param {string} file File name + * @return {string} Package name + */ +function getPackageName( file ) { + return path.relative( PACKAGES_DIR, file ).split( path.sep )[ 0 ]; +} + +/** + * Get Build Path for a specified file. + * + * @param {string} file File to build + * @param {string} buildFolder Output folder + * @return {string} Build path + */ +function getBuildPath( file, buildFolder ) { + const pkgName = getPackageName( file ); + const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, 'src' ); + const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder ); + const relativeToSrcPath = path.relative( pkgSrcPath, file ); + return path.resolve( pkgBuildPath, relativeToSrcPath ); +} + +/** + * Object of build tasks per file extension. + * + * @type {Object} + */ +const BUILD_TASK_BY_EXTENSION = { + async '.scss'( file ) { + const outputFile = getBuildPath( file.replace( '.scss', '.css' ), 'build-style' ); + const outputFileRTL = getBuildPath( file.replace( '.scss', '-rtl.css' ), 'build-style' ); + + const [ , contents ] = await Promise.all( [ + makeDir( path.dirname( outputFile ) ), + readFile( file, 'utf8' ), + ] ); + + const builtSass = await renderSass( { + file, + includePaths: [ path.resolve( __dirname, '../../assets/stylesheets' ) ], + data: ( + [ + 'colors', + 'breakpoints', + 'variables', + 'mixins', + 'animations', + 'z-index', + ].map( ( imported ) => `@import "${ imported }";` ).join( ' ' ) + + contents + ), + } ); + + const result = await postcss( require( './post-css-config' ) ).process( builtSass.css, { + from: 'src/app.css', + to: 'dest/app.css', + } ); + + const resultRTL = await postcss( [ require( 'rtlcss' )() ] ).process( result.css, { + from: 'src/app.css', + to: 'dest/app.css', + } ); + + await Promise.all( [ + writeFile( outputFile, result.css ), + writeFile( outputFileRTL, resultRTL.css ), + ] ); + }, + + async '.js'( file ) { + for ( const [ environment, buildDir ] of Object.entries( JS_ENVIRONMENTS ) ) { + const destPath = getBuildPath( file, buildDir ); + const babelOptions = getBabelConfig( environment, file.replace( PACKAGES_DIR, '@wordpress' ) ); + + const [ , transformed ] = await Promise.all( [ + makeDir( path.dirname( destPath ) ), + babel.transformFileAsync( file, babelOptions ), + ] ); + + await Promise.all( [ + writeFile( destPath + '.map', JSON.stringify( transformed.map ) ), + writeFile( destPath, transformed.code + '\n//# sourceMappingURL=' + path.basename( destPath ) + '.map' ), + ] ); + } + }, +}; + +module.exports = async ( file, callback ) => { + const extension = path.extname( file ); + const task = BUILD_TASK_BY_EXTENSION[ extension ]; + + if ( ! task ) { + return; + } + + try { + await task( file ); + callback(); + } catch ( error ) { + callback( error ); + } +}; diff --git a/bin/packages/build.js b/bin/packages/build.js index bb6954b4102e7c..9639f8b232f70e 100755 --- a/bin/packages/build.js +++ b/bin/packages/build.js @@ -1,40 +1,22 @@ -/** - * script to build WordPress packages into `build/` directory. - * - * Example: - * node ./scripts/build.js - */ +/* eslint-disable no-console */ /** * External dependencies */ -const fs = require( 'fs' ); const path = require( 'path' ); -const glob = require( 'glob' ); -const babel = require( '@babel/core' ); -const chalk = require( 'chalk' ); -const mkdirp = require( 'mkdirp' ); -const sass = require( 'node-sass' ); -const postcss = require( 'postcss' ); -const deasync = require( 'deasync' ); +const glob = require( 'fast-glob' ); +const ProgressBar = require( 'progress' ); +const workerFarm = require( 'worker-farm' ); +const { Readable, Transform } = require( 'stream' ); -/** - * Internal dependencies - */ -const getPackages = require( './get-packages' ); -const getBabelConfig = require( './get-babel-config' ); +const files = process.argv.slice( 2 ); /** - * Module Constants + * Path to packages directory. + * + * @type {string} */ const PACKAGES_DIR = path.resolve( __dirname, '../../packages' ); -const SRC_DIR = 'src'; -const BUILD_DIR = { - main: 'build', - module: 'build-module', - style: 'build-style', -}; -const DONE = chalk.reset.inverse.bold.green( ' DONE ' ); /** * Get the package name for a specified file @@ -46,175 +28,111 @@ function getPackageName( file ) { return path.relative( PACKAGES_DIR, file ).split( path.sep )[ 0 ]; } -const isJsFile = ( filepath ) => { - return /.\.js$/.test( filepath ); -}; - -const isScssFile = ( filepath ) => { - return /.\.scss$/.test( filepath ); -}; - -/** - * Get Build Path for a specified file - * - * @param {string} file File to build - * @param {string} buildFolder Output folder - * @return {string} Build path - */ -function getBuildPath( file, buildFolder ) { - const pkgName = getPackageName( file ); - const pkgSrcPath = path.resolve( PACKAGES_DIR, pkgName, SRC_DIR ); - const pkgBuildPath = path.resolve( PACKAGES_DIR, pkgName, buildFolder ); - const relativeToSrcPath = path.relative( pkgSrcPath, file ); - return path.resolve( pkgBuildPath, relativeToSrcPath ); -} - /** - * Given a list of scss and js filepaths, divide them into sets them and rebuild. + * Returns a stream transform which maps an individual stylesheet to its + * package entrypoint. Unlike JavaScript which uses an external bundler to + * efficiently manage rebuilds by entrypoints, stylesheets are rebuilt fresh + * in their entirety from the build script. * - * @param {Array} files list of files to rebuild + * @return {Transform} Stream transform instance. */ -function buildFiles( files ) { - // Reduce files into a unique sets of javaScript files and scss packages. - const buildPaths = files.reduce( ( accumulator, filePath ) => { - if ( isJsFile( filePath ) ) { - accumulator.jsFiles.add( filePath ); - } else if ( isScssFile( filePath ) ) { - const pkgName = getPackageName( filePath ); - const pkgPath = path.resolve( PACKAGES_DIR, pkgName ); - accumulator.scssPackagePaths.add( pkgPath ); - } - return accumulator; - }, { jsFiles: new Set(), scssPackagePaths: new Set() } ); - - buildPaths.jsFiles.forEach( buildJsFile ); - buildPaths.scssPackagePaths.forEach( buildPackageScss ); -} - -/** - * Build a javaScript file for the required environments (node and ES5) - * - * @param {string} file File path to build - * @param {boolean} silent Show logs - */ -function buildJsFile( file, silent ) { - buildJsFileFor( file, silent, 'main' ); - buildJsFileFor( file, silent, 'module' ); +function createStyleEntryTransform() { + const packages = new Set; + + return new Transform( { + objectMode: true, + async transform( file, encoding, callback ) { + // Only stylesheets are subject to this transform. + if ( path.extname( file ) !== '.scss' ) { + this.push( file ); + callback(); + return; + } + + // Only operate once per package, assuming entries are common. + const packageName = getPackageName( file ); + if ( packages.has( packageName ) ) { + callback(); + return; + } + + packages.add( packageName ); + const entries = await glob( path.resolve( PACKAGES_DIR, packageName, 'src/*.scss' ) ); + entries.forEach( ( entry ) => this.push( entry ) ); + callback(); + }, + } ); } -/** - * Build a package's scss styles - * - * @param {string} packagePath The path to the package. - */ -function buildPackageScss( packagePath ) { - const srcDir = path.resolve( packagePath, SRC_DIR ); - const scssFiles = glob.sync( `${ srcDir }/*.scss` ); +let onFileComplete = () => {}; - // Build scss files individually. - scssFiles.forEach( buildScssFile ); -} +let stream; -function buildScssFile( styleFile ) { - const outputFile = getBuildPath( styleFile.replace( '.scss', '.css' ), BUILD_DIR.style ); - const outputFileRTL = getBuildPath( styleFile.replace( '.scss', '-rtl.css' ), BUILD_DIR.style ); - mkdirp.sync( path.dirname( outputFile ) ); - const builtSass = sass.renderSync( { - file: styleFile, - includePaths: [ path.resolve( __dirname, '../../assets/stylesheets' ) ], - data: ( - [ - 'colors', - 'breakpoints', - 'variables', - 'mixins', - 'animations', - 'z-index', - ].map( ( imported ) => `@import "${ imported }";` ).join( ' ' ) + - fs.readFileSync( styleFile, 'utf8' ) - ), +if ( files.length ) { + stream = new Readable( { encoding: 'utf8' } ); + files.forEach( ( file ) => stream.push( file ) ); + stream.push( null ); + stream = stream.pipe( createStyleEntryTransform() ); +} else { + const bar = new ProgressBar( 'Build Progress: [:bar] :percent', { + width: 30, + incomplete: ' ', + total: 1, } ); - const postCSSSync = ( callback ) => { - postcss( require( './post-css-config' ) ) - .process( builtSass.css, { from: 'src/app.css', to: 'dest/app.css' } ) - .then( ( result ) => callback( null, result ) ); - }; - - const postCSSRTLSync = ( ltrCSS, callback ) => { - postcss( [ require( 'rtlcss' )() ] ) - .process( ltrCSS, { from: 'src/app.css', to: 'dest/app.css' } ) - .then( ( result ) => callback( null, result ) ); - }; - - const result = deasync( postCSSSync )(); - fs.writeFileSync( outputFile, result.css ); - - const resultRTL = deasync( postCSSRTLSync )( result ); - fs.writeFileSync( outputFileRTL, resultRTL ); -} - -/** - * Build a file for a specific environment - * - * @param {string} file File path to build - * @param {boolean} silent Show logs - * @param {string} environment Dist environment (node or es5) - */ -function buildJsFileFor( file, silent, environment ) { - const buildDir = BUILD_DIR[ environment ]; - const destPath = getBuildPath( file, buildDir ); - const babelOptions = getBabelConfig( environment, file.replace( PACKAGES_DIR, '@wordpress' ) ); - - mkdirp.sync( path.dirname( destPath ) ); - const transformed = babel.transformFileSync( file, babelOptions ); - fs.writeFileSync( destPath + '.map', JSON.stringify( transformed.map ) ); - fs.writeFileSync( destPath, transformed.code + '\n//# sourceMappingURL=' + path.basename( destPath ) + '.map' ); - - if ( ! silent ) { - process.stdout.write( - chalk.green( ' \u2022 ' ) + - path.relative( PACKAGES_DIR, file ) + - chalk.green( ' \u21D2 ' ) + - path.relative( PACKAGES_DIR, destPath ) + - '\n' - ); - } -} + bar.tick( 0 ); -/** - * Build the provided package path - * - * @param {string} packagePath absolute package path - */ -function buildPackage( packagePath ) { - const srcDir = path.resolve( packagePath, SRC_DIR ); - const jsFiles = glob.sync( `${ srcDir }/**/*.js`, { + stream = glob.stream( [ + `${ PACKAGES_DIR }/*/src/**/*.js`, + `${ PACKAGES_DIR }/*/src/*.scss`, + ], { ignore: [ - `${ srcDir }/**/test/**/*.js`, - `${ srcDir }/**/__mocks__/**/*.js`, + `**/test/**`, + `**/__mocks__/**`, ], - nodir: true, + onlyFiles: true, } ); - process.stdout.write( `${ path.basename( packagePath ) }\n` ); + // Pause to avoid data flow which would begin on the `data` event binding, + // but should wait until worker processing below. + // + // See: https://nodejs.org/api/stream.html#stream_two_reading_modes + stream + .pause() + .on( 'data', ( file ) => { + bar.total = files.push( file ); + } ); + + onFileComplete = () => { + bar.tick(); + }; +} - // Build js files individually. - jsFiles.forEach( ( file ) => buildJsFile( file, true ) ); +const worker = workerFarm( require.resolve( './build-worker' ) ); - // Build package CSS files - buildPackageScss( packagePath ); +let ended = false, + complete = 0; - process.stdout.write( `${ DONE }\n` ); -} +stream + .on( 'data', ( file ) => worker( file, ( error ) => { + onFileComplete(); -const files = process.argv.slice( 2 ); + if ( error ) { + // If an error occurs, the process can't be ended immediately since + // other workers are likely pending. Optimally, it would end at the + // earliest opportunity (after the current round of workers has had + // the chance to complete), but this is not made directly possible + // through `worker-farm`. Instead, ensure at least that when the + // process does exit, it exits with a non-zero code to reflect the + // fact that an error had occurred. + process.exitCode = 1; -if ( files.length ) { - buildFiles( files ); -} else { - process.stdout.write( chalk.inverse( '>> Building packages \n' ) ); - getPackages() - .forEach( buildPackage ); - process.stdout.write( '\n' ); -} + console.error( error ); + } + + if ( ended && ++complete === files.length ) { + workerFarm.end( worker ); + } + } ) ) + .on( 'end', () => ended = true ) + .resume(); diff --git a/bin/packages/get-packages.js b/bin/packages/get-packages.js index ed271db0434f23..de0147435dad23 100644 --- a/bin/packages/get-packages.js +++ b/bin/packages/get-packages.js @@ -3,7 +3,7 @@ */ const fs = require( 'fs' ); const path = require( 'path' ); -const { overEvery } = require( 'lodash' ); +const { isEmpty, overEvery } = require( 'lodash' ); /** * Absolute path to packages directory. @@ -24,6 +24,19 @@ function isDirectory( file ) { return fs.lstatSync( path.resolve( PACKAGES_DIR, file ) ).isDirectory(); } +/** + * Returns true if the given packages has "module" field. + * + * @param {string} file Packages directory file. + * + * @return {boolean} Whether file is a directory. + */ +function hasModuleField( file ) { + const { module } = require( path.resolve( PACKAGES_DIR, file, 'package.json' ) ); + + return ! isEmpty( module ); +} + /** * Filter predicate, returning true if the given base file name is to be * included in the build. @@ -32,7 +45,7 @@ function isDirectory( file ) { * * @return {boolean} Whether to include file in build. */ -const filterPackages = overEvery( isDirectory ); +const filterPackages = overEvery( isDirectory, hasModuleField ); /** * Returns the absolute path of all WordPress packages diff --git a/bin/setup-travis-e2e-tests.sh b/bin/setup-travis-e2e-tests.sh index 0ebc8bc1e6dff3..0594f4724b2e58 100755 --- a/bin/setup-travis-e2e-tests.sh +++ b/bin/setup-travis-e2e-tests.sh @@ -5,7 +5,8 @@ set -e npm ci -npm run build +# Force reduced motion in e2e tests +FORCE_REDUCED_MOTION=true npm run build # Set up environment variables . "$(dirname "$0")/bootstrap-env.sh" diff --git a/docs/designers-developers/assets/toolbar-text.png b/docs/designers-developers/assets/toolbar-text.png index 76b18c6b8f368f..8dbf503d503919 100644 Binary files a/docs/designers-developers/assets/toolbar-text.png and b/docs/designers-developers/assets/toolbar-text.png differ diff --git a/docs/designers-developers/developers/block-api/block-attributes.md b/docs/designers-developers/developers/block-api/block-attributes.md index 203113b06f5f97..2355fbc4356558 100644 --- a/docs/designers-developers/developers/block-api/block-attributes.md +++ b/docs/designers-developers/developers/block-api/block-attributes.md @@ -172,7 +172,7 @@ By default, a meta field will be excluded from a post object's meta. This can be ```php function gutenberg_my_block_init() { - register_meta( 'post', 'author', array( + register_post_meta( 'post', 'author', array( 'show_in_rest' => true, ) ); } @@ -184,11 +184,11 @@ Furthermore, be aware that WordPress defaults to: - not treating a meta datum as being unique, instead returning an array of values; - treating that datum as a string. -If either behavior is not desired, the same `register_meta` call can be complemented with the `single` and/or `type` parameters as follows: +If either behavior is not desired, the same `register_post_meta` call can be complemented with the `single` and/or `type` parameters as follows: ```php function gutenberg_my_block_init() { - register_meta( 'post', 'author_count', array( + register_post_meta( 'post', 'author_count', array( 'show_in_rest' => true, 'single' => true, 'type' => 'integer', diff --git a/docs/designers-developers/developers/block-api/block-registration.md b/docs/designers-developers/developers/block-api/block-registration.md index 8a2d6c26a7dabf..7986be28bee4bc 100644 --- a/docs/designers-developers/developers/block-api/block-registration.md +++ b/docs/designers-developers/developers/block-api/block-registration.md @@ -311,6 +311,40 @@ transforms: { ``` {% end %} +In addition to accepting an array of known block types, the `blocks` option also accepts a "wildcard" (`"*"`). This allows for transformations which apply to _all_ block types (eg: all blocks can transform into `core/group`): + +{% codetabs %} +{% ES5 %} +```js +transforms: { + from: [ + { + type: 'block', + blocks: [ '*' ], // wildcard - match any block + transform: function( attributes, innerBlocks ) { + // transform logic here + }, + }, + ], +}, +``` +{% ESNext %} +```js +transforms: { + from: [ + { + type: 'block', + blocks: [ '*' ], // wildcard - match any block + transform: ( attributes, innerBlocks ) => { + // transform logic here + }, + }, + ], +}, +``` +{% end %} + + A block with innerBlocks can also be transformed from and to another block with innerBlocks. {% codetabs %} diff --git a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md index c959b4cd5aec58..a4ac0f7a33de9b 100644 --- a/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md +++ b/docs/designers-developers/developers/tutorials/block-tutorial/writing-your-first-block-type.md @@ -90,16 +90,16 @@ registerBlockType( 'gutenberg-examples/example-01-basic-esnext', { icon: 'universal-access-alt', category: 'layout', edit() { - return
Basic example with JSX! (editor)
; + return
Hello World, step 1 (from the editor).
; }, save() { - return
Basic example with JSX! (front)
; + return
Hello World, step 1 (from the frontend).
; }, } ); ``` {% end %} -_By now you should be able to see `Hello editor` in the admin side and `Hello saved content` on the frontend side._ +_By now you should be able to see `Hello World, step 1 (from the editor).` in the admin side and `Hello World, step 1 (from the frontend).` on the frontend side._ Once a block is registered, you should immediately see that it becomes available as an option in the editor inserter dialog, using values from `title`, `icon`, and `category` to organize its display. You can choose an icon from any included in the built-in [Dashicons icon set](https://developer.wordpress.org/resource/dashicons/), or provide a [custom svg element](/docs/designers-developers/developers/block-api/block-registration.md#icon-optional). diff --git a/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md index 8a451c3d72361f..19a18057187629 100644 --- a/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md +++ b/docs/designers-developers/developers/tutorials/javascript/extending-the-block-editor.md @@ -22,7 +22,7 @@ Plugin Name: Fancy Quote function myguten_enqueue() { wp_enqueue_script( 'myguten-script', plugins_url( 'myguten.js', __FILE__ ), - array( 'wp-blocks') + array( 'wp-blocks' ) ); } add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); @@ -34,7 +34,9 @@ See [Packages](/docs/designers-developers/developers/packages.md) for list of av After you have updated both JavaScript and PHP files, go to the block editor and create a new post. -Add a quote block, and in the right sidebar under Styles, you will see your new Fancy Quote style listed. Click the Fancy Quote to select and apply that style to your quote block: +Add a quote block, and in the right sidebar under Styles, you will see your new Fancy Quote style listed. + +Click the Fancy Quote to select and apply that style to your quote block: ![Fancy Quote Style in Inspector](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/fancy-quote-in-inspector.png) @@ -42,7 +44,7 @@ Add a quote block, and in the right sidebar under Styles, you will see your new Even if you Preview or Publish the post you will not see a visible change. However, if you look at the source, you will see the `is-style-fancy-quote` class name is now attached to your quote block. -Let's add some style. Go ahead and create a `style.css` file with: +Let's add some style. In your plugin folder, create a `style.css` file with: ```css .is-style-fancy-quote { @@ -59,7 +61,7 @@ function myguten_stylesheet() { add_action( 'enqueue_block_assets', 'myguten_stylesheet' ); ``` -Now when you view in the editor and published, you will see your Fancy Quote style, a delicious tomato color text: +Now when you view in the editor and publish, you will see your Fancy Quote style, a delicious tomato color text: ![Fancy Quote with Style](https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/designers-developers/assets/fancy-quote-with-style.png) diff --git a/docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md b/docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md index 795bd722ece293..744e50dfae5341 100644 --- a/docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md +++ b/docs/designers-developers/developers/tutorials/metabox/meta-block-2-register-meta.md @@ -2,7 +2,7 @@ A post meta field is a WordPress object used to store extra data about a post. You need to first register a new meta field prior to use. See Managing [Post Metadata](https://developer.wordpress.org/plugins/metadata/managing-post-metadata/) to learn more about post meta. -When registering the field, note the `show_in_rest` parameter. This ensures the data will be included in the REST API, which the block editor uses to load and save meta data. See the [`register_meta`](https://developer.wordpress.org/reference/functions/register_meta/) function definition for extra information. +When registering the field, note the `show_in_rest` parameter. This ensures the data will be included in the REST API, which the block editor uses to load and save meta data. See the [`register_post_meta`](https://developer.wordpress.org/reference/functions/register_post_meta/) function definition for extra information. To register the field, create a PHP plugin file called `myguten-meta-block.php` including: @@ -13,20 +13,20 @@ To register the field, create a PHP plugin file called `myguten-meta-block.php` */ // register custom meta tag field -function myguten_register_meta() { - register_meta( 'post', 'myguten_meta_block_field', array( +function myguten_register_post_meta() { + register_post_meta( 'post', 'myguten_meta_block_field', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', ) ); } -add_action( 'init', 'myguten_register_meta' ); +add_action( 'init', 'myguten_register_post_meta' ); ``` -**Note:** If the meta key name starts with an underscore WordPress considers it a protected field. Editing this field requires passing a permission check, which is set as the `auth_callback` in the `register_meta` function. Here is an example: +**Note:** If the meta key name starts with an underscore WordPress considers it a protected field. Editing this field requires passing a permission check, which is set as the `auth_callback` in the `register_post_meta` function. Here is an example: ```php -register_meta( 'post', '_myguten_protected_key', array( +register_post_meta( 'post', '_myguten_protected_key', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', diff --git a/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md b/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md index 0a173a539a82be..67bec79789d844 100644 --- a/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md +++ b/docs/designers-developers/developers/tutorials/metabox/meta-block-3-add.md @@ -60,7 +60,6 @@ Add this code to your JavaScript file (this tutorial will call the file `myguten ``` {% ESNext %} ```jsx - const { registerBlockType } = wp.blocks; const { TextControl } = wp.components; diff --git a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md index da1717c8817f2b..999194e972bae7 100644 --- a/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md +++ b/docs/designers-developers/developers/tutorials/sidebar-tutorial/plugin-sidebar-3-register-meta.md @@ -1,11 +1,11 @@ # Register the Meta Field -To work with fields in the `post_meta` table, WordPress has a function called [register_meta](https://developer.wordpress.org/reference/functions/register_meta/). You're going to use it to register a new field called `sidebar_plugin_meta_block_field`, which will be a single string. Note that this field needs to be available through the [REST API](https://developer.wordpress.org/rest-api/) because that's how the block editor access data. +To work with fields in the `post_meta` table, WordPress has a function called [register_post_meta](https://developer.wordpress.org/reference/functions/register_post_meta/). You're going to use it to register a new field called `sidebar_plugin_meta_block_field`, which will be a single string. Note that this field needs to be available through the [REST API](https://developer.wordpress.org/rest-api/) because that's how the block editor access data. Add this to the PHP code, within the `init` callback function: ```php -register_meta( 'post', 'sidebar_plugin_meta_block_field', array( +register_post_meta( 'post', 'sidebar_plugin_meta_block_field', array( 'show_in_rest' => true, 'single' => true, 'type' => 'string', @@ -18,4 +18,4 @@ To make sure the field has been loaded, query the block editor [internal data st wp.data.select( 'core/editor' ).getCurrentPost().meta; ``` -Before adding the `register_meta` function to the plugin, this code returns a void array, because WordPress hasn't been told to load any meta field yet. After registering the field, the same code will return an object containing the registered meta field you registered. +Before adding the `register_post_meta` function to the plugin, this code returns a void array, because WordPress hasn't been told to load any meta field yet. After registering the field, the same code will return an object containing the registered meta field you registered. diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json index f762d1422c454d..94f21727035536 100644 --- a/docs/manifest-devhub.json +++ b/docs/manifest-devhub.json @@ -899,12 +899,6 @@ "markdown_source": "../packages/components/src/select-control/README.md", "parent": "components" }, - { - "title": "ServerSideRender", - "slug": "server-side-render", - "markdown_source": "../packages/components/src/server-side-render/README.md", - "parent": "components" - }, { "title": "SlotFill", "slug": "slot-fill", @@ -1301,6 +1295,12 @@ "markdown_source": "../packages/list-reusable-blocks/README.md", "parent": "packages" }, + { + "title": "@wordpress/media-utils", + "slug": "packages-media-utils", + "markdown_source": "../packages/media-utils/README.md", + "parent": "packages" + }, { "title": "@wordpress/notices", "slug": "packages-notices", @@ -1355,6 +1355,12 @@ "markdown_source": "../packages/scripts/README.md", "parent": "packages" }, + { + "title": "@wordpress/server-side-render", + "slug": "packages-server-side-render", + "markdown_source": "../packages/server-side-render/README.md", + "parent": "packages" + }, { "title": "@wordpress/shortcode", "slug": "packages-shortcode", diff --git a/docs/manifest.json b/docs/manifest.json index 6fbe894c39d69e..6cee0109f47498 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -449,6 +449,12 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/list-reusable-blocks/README.md", "parent": "packages" }, + { + "title": "@wordpress/media-utils", + "slug": "packages-media-utils", + "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/packages/media-utils/README.md", + "parent": "packages" + }, { "title": "@wordpress/notices", "slug": "packages-notices", @@ -1337,4 +1343,4 @@ "markdown_source": "https://raw.githubusercontent.com/WordPress/gutenberg/master/docs/contributors/outreach.md", "parent": "contributors" } -] \ No newline at end of file +] diff --git a/gutenberg.php b/gutenberg.php index e1f216512fceac..ed0eb8e6c0f508 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -3,7 +3,7 @@ * Plugin Name: Gutenberg * Plugin URI: https://github.com/WordPress/gutenberg * Description: Printing since 1440. This is the development plugin for the new block editor in core. - * Version: 5.8.0 + * Version: 5.9.2 * Author: Gutenberg Team * Text Domain: gutenberg * @@ -89,7 +89,7 @@ function gutenberg_wordpress_version_notice() { */ function gutenberg_build_files_notice() { echo '

'; - _e( 'Gutenberg development mode requires files to be built. Run npm install to install dependencies, npm run build to build the files or npm run dev to build the files and watch for changes. Read the contributing file for more information.', 'gutenberg' ); + _e( 'Gutenberg development mode requires files to be built. Run npm install to install dependencies, npm run build to build the files or npm run dev to build the files and watch for changes. Read the contributing file for more information.', 'gutenberg' ); echo '

'; } diff --git a/lib/client-assets.php b/lib/client-assets.php index 4611cadab1172a..5e4d2651bb603d 100644 --- a/lib/client-assets.php +++ b/lib/client-assets.php @@ -289,6 +289,21 @@ function gutenberg_register_scripts_and_styles() { ) ); + // Add back compatibility for calls to wp.components.ServerSideRender. + wp_add_inline_script( + 'wp-server-side-render', + implode( + "\n", + array( + '( function() {', + ' if ( wp && wp.components && wp.serverSideRender && ! wp.components.ServerSideRender ) {', + ' wp.components.ServerSideRender = wp.serverSideRender;', + ' };', + '} )();', + ) + ) + ); + // Editor Styles. // This empty stylesheet is defined to ensure backward compatibility. gutenberg_override_style( 'wp-blocks', false ); diff --git a/lib/widgets-page.php b/lib/widgets-page.php index 858866c02f1755..f790e60d24b7a1 100644 --- a/lib/widgets-page.php +++ b/lib/widgets-page.php @@ -68,6 +68,8 @@ function gutenberg_widgets_init( $hook ) { 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions(' . wp_json_encode( get_block_editor_server_block_settings() ) . ');' ); wp_enqueue_script( 'wp-edit-widgets' ); + wp_enqueue_script( 'wp-format-library' ); wp_enqueue_style( 'wp-edit-widgets' ); + wp_enqueue_style( 'wp-format-library' ); } add_action( 'admin_enqueue_scripts', 'gutenberg_widgets_init' ); diff --git a/lib/widgets.php b/lib/widgets.php index 28ebc003674759..3c528b14469271 100644 --- a/lib/widgets.php +++ b/lib/widgets.php @@ -11,6 +11,11 @@ * @return boolean True if a screen containing the block editor is being loaded. */ function gutenberg_is_block_editor() { + // If get_current_screen does not exist, we are neither in the standard block editor for posts, or the widget block editor. + // We can safely return false. + if ( ! function_exists( 'get_current_screen' ) ) { + return false; + } $screen = get_current_screen(); return $screen->is_block_editor() || 'gutenberg_page_gutenberg-widgets' === $screen->id; } @@ -100,9 +105,9 @@ function gutenberg_get_legacy_widget_settings() { $has_permissions_to_manage_widgets = current_user_can( 'edit_theme_options' ); $available_legacy_widgets = array(); - global $wp_widget_factory, $wp_registered_widgets; - foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) { - if ( ! in_array( $class, $core_widgets ) ) { + global $wp_widget_factory; + if ( ! empty( $wp_widget_factory ) ) { + foreach ( $wp_widget_factory->widgets as $class => $widget_obj ) { $available_legacy_widgets[ $class ] = array( 'name' => html_entity_decode( $widget_obj->name ), // wp_widget_description is not being used because its input parameter is a Widget Id. @@ -112,22 +117,26 @@ function gutenberg_get_legacy_widget_settings() { html_entity_decode( $widget_obj->widget_options['description'] ) : null, 'isCallbackWidget' => false, + 'isHidden' => in_array( $class, $core_widgets, true ), ); } } - foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) { - if ( - is_array( $widget_obj['callback'] ) && - isset( $widget_obj['callback'][0] ) && - ( $widget_obj['callback'][0] instanceof WP_Widget ) - ) { - continue; + global $wp_registered_widgets; + if ( ! empty( $wp_registered_widgets ) ) { + foreach ( $wp_registered_widgets as $widget_id => $widget_obj ) { + if ( + is_array( $widget_obj['callback'] ) && + isset( $widget_obj['callback'][0] ) && + ( $widget_obj['callback'][0] instanceof WP_Widget ) + ) { + continue; + } + $available_legacy_widgets[ $widget_id ] = array( + 'name' => html_entity_decode( $widget_obj['name'] ), + 'description' => html_entity_decode( wp_widget_description( $widget_id ) ), + 'isCallbackWidget' => true, + ); } - $available_legacy_widgets[ $widget_id ] = array( - 'name' => html_entity_decode( $widget_obj['name'] ), - 'description' => html_entity_decode( wp_widget_description( $widget_id ) ), - 'isCallbackWidget' => true, - ); } $settings['hasPermissionsToManageWidgets'] = $has_permissions_to_manage_widgets; diff --git a/package-lock.json b/package-lock.json index 0ad23808055801..659cba78d7fb3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "5.8.0", + "version": "5.9.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -53,12 +53,6 @@ "minimist": "^1.2.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -937,14 +931,6 @@ "requires": { "exec-sh": "^0.3.2", "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "@iarna/toml": { @@ -3323,6 +3309,7 @@ "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/viewport": "file:packages/viewport", "classnames": "^2.2.5", "fast-average-color": "4.3.0", @@ -3377,7 +3364,6 @@ "requires": { "@babel/runtime": "^7.4.4", "@wordpress/a11y": "file:packages/a11y", - "@wordpress/api-fetch": "file:packages/api-fetch", "@wordpress/compose": "file:packages/compose", "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", @@ -3398,6 +3384,7 @@ "re-resizable": "^4.7.1", "react-click-outside": "^3.0.0", "react-dates": "^17.1.1", + "react-spring": "^8.0.20", "rememo": "^3.0.0", "tinycolor2": "^1.4.1", "uuid": "^3.3.2" @@ -3541,7 +3528,6 @@ "@wordpress/data": "file:packages/data", "@wordpress/editor": "file:packages/editor", "@wordpress/element": "file:packages/element", - "@wordpress/format-library": "file:packages/format-library", "@wordpress/hooks": "file:packages/hooks", "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", @@ -3584,6 +3570,7 @@ "@wordpress/html-entities": "file:packages/html-entities", "@wordpress/i18n": "file:packages/i18n", "@wordpress/keycodes": "file:packages/keycodes", + "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", "@wordpress/url": "file:packages/url", @@ -3725,6 +3712,17 @@ "lodash": "^4.17.11" } }, + "@wordpress/media-utils": { + "version": "file:packages/media-utils", + "requires": { + "@babel/runtime": "^7.4.4", + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/blob": "file:packages/blob", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "lodash": "^4.17.11" + } + }, "@wordpress/notices": { "version": "file:packages/notices", "requires": { @@ -3812,6 +3810,7 @@ "eslint": "^5.16.0", "jest": "^24.7.1", "jest-puppeteer": "^4.0.0", + "minimist": "^1.2.0", "npm-package-json-lint": "^3.6.0", "puppeteer": "1.6.1", "read-pkg-up": "^1.0.1", @@ -3826,6 +3825,19 @@ "webpack-livereload-plugin": "^2.2.0" } }, + "@wordpress/server-side-render": { + "version": "file:packages/server-side-render", + "requires": { + "@babel/runtime": "^7.4.4", + "@wordpress/api-fetch": "file:packages/api-fetch", + "@wordpress/components": "file:packages/components", + "@wordpress/data": "file:packages/data", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "@wordpress/url": "file:packages/url", + "lodash": "^4.17.11" + } + }, "@wordpress/shortcode": { "version": "file:packages/shortcode", "requires": { @@ -5134,44 +5146,36 @@ } }, "browserslist": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.4.1.tgz", - "integrity": "sha512-pEBxEXg7JwaakBXjATYw/D1YZh4QUSCX/Mnd/wnqSRPPSi1U39iDhDoKGoBUcraKdxDlrYqJxSI5nNvD+dWP2A==", + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.2.tgz", + "integrity": "sha512-2neU/V0giQy9h3XMPwLhEY3+Ao0uHSwHvU8Q1Ea6AgLVL1sXbX3dzPrJ8NWe5Hi4PoTkCYXOtVR9rfRLI0J/8Q==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30000929", - "electron-to-chromium": "^1.3.103", - "node-releases": "^1.1.3" + "caniuse-lite": "^1.0.30000974", + "electron-to-chromium": "^1.3.150", + "node-releases": "^1.1.23" }, "dependencies": { "caniuse-lite": { - "version": "1.0.30000929", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000929.tgz", - "integrity": "sha512-n2w1gPQSsYyorSVYqPMqbSaz1w7o9ZC8VhOEGI9T5MfGDzp7sbopQxG6GaQmYsaq13Xfx/mkxJUWC1Dz3oZfzw==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.103", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.103.tgz", - "integrity": "sha512-tObPqGmY9X8MUM8i3MEimYmbnLLf05/QV5gPlkR8MQ3Uj8G8B2govE1U4cQcBYtv3ymck9Y8cIOu4waoiykMZQ==", + "version": "1.0.30000974", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000974.tgz", + "integrity": "sha512-xc3rkNS/Zc3CmpMKuczWEdY2sZgx09BkAxfvkxlAEBTqcMHeL8QnPqhKse+5sRTi3nrw2pJwToD2WvKn1Uhvww==", "dev": true }, "node-releases": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.3.tgz", - "integrity": "sha512-6VrvH7z6jqqNFY200kdB6HdzkgM96Oaj9v3dqGfgp6mF+cHmU4wyQKZ2/WPDRVoR0Jz9KqbamaBN0ZhdUaysUQ==", + "version": "1.1.23", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.23.tgz", + "integrity": "sha512-uq1iL79YjfYC0WXoHbC/z28q/9pOl8kSHaXdWmAAc8No+bDwqkZbzIJz55g/MUsPgSGm9LZ7QSUbzTcH5tz47w==", "dev": true, "requires": { "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", + "dev": true } } }, @@ -5466,12 +5470,6 @@ "semver": "^5.0.3" }, "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "semver": { "version": "5.7.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", @@ -5923,7 +5921,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -6814,12 +6813,6 @@ "trim-newlines": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -6928,12 +6921,6 @@ "trim-newlines": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -7045,12 +7032,6 @@ "trim-newlines": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -8267,6 +8248,12 @@ "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==", "dev": true }, + "electron-to-chromium": { + "version": "1.3.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.155.tgz", + "integrity": "sha512-/ci/XgZG8jkLYOgOe3mpJY1onxPPTDY17y7scldhnSjjZqV6VvREG/LvwhRuV7BJbnENFfuDWZkSqlTh4x9ZjQ==", + "dev": true + }, "elegant-spinner": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/elegant-spinner/-/elegant-spinner-1.0.1.tgz", @@ -9352,9 +9339,9 @@ "dev": true }, "fast-glob": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.6.tgz", - "integrity": "sha512-0BvMaZc1k9F+MeWWMe8pL6YltFzZYcJsYU7D4JyDA6PAczaXvxqQQ/z+mDF7/4Mw01DeUc+i3CTKajnkANkV4w==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", "dev": true, "requires": { "@mrmlnc/readdir-enhanced": "^2.2.1", @@ -9520,6 +9507,23 @@ "commondir": "^1.0.1", "make-dir": "^1.0.0", "pkg-dir": "^2.0.0" + }, + "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + } } }, "find-file-up": { @@ -10194,7 +10198,8 @@ "dependencies": { "minimist": { "version": "1.2.0", - "bundled": true, + "resolved": false, + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true, "optional": true } @@ -10524,12 +10529,6 @@ "trim-newlines": "^1.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", @@ -10640,12 +10639,6 @@ "trim-newlines": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -10740,12 +10733,6 @@ "trim-newlines": "^2.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -12129,6 +12116,21 @@ "dev": true } } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true } } }, @@ -12181,6 +12183,21 @@ "supports-color": "^6.0.0" }, "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, "supports-color": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", @@ -12214,12 +12231,27 @@ "ms": "^2.1.1" } }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", "dev": true }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14239,18 +14271,18 @@ } }, "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", + "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", "dev": true, "requires": { - "pify": "^3.0.0" + "semver": "^6.0.0" }, "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", + "semver": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.0.0.tgz", + "integrity": "sha512-0UewU+9rFapKFnlbirLi3byoOuhrSsli/z/ihNnvM24vgF+8sNBiI1LZPBSH9wJKUwaUbw+s3hToDLCXkrghrQ==", "dev": true } } @@ -14804,9 +14836,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, "minimist-options": { @@ -14910,6 +14942,14 @@ "dev": true, "requires": { "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } } }, "modify-values": { @@ -15369,12 +15409,6 @@ "mime-db": "1.40.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "nan": { "version": "2.13.2", "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", @@ -16025,6 +16059,12 @@ "wordwrap": "~0.0.2" }, "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, "wordwrap": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", @@ -16684,12 +16724,6 @@ "minimist": "^1.2.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -17446,12 +17480,6 @@ "minimist": "^1.2.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "ms": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", @@ -18023,9 +18051,9 @@ "dev": true }, "progress": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz", - "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8=", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, "promise": { @@ -18256,14 +18284,6 @@ "buffer-equal": "0.0.1", "minimist": "^1.1.3", "through2": "^2.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "raf": { @@ -18447,6 +18467,15 @@ "prop-types": "^15.5.8" } }, + "react-spring": { + "version": "8.0.20", + "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.20.tgz", + "integrity": "sha512-40ZUQ5uI5YHsoQWLPchWNcEUh6zQ6qvcVDeTI2vW10ldoCN3PvDsII9wBH2xEbMl+BQvYmHzGdfLTQxPxJWGnQ==", + "requires": { + "@babel/runtime": "^7.3.1", + "prop-types": "^15.5.8" + } + }, "react-test-renderer": { "version": "16.8.4", "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-16.8.4.tgz", @@ -19412,12 +19441,6 @@ "pump": "^3.0.0" } }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -20532,14 +20555,6 @@ "duplexer": "^0.1.1", "minimist": "^1.2.0", "through": "^2.3.4" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - } } }, "style-search": { @@ -21164,6 +21179,15 @@ "uuid": "^3.0.1" }, "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", @@ -22821,9 +22845,9 @@ "dev": true }, "worker-farm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.6.0.tgz", - "integrity": "sha512-6w+3tHbM87WnSWnENBUvA2pxJPLhQUg5LKwUQHq3r+XPhIM+Gh2R5ycbwPCyuGbNg+lPgdcnQUhuC02kJCvffQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", + "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", "dev": true, "requires": { "errno": "~0.1.7" @@ -22902,6 +22926,15 @@ "write-file-atomic": "^2.0.0" }, "dependencies": { + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "requires": { + "pify": "^3.0.0" + } + }, "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", diff --git a/package.json b/package.json index 527d358b982b37..c74000d2a1f86d 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,19 @@ { "name": "gutenberg", - "version": "5.8.0", + "version": "5.9.2", "private": true, - "description": "A new WordPress editor experience", - "repository": "git+https://github.com/WordPress/gutenberg.git", + "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", "keywords": [ "WordPress", "editor" ], + "homepage": "https://github.com/WordPress/gutenberg/", + "repository": "git+https://github.com/WordPress/gutenberg.git", + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, "config": { "GUTENBERG_PHASE": 2 }, @@ -45,12 +49,14 @@ "@wordpress/is-shallow-equal": "file:packages/is-shallow-equal", "@wordpress/keycodes": "file:packages/keycodes", "@wordpress/list-reusable-blocks": "file:packages/list-reusable-blocks", + "@wordpress/media-utils": "file:packages/media-utils", "@wordpress/notices": "file:packages/notices", "@wordpress/nux": "file:packages/nux", "@wordpress/plugins": "file:packages/plugins", "@wordpress/priority-queue": "file:packages/priority-queue", "@wordpress/redux-routine": "file:packages/redux-routine", "@wordpress/rich-text": "file:packages/rich-text", + "@wordpress/server-side-render": "file:packages/server-side-render", "@wordpress/shortcode": "file:packages/shortcode", "@wordpress/token-list": "file:packages/token-list", "@wordpress/url": "file:packages/url", @@ -82,7 +88,7 @@ "@wordpress/scripts": "file:packages/scripts", "babel-plugin-inline-json-import": "0.3.2", "benchmark": "2.1.4", - "browserslist": "4.4.1", + "browserslist": "4.6.2", "chalk": "2.4.1", "commander": "2.20.0", "concurrently": "3.5.0", @@ -90,12 +96,12 @@ "core-js": "3.0.1", "cross-env": "3.2.4", "cssnano": "4.1.10", - "deasync": "0.1.14", "deep-freeze": "0.0.1", "doctrine": "2.1.0", "enzyme": "3.9.0", "eslint-plugin-jest": "21.5.0", "espree": "4.0.0", + "fast-glob": "2.2.7", "fbjs": "0.8.17", "fs-extra": "8.0.1", "glob": "7.1.2", @@ -106,6 +112,7 @@ "lerna": "3.14.1", "lint-staged": "8.1.5", "lodash": "4.17.11", + "make-dir": "3.0.0", "mkdirp": "0.5.1", "node-sass": "4.12.0", "node-watch": "0.6.0", @@ -113,6 +120,7 @@ "pegjs": "0.10.0", "phpegjs": "1.0.0-beta7", "postcss": "7.0.13", + "progress": "2.0.3", "react": "16.8.4", "react-dom": "16.8.4", "react-test-renderer": "16.8.4", @@ -125,10 +133,12 @@ "shallow-equals": "1.0.0", "shallowequal": "1.1.0", "simple-git": "1.113.0", + "source-map-loader": "0.2.4", "sprintf-js": "1.1.1", "stylelint-config-wordpress": "13.1.0", "uuid": "3.3.2", - "webpack": "4.8.3" + "webpack": "4.8.3", + "worker-farm": "1.7.0" }, "npmPackageJsonLintConfig": { "extends": "@wordpress/npm-package-json-lint-config", @@ -177,7 +187,7 @@ "fixtures:generate": "npm run fixtures:server-registered && cross-env GENERATE_MISSING_FIXTURES=y npm run test-unit", "fixtures:regenerate": "npm run fixtures:clean && npm run fixtures:generate", "lint": "concurrently \"npm run lint-js\" \"npm run lint-pkg-json\" \"npm run lint-css\"", - "lint-js": "wp-scripts lint-js .", + "lint-js": "wp-scripts lint-js", "lint-js:fix": "npm run lint-js -- --fix", "lint-php": "docker-compose run --rm composer run-script lint", "lint-pkg-json": "wp-scripts lint-pkg-json ./packages", diff --git a/packages/a11y/package.json b/packages/a11y/package.json index fc3561a2bef4b8..1621eb97643289 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "2.3.0", + "version": "2.4.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 4f0c9165ff4edc..16bec17d283ed8 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "1.3.0", + "version": "1.4.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 1c647d817bbf02..0221ac0a1e8a37 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "3.2.0", + "version": "3.3.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 22c29f779e529e..e0ce0c0f2a3cd1 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "4.2.0", + "version": "4.3.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index ac9a9f9f9ebafe..15e4a9de6b0a80 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.2.0 (2019-06-12) + +### Internal + +- Refactored `BlockSettingsMenu` to use `DropdownMenu` from `@wordpress/components`. + ## 2.0.0 (2019-04-16) ### New Features diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 072bec27f51b9e..ff1a8579a0b016 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -208,7 +208,7 @@ _Parameters_ _Returns_ -- `string`: String with the class corresponding to the color in the provided context. +- `?string`: String with the class corresponding to the color in the provided context. Returns undefined if either colorContextName or colorSlug are not provided. # **getColorObjectByAttributeValues** @@ -223,7 +223,7 @@ _Parameters_ _Returns_ -- `?string`: If definedColor is passed and the name is found in colors, the color object exactly as set by the theme or editor defaults is returned. Otherwise, an object that just sets the color is defined. +- `?Object`: If definedColor is passed and the name is found in colors, the color object exactly as set by the theme or editor defaults is returned. Otherwise, an object that just sets the color is defined. # **getColorObjectByColorValue** @@ -236,7 +236,7 @@ _Parameters_ _Returns_ -- `?string`: Returns the color object included in the colors array whose color property equals colorValue. Returns undefined if no color object matches this requirement. +- `?Object`: Color object included in the colors array whose color property equals colorValue. Returns undefined if no color object matches this requirement. # **getFontSize** @@ -375,6 +375,18 @@ The default editor settings Undocumented declaration. +# **storeConfig** + +Block editor data store configuration. + +_Related_ + +- + +_Type_ + +- `Object` + # **URLInput** _Related_ diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index d8b9d21b64dd4e..da701fc0c08669 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "2.1.0", + "version": "2.2.0", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index 3b3f8032432e8b..c10d03a5c060f0 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -8,13 +8,15 @@ import { castArray, first, last, every } from 'lodash'; */ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; -import { cloneBlock, hasBlockSupport } from '@wordpress/blocks'; +import { cloneBlock, hasBlockSupport, switchToBlockType } from '@wordpress/blocks'; function BlockActions( { onDuplicate, onRemove, onInsertBefore, onInsertAfter, + onGroup, + onUngroup, isLocked, canDuplicate, children, @@ -24,6 +26,8 @@ function BlockActions( { onRemove, onInsertAfter, onInsertBefore, + onGroup, + onUngroup, isLocked, canDuplicate, } ); @@ -65,6 +69,7 @@ export default compose( [ multiSelect, removeBlocks, insertDefaultBlock, + replaceBlocks, } = dispatch( 'core/block-editor' ); return { @@ -107,6 +112,39 @@ export default compose( [ insertDefaultBlock( {}, rootClientId, lastSelectedIndex + 1 ); } }, + onGroup() { + if ( ! blocks.length ) { + return; + } + + // Activate the `transform` on `core/group` which does the conversion + const newBlocks = switchToBlockType( blocks, 'core/group' ); + + if ( ! newBlocks ) { + return; + } + replaceBlocks( + clientIds, + newBlocks + ); + }, + + onUngroup() { + if ( ! blocks.length ) { + return; + } + + const innerBlocks = blocks[ 0 ].innerBlocks; + + if ( ! innerBlocks.length ) { + return; + } + + replaceBlocks( + clientIds, + innerBlocks + ); + }, }; } ), ] )( BlockActions ); diff --git a/packages/block-editor/src/components/block-list-appender/style.scss b/packages/block-editor/src/components/block-list-appender/style.scss index e1725aa00873a6..5f83d4b44b290b 100644 --- a/packages/block-editor/src/components/block-list-appender/style.scss +++ b/packages/block-editor/src/components/block-list-appender/style.scss @@ -1,4 +1,6 @@ -.block-list-appender { +// These styles are only applied to the appender when it appears inside of a block. +// Otherwise the default appender may be improperly positioned in some themes. +.block-editor-block-list__block .block-list-appender { margin: $block-padding; // Add additional margin to the appender when inside a group with a background color. diff --git a/packages/block-editor/src/components/block-list/block-async-mode-provider.js b/packages/block-editor/src/components/block-list/block-async-mode-provider.js new file mode 100644 index 00000000000000..aaa2e709db92c6 --- /dev/null +++ b/packages/block-editor/src/components/block-list/block-async-mode-provider.js @@ -0,0 +1,23 @@ +/** + * WordPress dependencies + */ +import { + __experimentalAsyncModeProvider as AsyncModeProvider, + useSelect, +} from '@wordpress/data'; + +const BlockAsyncModeProvider = ( { children, clientId, isBlockInSelection } ) => { + const isParentOfSelectedBlock = useSelect( ( select ) => { + return select( 'core/block-editor' ).hasSelectedInnerBlock( clientId, true ); + } ); + + const isSyncModeForced = isBlockInSelection || isParentOfSelectedBlock; + + return ( + + { children } + + ); +}; + +export default BlockAsyncModeProvider; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index a9b28f618daf5f..aac68e2e6faa88 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -23,7 +23,10 @@ import { } from '@wordpress/blocks'; import { KeyboardShortcuts, withFilters } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; -import { withDispatch, withSelect } from '@wordpress/data'; +import { + withDispatch, + withSelect, +} from '@wordpress/data'; import { withViewportMatch } from '@wordpress/viewport'; import { compose, pure } from '@wordpress/compose'; @@ -45,7 +48,7 @@ import BlockInsertionPoint from './insertion-point'; import IgnoreNestedEvents from '../ignore-nested-events'; import InserterWithShortcuts from '../inserter-with-shortcuts'; import Inserter from '../inserter'; -import HoverArea from './hover-area'; +import useHoveredArea from './hover-area'; import { isInsideRootBlock } from '../../utils/dom'; /** @@ -76,9 +79,11 @@ function BlockListBlock( { isParentOfSelectedBlock, isDraggable, isSelectionEnabled, + isRTL, className, name, isValid, + isLast, attributes, initialPosition, wrapperProps, @@ -101,13 +106,14 @@ function BlockListBlock( { const wrapper = useRef( null ); useEffect( () => { blockRef( wrapper.current, clientId ); - // We need to rerender to trigger a rerendering of HoverArea. - rerender(); }, [] ); // Reference to the block edit node const blockNodeRef = useRef(); + // Hovered area of the block + const hoverArea = useHoveredArea( wrapper ); + // Keep track of touchstart to disable hover on iOS const hadTouchStart = useRef( false ); const onTouchStart = () => { @@ -329,240 +335,236 @@ function BlockListBlock( { } }; + // Rendering the output + const isHovered = isBlockHovered && ! isPartOfMultiSelection; + const blockType = getBlockType( name ); + // translators: %s: Type of block (i.e. Text, Image etc) + const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); + // The block as rendered in the editor is composed of general block UI + // (mover, toolbar, wrapper) and the display of the block content. + + const isUnregisteredBlock = name === getUnregisteredTypeHandlerName(); + + // If the block is selected and we're typing the block should not appear. + // Empty paragraph blocks should always show up as unselected. + const showInserterShortcuts = ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; + const showEmptyBlockSideInserter = ( isSelected || isHovered || isLast ) && isEmptyDefaultBlock && isValid; + const shouldAppearSelected = + ! isFocusMode && + ! showEmptyBlockSideInserter && + isSelected && + ! isTypingWithinBlock; + const shouldAppearHovered = + ! isFocusMode && + ! hasFixedToolbar && + isHovered && + ! isEmptyDefaultBlock; + // We render block movers and block settings to keep them tabbale even if hidden + const shouldRenderMovers = + ( isSelected || hoverArea === ( isRTL ? 'right' : 'left' ) ) && + ! showEmptyBlockSideInserter && + ! isPartOfMultiSelection && + ! isTypingWithinBlock; + const shouldShowBreadcrumb = + ! isFocusMode && isHovered && ! isEmptyDefaultBlock; + const shouldShowContextualToolbar = + ! hasFixedToolbar && + ! showEmptyBlockSideInserter && + ( + ( isSelected && ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || + isFirstMultiSelected + ); + const shouldShowMobileToolbar = shouldAppearSelected; + + // Insertion point can only be made visible if the block is at the + // the extent of a multi-selection, or not in a multi-selection. + const shouldShowInsertionPoint = + ( isPartOfMultiSelection && isFirstMultiSelected ) || + ! isPartOfMultiSelection; + + // The wp-block className is important for editor styles. + // Generate the wrapper class names handling the different states of the block. + const wrapperClassName = classnames( + 'wp-block editor-block-list__block block-editor-block-list__block', + { + 'has-warning': ! isValid || !! hasError || isUnregisteredBlock, + 'is-selected': shouldAppearSelected, + 'is-multi-selected': isPartOfMultiSelection, + 'is-hovered': shouldAppearHovered, + 'is-reusable': isReusableBlock( blockType ), + 'is-dragging': isDragging, + 'is-typing': isTypingWithinBlock, + 'is-focused': isFocusMode && ( isSelected || isParentOfSelectedBlock ), + 'is-focus-mode': isFocusMode, + }, + className + ); + + // Determine whether the block has props to apply to the wrapper. + let blockWrapperProps = wrapperProps; + if ( blockType.getEditWrapperProps ) { + blockWrapperProps = { + ...blockWrapperProps, + ...blockType.getEditWrapperProps( attributes ), + }; + } + const blockElementId = `block-${ clientId }`; + + // We wrap the BlockEdit component in a div that hides it when editing in + // HTML mode. This allows us to render all of the ancillary pieces + // (InspectorControls, etc.) which are inside `BlockEdit` but not + // `BlockHTML`, even in HTML mode. + let blockEdit = ( + + ); + if ( mode !== 'visual' ) { + blockEdit =
{ blockEdit }
; + } + + // Disable reasons: + // + // jsx-a11y/mouse-events-have-key-events: + // - onMouseOver is explicitly handling hover effects + // + // jsx-a11y/no-static-element-interactions: + // - Each block can be selected by clicking on it + + /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ + return ( - - { ( { hoverArea } ) => { - const isHovered = isBlockHovered && ! isPartOfMultiSelection; - const blockType = getBlockType( name ); - // translators: %s: Type of block (i.e. Text, Image etc) - const blockLabel = sprintf( __( 'Block: %s' ), blockType.title ); - // The block as rendered in the editor is composed of general block UI - // (mover, toolbar, wrapper) and the display of the block content. - - const isUnregisteredBlock = name === getUnregisteredTypeHandlerName(); - - // If the block is selected and we're typing the block should not appear. - // Empty paragraph blocks should always show up as unselected. - const showEmptyBlockSideInserter = - ( isSelected || isHovered ) && isEmptyDefaultBlock && isValid; - const shouldAppearSelected = - ! isFocusMode && - ! showEmptyBlockSideInserter && + + { shouldShowInsertionPoint && ( + + ) } + + { isFirstMultiSelected && ( + + ) } +
+ { shouldRenderMovers && ( + + ) } + { shouldShowBreadcrumb && ( + + ) } + { ( shouldShowContextualToolbar || isForcingContextualToolbar.current ) && ( + + ) } + { + ! shouldShowContextualToolbar && isSelected && - ! isTypingWithinBlock; - const shouldAppearHovered = - ! isFocusMode && - ! hasFixedToolbar && - isHovered && - ! isEmptyDefaultBlock; - // We render block movers and block settings to keep them tabbale even if hidden - const shouldRenderMovers = - ( isSelected || hoverArea === 'left' ) && - ! showEmptyBlockSideInserter && - ! isPartOfMultiSelection && - ! isTypingWithinBlock; - const shouldShowBreadcrumb = - ! isFocusMode && isHovered && ! isEmptyDefaultBlock; - const shouldShowContextualToolbar = ! hasFixedToolbar && - ! showEmptyBlockSideInserter && - ( ( isSelected && - ( ! isTypingWithinBlock || isCaretWithinFormattedText ) ) || - isFirstMultiSelected ); - const shouldShowMobileToolbar = shouldAppearSelected; - - // Insertion point can only be made visible if the block is at the - // the extent of a multi-selection, or not in a multi-selection. - const shouldShowInsertionPoint = - ( isPartOfMultiSelection && isFirstMultiSelected ) || - ! isPartOfMultiSelection; - - // The wp-block className is important for editor styles. - // Generate the wrapper class names handling the different states of the block. - const wrapperClassName = classnames( - 'wp-block editor-block-list__block block-editor-block-list__block', - { - 'has-warning': ! isValid || !! hasError || isUnregisteredBlock, - 'is-selected': shouldAppearSelected, - 'is-multi-selected': isPartOfMultiSelection, - 'is-hovered': shouldAppearHovered, - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': isDragging, - 'is-typing': isTypingWithinBlock, - 'is-focused': - isFocusMode && ( isSelected || isParentOfSelectedBlock ), - 'is-focus-mode': isFocusMode, - }, - className - ); - - // Determine whether the block has props to apply to the wrapper. - let blockWrapperProps = wrapperProps; - if ( blockType.getEditWrapperProps ) { - blockWrapperProps = { - ...blockWrapperProps, - ...blockType.getEditWrapperProps( attributes ), - }; + ! isEmptyDefaultBlock && ( + + ) } - const blockElementId = `block-${ clientId }`; - - // We wrap the BlockEdit component in a div that hides it when editing in - // HTML mode. This allows us to render all of the ancillary pieces - // (InspectorControls, etc.) which are inside `BlockEdit` but not - // `BlockHTML`, even in HTML mode. - let blockEdit = ( - + + { isValid && blockEdit } + { isValid && mode === 'html' && ( + + ) } + { ! isValid && [ + , +
+ { getSaveElement( blockType, attributes ) } +
, + ] } +
+ { shouldShowMobileToolbar && ( + + ) } + { !! hasError && } + +
+ { showInserterShortcuts && ( +
+ - ); - if ( mode !== 'visual' ) { - blockEdit =
{ blockEdit }
; - } - - // Disable reasons: - // - // jsx-a11y/mouse-events-have-key-events: - // - onMouseOver is explicitly handling hover effects - // - // jsx-a11y/no-static-element-interactions: - // - Each block can be selected by clicking on it - - /* eslint-disable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ - - return ( - - { shouldShowInsertionPoint && ( - - ) } - - { isFirstMultiSelected && ( - - ) } -
- { shouldRenderMovers && ( - - ) } - { shouldShowBreadcrumb && ( - - ) } - { ( shouldShowContextualToolbar || - isForcingContextualToolbar.current ) && ( - - ) } - { ! shouldShowContextualToolbar && - isSelected && - ! hasFixedToolbar && - ! isEmptyDefaultBlock && ( - - ) } - - - { isValid && blockEdit } - { isValid && mode === 'html' && ( - - ) } - { ! isValid && [ - , -
- { getSaveElement( blockType, attributes ) } -
, - ] } -
- { shouldShowMobileToolbar && ( - - ) } - { !! hasError && } -
-
- { showEmptyBlockSideInserter && ( - <> -
- -
-
- -
- - ) } -
- ); - /* eslint-enable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ - } } - +
+ ) } + { showEmptyBlockSideInserter && ( +
+ +
+ ) } +
); + /* eslint-enable jsx-a11y/mouse-events-have-key-events, jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ } const applyWithSelect = withSelect( @@ -580,13 +582,17 @@ const applyWithSelect = withSelect( getSettings, hasSelectedInnerBlock, getTemplateLock, + getBlockIndex, + getBlockOrder, __unstableGetBlockWithoutInnerBlocks, } = select( 'core/block-editor' ); const block = __unstableGetBlockWithoutInnerBlocks( clientId ); const isSelected = isBlockSelected( clientId ); - const { hasFixedToolbar, focusMode } = getSettings(); + const { hasFixedToolbar, focusMode, isRTL } = getSettings(); const templateLock = getTemplateLock( rootClientId ); const isParentOfSelectedBlock = hasSelectedInnerBlock( clientId, true ); + const index = getBlockIndex( clientId, rootClientId ); + const blockOrder = getBlockOrder( rootClientId ); // The fallback to `{}` is a temporary fix. // This function should never be called when a block is not present in the state. @@ -611,6 +617,8 @@ const applyWithSelect = withSelect( isLocked: !! templateLock, isFocusMode: focusMode && isLargeViewport, hasFixedToolbar: hasFixedToolbar && isLargeViewport, + isLast: index === blockOrder.length - 1, + isRTL, // Users of the editor.BlockListBlock filter used to be able to access the block prop // Ideally these blocks would rely on the clientId prop only. @@ -637,7 +645,6 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, { select } ) => { mergeBlocks, replaceBlocks, toggleSelection, - } = dispatch( 'core/block-editor' ); return { diff --git a/packages/block-editor/src/components/block-list/hover-area.js b/packages/block-editor/src/components/block-list/hover-area.js index a79b0bcd9b088b..36664e3c9d797c 100644 --- a/packages/block-editor/src/components/block-list/hover-area.js +++ b/packages/block-editor/src/components/block-list/hover-area.js @@ -1,82 +1,41 @@ /** * WordPress dependencies */ -import { Component } from '@wordpress/element'; -import { withSelect } from '@wordpress/data'; +import { useState, useEffect } from '@wordpress/element'; -class HoverArea extends Component { - constructor() { - super( ...arguments ); - this.state = { - hoverArea: null, - }; - this.onMouseLeave = this.onMouseLeave.bind( this ); - this.onMouseMove = this.onMouseMove.bind( this ); - } - - componentWillUnmount() { - if ( this.props.container ) { - this.toggleListeners( this.props.container, false ); - } - } - - componentDidMount() { - if ( this.props.container ) { - this.toggleListeners( this.props.container ); - } - } - - componentDidUpdate( prevProps ) { - if ( prevProps.container === this.props.container ) { - return; - } - if ( prevProps.container ) { - this.toggleListeners( prevProps.container, false ); - } - if ( this.props.container ) { - this.toggleListeners( this.props.container, true ); - } - } +const useHoveredArea = ( wrapper ) => { + const [ hoveredArea, setHoveredArea ] = useState( null ); - toggleListeners( container, shouldListnerToEvents = true ) { - const method = shouldListnerToEvents ? 'addEventListener' : 'removeEventListener'; - container[ method ]( 'mousemove', this.onMouseMove ); - container[ method ]( 'mouseleave', this.onMouseLeave ); - } - - onMouseLeave() { - if ( this.state.hoverArea ) { - this.setState( { hoverArea: null } ); - } - } + useEffect( () => { + const onMouseLeave = () => { + if ( hoveredArea ) { + setHoveredArea( null ); + } + }; - onMouseMove( event ) { - const { isRTL, container } = this.props; - const { width, left, right } = container.getBoundingClientRect(); + const onMouseMove = ( event ) => { + const { width, left, right } = wrapper.current.getBoundingClientRect(); - let hoverArea = null; - if ( ( event.clientX - left ) < width / 3 ) { - hoverArea = isRTL ? 'right' : 'left'; - } else if ( ( right - event.clientX ) < width / 3 ) { - hoverArea = isRTL ? 'left' : 'right'; - } + let newHoveredArea = null; + if ( ( event.clientX - left ) < width / 3 ) { + newHoveredArea = 'left'; + } else if ( ( right - event.clientX ) < width / 3 ) { + newHoveredArea = 'right'; + } - if ( hoverArea !== this.state.hoverArea ) { - this.setState( { hoverArea } ); - } - } + setHoveredArea( newHoveredArea ); + }; - render() { - const { hoverArea } = this.state; - const { children } = this.props; + wrapper.current.addEventListener( 'mousemove', onMouseMove ); + wrapper.current.addEventListener( 'mouseleave', onMouseLeave ); - return children( { hoverArea } ); - } -} + return () => { + wrapper.current.removeEventListener( 'mousemove', onMouseMove ); + wrapper.current.removeEventListener( 'mouseleave', onMouseLeave ); + }; + }, [] ); -export default withSelect( ( select ) => { - return { - isRTL: select( 'core/block-editor' ).getSettings().isRTL, - }; -} )( HoverArea ); + return hoveredArea; +}; +export default useHoveredArea; diff --git a/packages/block-editor/src/components/block-list/index.js b/packages/block-editor/src/components/block-list/index.js index 2da9071693d5fa..79cc1b29273375 100644 --- a/packages/block-editor/src/components/block-list/index.js +++ b/packages/block-editor/src/components/block-list/index.js @@ -24,6 +24,7 @@ import { compose } from '@wordpress/compose'; /** * Internal dependencies */ +import BlockAsyncModeProvider from './block-async-mode-provider'; import BlockListBlock from './block'; import BlockListAppender from '../block-list-appender'; import { getBlockDOMNode } from '../../utils/dom'; @@ -207,9 +208,10 @@ class BlockList extends Component { selectedBlockClientId === clientId; return ( - - + ); } ) } diff --git a/packages/block-editor/src/components/block-list/style.scss b/packages/block-editor/src/components/block-list/style.scss index 216fac5253cf3a..8ee3558ddf6398 100644 --- a/packages/block-editor/src/components/block-list/style.scss +++ b/packages/block-editor/src/components/block-list/style.scss @@ -109,8 +109,9 @@ border: $border-width solid transparent; border-left: none; box-shadow: none; - transition: border-color 0.1s linear, box-shadow 0.1s linear; pointer-events: none; + transition: border-color 0.1s linear, box-shadow 0.1s linear; + @include reduce-motion("transition"); // Include a transparent outline for Windows High Contrast mode. outline: $border-width solid transparent; @@ -160,6 +161,7 @@ &.is-focus-mode:not(.is-multi-selected) { opacity: 0.5; transition: opacity 0.1s linear; + @include reduce-motion("transition"); &:not(.is-focused) .block-editor-block-list__block, &.is-focused { @@ -272,13 +274,11 @@ } // Appender - &.is-typing .block-editor-block-list__empty-block-inserter, &.is-typing .block-editor-block-list__side-inserter { opacity: 0; animation: none; } - .block-editor-block-list__empty-block-inserter, .block-editor-block-list__side-inserter { @include edit-post__fade-in-animation; } @@ -303,6 +303,20 @@ } } + // Reusable Blocks clickthrough overlays + &.is-reusable > .block-editor-block-list__block-edit .block-editor-inner-blocks.has-overlay { + // Remove only the top click overlay. + &::after { + display: none; + } + + // Restore it for subsequent. + .block-editor-inner-blocks.has-overlay::after { + display: block; + } + } + + // Alignments &[data-align="left"], &[data-align="right"] { @@ -752,6 +766,7 @@ // Hide both the button until hovered. opacity: 0; transition: opacity 0.1s linear; + @include reduce-motion("transition"); &:hover, &.is-visible { @@ -815,6 +830,7 @@ font-size: $text-editor-font-size; line-height: 150%; transition: padding 0.2s linear; + @include reduce-motion("transition"); &:focus { box-shadow: none; diff --git a/packages/block-editor/src/components/block-settings-menu/index.js b/packages/block-editor/src/components/block-settings-menu/index.js index 21899126bae2fa..005be38e380b43 100644 --- a/packages/block-editor/src/components/block-settings-menu/index.js +++ b/packages/block-editor/src/components/block-settings-menu/index.js @@ -1,15 +1,18 @@ /** * External dependencies */ -import classnames from 'classnames'; import { castArray, flow } from 'lodash'; /** * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Toolbar, Dropdown, NavigableMenu, MenuItem } from '@wordpress/components'; -import { withDispatch } from '@wordpress/data'; +import { + Toolbar, + DropdownMenu, + MenuGroup, + MenuItem, +} from '@wordpress/components'; /** * Internal dependencies @@ -22,7 +25,7 @@ import BlockUnknownConvertButton from './block-unknown-convert-button'; import __experimentalBlockSettingsMenuFirstItem from './block-settings-menu-first-item'; import __experimentalBlockSettingsMenuPluginsExtension from './block-settings-menu-plugins-extension'; -export function BlockSettingsMenu( { clientIds, onSelect } ) { +export function BlockSettingsMenu( { clientIds } ) { const blockClientIds = castArray( clientIds ); const count = blockClientIds.length; const firstBlockClientId = blockClientIds[ 0 ]; @@ -30,105 +33,91 @@ export function BlockSettingsMenu( { clientIds, onSelect } ) { return ( { ( { onDuplicate, onRemove, onInsertAfter, onInsertBefore, canDuplicate, isLocked } ) => ( - { - const toggleClassname = classnames( 'editor-block-settings-menu__toggle block-editor-block-settings-menu__toggle', { - 'is-opened': isOpen, - } ); - const label = isOpen ? __( 'Hide options' ) : __( 'More options' ); - - return ( - { - if ( count === 1 ) { - onSelect( firstBlockClientId ); - } - onToggle(); - }, - className: toggleClassname, - extraProps: { 'aria-expanded': isOpen }, - } ] } /> - ); - } } - renderContent={ ( { onClose } ) => ( - - <__experimentalBlockSettingsMenuFirstItem.Slot fillProps={ { onClose } } /> - { count === 1 && ( - - ) } - { count === 1 && ( - - ) } - { ! isLocked && canDuplicate && ( - - { __( 'Duplicate' ) } - - ) } - { ! isLocked && ( - <> - - { __( 'Insert Before' ) } - - - { __( 'Insert After' ) } - - - ) } - { count === 1 && ( - - ) } - <__experimentalBlockSettingsMenuPluginsExtension.Slot fillProps={ { clientIds, onClose } } /> -
- { ! isLocked && ( - - { __( 'Remove Block' ) } - - ) } - - ) } - /> + + + { ( { onClose } ) => ( + <> + + <__experimentalBlockSettingsMenuFirstItem.Slot + fillProps={ { onClose } } + /> + { count === 1 && ( + + ) } + { count === 1 && ( + + ) } + { ! isLocked && canDuplicate && ( + + { __( 'Duplicate' ) } + + ) } + { ! isLocked && ( + <> + + { __( 'Insert Before' ) } + + + { __( 'Insert After' ) } + + + ) } + { count === 1 && ( + + ) } + <__experimentalBlockSettingsMenuPluginsExtension.Slot + fillProps={ { clientIds, onClose } } + /> + + + { ! isLocked && ( + + { __( 'Remove Block' ) } + + ) } + + + ) } + + ) } ); } -export default withDispatch( ( dispatch ) => { - const { selectBlock } = dispatch( 'core/block-editor' ); - - return { - onSelect( clientId ) { - selectBlock( clientId ); - }, - }; -} )( BlockSettingsMenu ); +export default BlockSettingsMenu; diff --git a/packages/block-editor/src/components/block-settings-menu/style.scss b/packages/block-editor/src/components/block-settings-menu/style.scss index 39b62ec34b59de..0b690745012edd 100644 --- a/packages/block-editor/src/components/block-settings-menu/style.scss +++ b/packages/block-editor/src/components/block-settings-menu/style.scss @@ -1,59 +1,7 @@ -.block-editor-block-settings-menu__toggle .dashicon { - transform: rotate(90deg); +.block-editor-block-settings-menu__content { + padding: 0; } -// Popout menu -.block-editor-block-settings-menu__popover { - &::before, - &::after { - margin-left: 2px; - } - - .block-editor-block-settings-menu__content { - padding: ($grid-size - $border-width) 0; - } - - .block-editor-block-settings-menu__separator { - margin-top: $grid-size; - margin-bottom: $grid-size; - margin-left: 0; - margin-right: 0; - border-top: $border-width solid $light-gray-500; - - // Check if the separator is the last child in the node and if so, hide itself - &:last-child { - display: none; - } - } - - .block-editor-block-settings-menu__title { - display: block; - padding: 6px; - color: $dark-gray-300; - } - - // Menu items - .block-editor-block-settings-menu__control { - width: 100%; - justify-content: flex-start; - background: none; - outline: none; - border-radius: 0; - color: $dark-gray-500; - text-align: left; - cursor: pointer; - @include menu-style__neutral; - - &:hover:not(:disabled):not([aria-disabled="true"]) { - @include menu-style__hover; - } - - &:focus:not(:disabled):not([aria-disabled="true"]) { - @include menu-style__focus; - } - - .dashicon { - margin-right: 5px; - } - } +.block-editor-block-settings-menu__toggle .dashicon { + transform: rotate(90deg); } diff --git a/packages/block-editor/src/components/block-switcher/style.scss b/packages/block-editor/src/components/block-switcher/style.scss index 9431b74fe1f31f..88b0e984fb7319 100644 --- a/packages/block-editor/src/components/block-switcher/style.scss +++ b/packages/block-editor/src/components/block-switcher/style.scss @@ -59,6 +59,7 @@ display: flex; align-items: center; transition: all 0.1s cubic-bezier(0.165, 0.84, 0.44, 1); + @include reduce-motion("transition"); } // Add a dropdown arrow indicator. diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index e0fbc6601ee369..3d741214e98799 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -4,8 +4,9 @@ width: 100%; overflow: auto; // Allow horizontal scrolling on mobile. position: relative; - transition: border-color 0.1s linear, box-shadow 0.1s linear; border-left: $border-width solid $light-gray-800; + transition: border-color 0.1s linear, box-shadow 0.1s linear; + @include reduce-motion("transition"); @include break-small() { // Allow overflow on desktop. diff --git a/packages/block-editor/src/components/colors/test/utils.js b/packages/block-editor/src/components/colors/test/utils.js new file mode 100644 index 00000000000000..8eb370394aae83 --- /dev/null +++ b/packages/block-editor/src/components/colors/test/utils.js @@ -0,0 +1,83 @@ +/** + * Internal dependencies + */ +import { + getColorObjectByAttributeValues, + getColorObjectByColorValue, + getColorClassName, +} from '../utils'; + +describe( 'color utils', () => { + describe( 'getColorObjectByAttributeValues', () => { + it( 'should return the custom color object when there is no definedColor', () => { + const colors = [ + { slug: 'red' }, + { slug: 'green' }, + { slug: 'blue' }, + ]; + const customColor = '#ffffff'; + + expect( getColorObjectByAttributeValues( colors, undefined, customColor ) ).toEqual( { color: customColor } ); + } ); + + it( 'should return the custom color object when definedColor was not found', () => { + const colors = [ + { slug: 'red' }, + { slug: 'green' }, + { slug: 'blue' }, + ]; + const definedColor = 'purple'; + const customColor = '#ffffff'; + + expect( getColorObjectByAttributeValues( colors, definedColor, customColor ) ).toEqual( { color: customColor } ); + } ); + + it( 'should return the found color object', () => { + const colors = [ + { slug: 'red' }, + { slug: 'green' }, + { slug: 'blue' }, + ]; + const definedColor = 'blue'; + const customColor = '#ffffff'; + + expect( getColorObjectByAttributeValues( colors, definedColor, customColor ) ).toEqual( { slug: 'blue' } ); + } ); + } ); + + describe( 'getColorObjectByColorValue', () => { + it( 'should return undefined if the given color was not found', () => { + const colors = [ + { slug: 'red', color: '#ff0000' }, + { slug: 'green', color: '#00ff00' }, + { slug: 'blue', color: '#0000ff' }, + ]; + + expect( getColorObjectByColorValue( colors, '#ffffff' ) ).toBeUndefined(); + } ); + + it( 'should return a color object for the given color value', () => { + const colors = [ + { slug: 'red', color: '#ff0000' }, + { slug: 'green', color: '#00ff00' }, + { slug: 'blue', color: '#0000ff' }, + ]; + + expect( getColorObjectByColorValue( colors, '#00ff00' ) ).toEqual( { slug: 'green', color: '#00ff00' } ); + } ); + } ); + + describe( 'getColorClassName', () => { + it( 'should return undefined if colorContextName is missing', () => { + expect( getColorClassName( undefined, 'Light Purple' ) ).toBeUndefined(); + } ); + + it( 'should return undefined if colorSlug is missing', () => { + expect( getColorClassName( 'background', undefined ) ).toBeUndefined(); + } ); + + it( 'should return a class name with the color slug in kebab case', () => { + expect( getColorClassName( 'background', 'Light Purple' ) ).toBe( 'has-light-purple-background' ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/colors/utils.js b/packages/block-editor/src/components/colors/utils.js index 29ab15f30e731b..67be0eed5a2517 100644 --- a/packages/block-editor/src/components/colors/utils.js +++ b/packages/block-editor/src/components/colors/utils.js @@ -12,7 +12,7 @@ import tinycolor from 'tinycolor2'; * @param {?string} definedColor A string containing the color slug. * @param {?string} customColor A string containing the customColor value. * - * @return {?string} If definedColor is passed and the name is found in colors, + * @return {?Object} If definedColor is passed and the name is found in colors, * the color object exactly as set by the theme or editor defaults is returned. * Otherwise, an object that just sets the color is defined. */ @@ -35,7 +35,7 @@ export const getColorObjectByAttributeValues = ( colors, definedColor, customCol * @param {Array} colors Array of color objects as set by the theme or by the editor defaults. * @param {?string} colorValue A string containing the color value. * -* @return {?string} Returns the color object included in the colors array whose color property equals colorValue. +* @return {?Object} Color object included in the colors array whose color property equals colorValue. * Returns undefined if no color object matches this requirement. */ export const getColorObjectByColorValue = ( colors, colorValue ) => { @@ -48,11 +48,12 @@ export const getColorObjectByColorValue = ( colors, colorValue ) => { * @param {string} colorContextName Context/place where color is being used e.g: background, text etc... * @param {string} colorSlug Slug of the color. * - * @return {string} String with the class corresponding to the color in the provided context. + * @return {?string} String with the class corresponding to the color in the provided context. + * Returns undefined if either colorContextName or colorSlug are not provided. */ export function getColorClassName( colorContextName, colorSlug ) { if ( ! colorContextName || ! colorSlug ) { - return; + return undefined; } return `has-${ kebabCase( colorSlug ) }-${ colorContextName }`; diff --git a/packages/block-editor/src/components/default-block-appender/style.scss b/packages/block-editor/src/components/default-block-appender/style.scss index c5cb3ac744571b..6f6c28da6aa8c8 100644 --- a/packages/block-editor/src/components/default-block-appender/style.scss +++ b/packages/block-editor/src/components/default-block-appender/style.scss @@ -12,6 +12,7 @@ width: 100%; outline: $border-width solid transparent; transition: 0.2s outline; + @include reduce-motion("transition"); resize: none; margin-top: $default-block-margin; margin-bottom: $default-block-margin; @@ -28,20 +29,10 @@ } } - // Don't show the inserter until mousing over. - .block-editor-inserter__toggle:not([aria-expanded="true"]) { - opacity: 0; - transition: opacity 0.2s; - } - &:hover { .block-editor-inserter-with-shortcuts { @include edit-post__fade-in-animation; } - - .block-editor-inserter__toggle { - opacity: 1; - } } // Dropzone. diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index df605eb54da3f2..5b3249e1f1a8a9 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -20,6 +20,7 @@ export { default as BlockInvalidWarning } from './block-list/block-invalid-warni // Content Related Components export { default as DefaultBlockAppender } from './default-block-appender'; +export { default as Inserter } from './inserter'; // State Related Components export { default as BlockEditorProvider } from './provider'; diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index 2a69e5305fc3fa..75a6731f42db57 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -7,7 +7,6 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { withViewportMatch } from '@wordpress/viewport'; import { Component } from '@wordpress/element'; import { withSelect, withDispatch } from '@wordpress/data'; import { synchronizeBlocksWithTemplate, withBlockContentContext } from '@wordpress/blocks'; @@ -106,14 +105,13 @@ class InnerBlocks extends Component { render() { const { clientId, - isSmallScreen, - isSelectedBlockInRoot, + hasOverlay, renderAppender, } = this.props; const { templateInProcess } = this.state; const classes = classnames( 'editor-inner-blocks block-editor-inner-blocks', { - 'has-overlay': isSmallScreen && ! isSelectedBlockInRoot, + 'has-overlay': hasOverlay, } ); return ( @@ -131,7 +129,6 @@ class InnerBlocks extends Component { InnerBlocks = compose( [ withBlockEditContext( ( context ) => pick( context, [ 'clientId' ] ) ), - withViewportMatch( { isSmallScreen: '< medium' } ), withSelect( ( select, ownProps ) => { const { isBlockSelected, @@ -142,12 +139,13 @@ InnerBlocks = compose( [ getTemplateLock, } = select( 'core/block-editor' ); const { clientId } = ownProps; + const block = getBlock( clientId ); const rootClientId = getBlockRootClientId( clientId ); return { - isSelectedBlockInRoot: isBlockSelected( clientId ) || hasSelectedInnerBlock( clientId ), - block: getBlock( clientId ), + block, blockListSettings: getBlockListSettings( clientId ), + hasOverlay: block.name !== 'core/template' && ! isBlockSelected( clientId ) && ! hasSelectedInnerBlock( clientId, true ), parentLock: getTemplateLock( rootClientId ), }; } ), diff --git a/packages/block-editor/src/components/inner-blocks/style.scss b/packages/block-editor/src/components/inner-blocks/style.scss index f4218ef0667ebe..ff8e4b9adbe96a 100644 --- a/packages/block-editor/src/components/inner-blocks/style.scss +++ b/packages/block-editor/src/components/inner-blocks/style.scss @@ -1,9 +1,19 @@ -.block-editor-inner-blocks.has-overlay::after { - content: ""; - position: absolute; - top: 0; +// Add clickable overlay to blocks with nesting. +// This makes it easy to select all layers of the block. +.block-editor-inner-blocks.has-overlay { + &::after { + content: ""; + position: absolute; + top: -$block-padding; + right: -$block-padding; + bottom: -$block-padding; + left: -$block-padding; + z-index: z-index(".block-editor-inner-blocks.has-overlay::after"); + } +} + +// On fullwide blocks, don't go beyond the canvas. +[data-align="full"] > .editor-block-list__block-edit > [data-block] .has-overlay::after { right: 0; - bottom: 0; left: 0; - z-index: z-index(".block-editor-inner-blocks__small-screen-overlay:after"); } diff --git a/packages/block-editor/src/components/inserter-list-item/style.scss b/packages/block-editor/src/components/inserter-list-item/style.scss index 073c815dc2909e..d9051da5c839aa 100644 --- a/packages/block-editor/src/components/inserter-list-item/style.scss +++ b/packages/block-editor/src/components/inserter-list-item/style.scss @@ -20,6 +20,7 @@ border-radius: $radius-round-rectangle; border: $border-width solid transparent; transition: all 0.05s ease-in-out; + @include reduce-motion("transition"); position: relative; &:disabled { @@ -71,6 +72,7 @@ border-radius: $radius-round-rectangle; color: $dark-gray-500; transition: all 0.05s ease-in-out; + @include reduce-motion("transition"); .block-editor-block-icon { margin-left: auto; @@ -79,6 +81,7 @@ svg { transition: all 0.15s ease-out; + @include reduce-motion("transition"); } } diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js new file mode 100644 index 00000000000000..2985d61c7811c4 --- /dev/null +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -0,0 +1,96 @@ +/** + * External dependencies + */ +import { FlatList, Text, TouchableHighlight, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { BottomSheet, Icon } from '@wordpress/components'; +import { Component } from '@wordpress/element'; +import { withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { getUnregisteredTypeHandlerName } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +class Inserter extends Component { + calculateNumberOfColumns() { + const bottomSheetWidth = BottomSheet.getWidth(); + const { paddingLeft: itemPaddingLeft, paddingRight: itemPaddingRight } = styles.modalItem; + const { paddingLeft: containerPaddingLeft, paddingRight: containerPaddingRight } = styles.content; + const { width: itemWidth } = styles.modalIconWrapper; + const itemTotalWidth = itemWidth + itemPaddingLeft + itemPaddingRight; + const containerTotalWidth = bottomSheetWidth - ( containerPaddingLeft + containerPaddingRight ); + return Math.floor( containerTotalWidth / itemTotalWidth ); + } + + render() { + const numberOfColumns = this.calculateNumberOfColumns(); + const bottomPadding = this.props.addExtraBottomPadding && styles.contentBottomPadding; + + return ( + + + + } + keyExtractor={ ( item ) => item.name } + renderItem={ ( { item } ) => + this.props.onValueSelected( item.name ) }> + + + + + + + { item.title } + + + } + /> + + ); + } +} + +export default compose( [ + withSelect( ( select, { clientId, isAppender, rootClientId } ) => { + const { + getInserterItems, + getBlockRootClientId, + getBlockSelectionEnd, + } = select( 'core/block-editor' ); + + let destinationRootClientId = rootClientId; + if ( ! destinationRootClientId && ! clientId && ! isAppender ) { + const end = getBlockSelectionEnd(); + if ( end ) { + destinationRootClientId = getBlockRootClientId( end ) || undefined; + } + } + const inserterItems = getInserterItems( destinationRootClientId ); + + return { + items: inserterItems.filter( ( { name } ) => name !== getUnregisteredTypeHandlerName() ), + }; + } ), +] )( Inserter ); diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss new file mode 100644 index 00000000000000..82d5fa58226504 --- /dev/null +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -0,0 +1,57 @@ +/** @format */ + +.touchableArea { + border-radius: 8px 8px 8px 8px; +} + +.content { + padding: 0 0 0 0; + align-items: center; + justify-content: space-evenly; +} + +.contentBottomPadding { + padding-bottom: 20px; +} + +.rowSeparator { + height: 12px; +} + +.modalItem { + flex-direction: column; + justify-content: center; + align-items: center; + padding-left: 8; + padding-right: 8; + padding-top: 0; + padding-bottom: 0; +} + +.modalIconWrapper { + width: 104px; + height: 64px; + background-color: $gray-light; //#f3f6f8 + border-radius: 8px 8px 8px 8px; + justify-content: center; + align-items: center; +} + +.modalIcon { + width: 32px; + height: 32px; + justify-content: center; + align-items: center; + fill: $gray-dark; +} + +.modalItemLabel { + background-color: transparent; + padding-left: 2; + padding-right: 2; + padding-top: 4; + padding-bottom: 0; + justify-content: center; + font-size: 12; + color: $gray-dark; +} diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 41f53d25e00e38..87b9a79f12fa40 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -32,6 +32,7 @@ $block-inserter-search-height: 38px; border: none; outline: none; transition: color 0.2s ease; + @include reduce-motion("transition"); } .block-editor-inserter__menu { diff --git a/packages/block-editor/src/components/inspector-controls/README.md b/packages/block-editor/src/components/inspector-controls/README.md index f8b19593fbf976..2c6a21d59dbaf5 100644 --- a/packages/block-editor/src/components/inspector-controls/README.md +++ b/packages/block-editor/src/components/inspector-controls/README.md @@ -1,6 +1,6 @@ # Inspector Controls -inspector +inspector Inspector Controls appear in the post settings sidebar when a block is being edited. The controls appear in both HTML and visual editing modes, and thus should contain settings that affect the entire block. diff --git a/packages/block-editor/src/components/media-placeholder/index.js b/packages/block-editor/src/components/media-placeholder/index.js index 07daa4d8f813eb..efad860f586e7a 100644 --- a/packages/block-editor/src/components/media-placeholder/index.js +++ b/packages/block-editor/src/components/media-placeholder/index.js @@ -175,14 +175,14 @@ export class MediaPlaceholder extends Component { const isVideo = isOneType && 'video' === allowedTypes[ 0 ]; if ( instructions === undefined && mediaUpload ) { - instructions = __( 'Drag a media file, upload a new one or select a file from your library.' ); + instructions = __( 'Upload a media file or pick one from your media library.' ); if ( isAudio ) { - instructions = __( 'Drag an audio, upload a new one or select a file from your library.' ); + instructions = __( 'Upload an audio file, pick one from your media library, or add one with a URL.' ); } else if ( isImage ) { - instructions = __( 'Drag an image, upload a new one or select a file from your library.' ); + instructions = __( 'Upload an image file, pick one from your media library, or add one with a URL.' ); } else if ( isVideo ) { - instructions = __( 'Drag a video, upload a new one or select a file from your library.' ); + instructions = __( 'Upload a video file, pick one from your media library, or add one with a URL.' ); } } diff --git a/packages/block-editor/src/components/media-upload/test/index.native.js b/packages/block-editor/src/components/media-upload/test/index.native.js new file mode 100644 index 00000000000000..72a2015277e4df --- /dev/null +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -0,0 +1,115 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import { TouchableWithoutFeedback } from 'react-native'; +import { + requestMediaPickFromMediaLibrary, + requestMediaPickFromDeviceLibrary, + requestMediaPickFromDeviceCamera, +} from 'react-native-gutenberg-bridge'; + +/** + * Internal dependencies + */ +import { + MediaUpload, + MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, + MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, + MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, + OPTION_TAKE_VIDEO, + OPTION_TAKE_PHOTO, +} from '../index'; + +jest.mock( 'react-native-gutenberg-bridge', () => ( + { + requestMediaPickFromMediaLibrary: jest.fn(), + requestMediaPickFromDeviceLibrary: jest.fn(), + requestMediaPickFromDeviceCamera: jest.fn(), + } +) ); + +const MEDIA_URL = 'http://host.media.type'; +const MEDIA_ID = 123; + +describe( 'MediaUpload component', () => { + it( 'renders without crashing', () => { + const wrapper = shallow( + {} } /> + ); + expect( wrapper ).toBeTruthy(); + } ); + + it( 'opens media options picker', () => { + const wrapper = shallow( + { + return ( + + { getMediaOptions() } + + ); + } } /> + ); + expect( wrapper.find( 'Picker' ) ).toHaveLength( 1 ); + } ); + + it( 'shows right media capture option for media type', () => { + const expectOptionForMediaType = ( mediaType, expectedOption ) => { + const wrapper = shallow( + { + return ( + + { getMediaOptions() } + + ); + } } /> + ); + expect( wrapper.find( 'Picker' ).props().options.filter( ( item ) => item.label === expectedOption ) ).toHaveLength( 1 ); + }; + expectOptionForMediaType( MEDIA_TYPE_IMAGE, OPTION_TAKE_PHOTO ); + expectOptionForMediaType( MEDIA_TYPE_VIDEO, OPTION_TAKE_VIDEO ); + } ); + + const expectMediaPickerForOption = ( option, requestFunction ) => { + requestFunction.mockImplementation( ( mediaTypes, callback ) => { + expect( mediaTypes[ 0 ] ).toEqual( MEDIA_TYPE_VIDEO ); + callback( MEDIA_ID, MEDIA_URL ); + } ); + + const onSelectURL = jest.fn(); + + const wrapper = shallow( + { + return ( + + { getMediaOptions() } + + ); + } } /> + ); + wrapper.find( 'Picker' ).simulate( 'change', option ); + expect( requestFunction ).toHaveBeenCalledTimes( 1 ); + + expect( onSelectURL ).toHaveBeenCalledTimes( 1 ); + expect( onSelectURL ).toHaveBeenCalledWith( MEDIA_ID, MEDIA_URL ); + }; + + it( 'can select media from device library', () => { + expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE, requestMediaPickFromDeviceLibrary ); + } ); + + it( 'can select media from WP media library', () => { + expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY, requestMediaPickFromMediaLibrary ); + } ); + + it( 'can select media by capturig', () => { + expectMediaPickerForOption( MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA, requestMediaPickFromDeviceCamera ); + } ); +} ); diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 0b03bcca6a490d..40f758f064e081 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -30,6 +30,7 @@ import { getTextContent, insert, __unstableInsertLineSeparator as insertLineSeparator, + __unstableRemoveLineSeparator as removeLineSeparator, __unstableIsEmptyLine as isEmptyLine, __unstableToDom as toDom, remove, @@ -639,7 +640,7 @@ export class RichText extends Component { if ( keyCode === DELETE || keyCode === BACKSPACE ) { const value = this.createRecord(); - const { replacements, text, start, end } = value; + const { start, end } = value; // Always handle full content deletion ourselves. if ( start === 0 && end !== 0 && end === value.text.length ) { @@ -649,58 +650,7 @@ export class RichText extends Component { } if ( this.multilineTag ) { - let newValue; - - if ( keyCode === BACKSPACE ) { - const index = start - 1; - - if ( text[ index ] === LINE_SEPARATOR ) { - const collapsed = isCollapsed( value ); - - // If the line separator that is about te be removed - // contains wrappers, remove the wrappers first. - if ( collapsed && replacements[ index ] && replacements[ index ].length ) { - const newReplacements = replacements.slice(); - - newReplacements[ index ] = replacements[ index ].slice( 0, -1 ); - newValue = { - ...value, - replacements: newReplacements, - }; - } else { - newValue = remove( - value, - // Only remove the line if the selection is - // collapsed, otherwise remove the selection. - collapsed ? start - 1 : start, - end - ); - } - } - } else if ( text[ end ] === LINE_SEPARATOR ) { - const collapsed = isCollapsed( value ); - - // If the line separator that is about te be removed - // contains wrappers, remove the wrappers first. - if ( collapsed && replacements[ end ] && replacements[ end ].length ) { - const newReplacements = replacements.slice(); - - newReplacements[ end ] = replacements[ end ].slice( 0, -1 ); - newValue = { - ...value, - replacements: newReplacements, - }; - } else { - newValue = remove( - value, - start, - // Only remove the line if the selection is - // collapsed, otherwise remove the selection. - collapsed ? end + 1 : end, - ); - } - } - + const newValue = removeLineSeparator( value, keyCode === BACKSPACE ); if ( newValue ) { this.onChange( newValue ); event.preventDefault(); diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 13fcc41771e1c7..7d8cfe2b737df5 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -26,15 +26,19 @@ import { split, toHTMLString, insert, - __UNSTABLE_LINE_SEPARATOR as LINE_SEPARATOR, __unstableInsertLineSeparator as insertLineSeparator, __unstableIsEmptyLine as isEmptyLine, + __unstableRemoveLineSeparator as removeLineSeparator, isCollapsed, remove, } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { BACKSPACE } from '@wordpress/keycodes'; -import { pasteHandler, children } from '@wordpress/blocks'; +import { + children, + isUnmodifiedDefaultBlock, + pasteHandler, +} from '@wordpress/blocks'; import { isURL } from '@wordpress/url'; /** @@ -165,7 +169,7 @@ export class RichText extends Component { * @return {Object} A RichText value with formats and selection. */ createRecord() { - return { + const value = { start: this.selectionStart, end: this.selectionEnd, ...create( { @@ -175,6 +179,9 @@ export class RichText extends Component { multilineWrapperTags: this.multilineWrapperTags, } ), }; + const start = Math.min( this.selectionStart, value.text.length ); + const end = Math.min( this.selectionEnd, value.text.length ); + return { ...value, start, end }; } /** @@ -296,6 +303,12 @@ export class RichText extends Component { result = this.removeRootTag( element, result ); } ); } + + if ( this.props.tagsToEliminate ) { + this.props.tagsToEliminate.forEach( ( element ) => { + result = this.removeTag( element, result ); + } ); + } return result; } @@ -304,6 +317,11 @@ export class RichText extends Component { const closingTagRegexp = RegExp( '$', 'gim' ); return html.replace( openingTagRegexp, '' ).replace( closingTagRegexp, '' ); } + removeTag( tag, html ) { + const openingTagRegexp = RegExp( '<' + tag + '>', 'gim' ); + const closingTagRegexp = RegExp( '', 'gim' ); + return html.replace( openingTagRegexp, '' ).replace( closingTagRegexp, '' ); + } /* * Handles any case where the content of the AztecRN instance has changed @@ -344,25 +362,30 @@ export class RichText extends Component { // eslint-disable-next-line no-unused-vars onEnter( event ) { + if ( this.props.onEnter ) { + this.props.onEnter(); + return; + } + this.lastEventCount = event.nativeEvent.eventCount; this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; - + const { onReplace, onSplit } = this.props; + const canSplit = onReplace && onSplit; const currentRecord = this.createRecord(); - if ( this.multilineTag ) { if ( event.shiftKey ) { this.needsSelectionUpdate = true; const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; this.onFormatChange( insertedLineBreak ); - } else if ( this.onSplit && isEmptyLine( currentRecord ) ) { + } else if ( canSplit && isEmptyLine( currentRecord ) ) { this.onSplit( currentRecord ); } else { this.needsSelectionUpdate = true; const insertedLineSeparator = { ...insertLineSeparator( currentRecord ) }; this.onFormatChange( insertedLineSeparator ); } - } else if ( event.shiftKey || ! this.onSplit ) { + } else if ( event.shiftKey || ! onSplit ) { this.needsSelectionUpdate = true; const insertedLineBreak = { ...insert( currentRecord, '\n' ) }; this.onFormatChange( insertedLineBreak ); @@ -386,68 +409,18 @@ export class RichText extends Component { this.comesFromAztec = true; this.firedAfterTextChanged = event.nativeEvent.firedAfterTextChanged; const value = this.createRecord(); - const { replacements, text, start, end } = value; + const { start, end } = value; let newValue; // Always handle full content deletion ourselves. if ( start === 0 && end !== 0 && end >= value.text.length ) { newValue = remove( value, start, end ); this.props.onChange( newValue ); - this.forceSelectionUpdate( 0, 0 ); return; } if ( this.multilineTag ) { - if ( keyCode === BACKSPACE ) { - const index = start - 1; - - if ( text[ index ] === LINE_SEPARATOR ) { - const collapsed = isCollapsed( value ); - - // If the line separator that is about te be removed - // contains wrappers, remove the wrappers first. - if ( collapsed && replacements[ index ] && replacements[ index ].length ) { - const newReplacements = replacements.slice(); - - newReplacements[ index ] = replacements[ index ].slice( 0, -1 ); - newValue = { - ...value, - replacements: newReplacements, - }; - } else { - newValue = remove( - value, - // Only remove the line if the selection is - // collapsed, otherwise remove the selection. - collapsed ? start - 1 : start, - end - ); - } - } - } else if ( text[ end ] === LINE_SEPARATOR ) { - const collapsed = isCollapsed( value ); - - // If the line separator that is about te be removed - // contains wrappers, remove the wrappers first. - if ( collapsed && replacements[ end ] && replacements[ end ].length ) { - const newReplacements = replacements.slice(); - - newReplacements[ end ] = replacements[ end ].slice( 0, -1 ); - newValue = { - ...value, - replacements: newReplacements, - }; - } else { - newValue = remove( - value, - start, - // Only remove the line if the selection is - // collapsed, otherwise remove the selection. - collapsed ? end + 1 : end, - ); - } - } - + newValue = removeLineSeparator( value, keyCode === BACKSPACE ); if ( newValue ) { this.onFormatChange( newValue ); return; @@ -552,14 +525,14 @@ export class RichText extends Component { if ( typeof pastedContent === 'string' ) { const recordToInsert = create( { html: pastedContent } ); - const insertedContent = insert( currentRecord, recordToInsert ); - const newContent = this.valueToFormat( insertedContent ); + const resultingRecord = insert( currentRecord, recordToInsert ); + const resultingContent = this.valueToFormat( resultingRecord ); this.lastEventCount = undefined; - this.value = newContent; + this.value = resultingContent; // explicitly set selection after inline paste - this.forceSelectionUpdate( insertedContent.start, insertedContent.end ); + this.onSelectionChange( resultingRecord.start, resultingRecord.end ); this.props.onChange( this.value ); } else if ( onSplit ) { @@ -578,10 +551,18 @@ export class RichText extends Component { onFocus() { this.isTouched = true; - if ( this.props.onFocus ) { - this.props.onFocus(); + const { unstableOnFocus } = this.props; + + if ( unstableOnFocus ) { + unstableOnFocus(); } + // We know for certain that on focus, the old selection is invalid. It + // will be recalculated on `selectionchange`. + const index = undefined; + + this.props.onSelectionChange( index, index ); + this.lastAztecEventType = 'focus'; } @@ -673,15 +654,6 @@ export class RichText extends Component { return value; } - forceSelectionUpdate( start, end ) { - if ( ! this.needsSelectionUpdate ) { - this.needsSelectionUpdate = true; - this.selectionStart = start; - this.selectionEnd = end; - this.forceUpdate(); - } - } - shouldComponentUpdate( nextProps ) { if ( nextProps.tagName !== this.props.tagName ) { this.lastEventCount = undefined; @@ -713,7 +685,9 @@ export class RichText extends Component { } if ( ! this.comesFromAztec ) { - if ( nextProps.selectionStart !== this.props.selectionStart && + if ( ( typeof nextProps.selectionStart !== 'undefined' ) && + ( typeof nextProps.selectionEnd !== 'undefined' ) && + nextProps.selectionStart !== this.props.selectionStart && nextProps.selectionStart !== this.selectionStart && nextProps.isSelected ) { this.needsSelectionUpdate = true; @@ -725,14 +699,18 @@ export class RichText extends Component { } componentDidMount() { - if ( this.props.isSelected ) { + // Request focus if wrapping block is selected and parent hasn't inhibited the focus request. This method of focusing + // is trying to implement the web-side counterpart of BlockList's `focusTabbable` where the BlockList is focusing an + // inputbox by searching the DOM. We don't have the DOM in RN so, using the combination of blockIsSelected and __unstableMobileNoFocusOnMount + // to determine if we should focus the RichText. + if ( this.props.blockIsSelected && ! this.props.__unstableMobileNoFocusOnMount ) { this._editor.focus(); this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); } } componentWillUnmount() { - if ( this._editor.isFocused() ) { + if ( this._editor.isFocused() && this.props.shouldBlurOnUnmount ) { this._editor.blur(); } } @@ -747,7 +725,7 @@ export class RichText extends Component { // Update selection props explicitly when component is selected as Aztec won't call onSelectionChange // if its internal value hasn't change. When created, default value is 0, 0 this.onSelectionChange( this.props.selectionStart || 0, this.props.selectionEnd || 0 ); - } else if ( ! this.props.isSelected && prevProps.isSelected && this.isIOS ) { + } else if ( ! this.props.isSelected && prevProps.isSelected ) { this._editor.blur(); } } @@ -850,7 +828,7 @@ export class RichText extends Component { } } text={ { text: html, eventCount: this.lastEventCount, selection } } placeholder={ this.props.placeholder } - placeholderTextColor={ this.props.placeholderTextColor || styles[ 'block-editor-rich-text' ].textDecorationColor } + placeholderTextColor={ this.props.placeholderTextColor || styles[ 'block-editor-rich-text-placeholder' ].color } deleteEnter={ this.props.deleteEnter } onChange={ this.onChange } onFocus={ this.onFocus } @@ -862,12 +840,12 @@ export class RichText extends Component { onContentSizeChange={ this.onContentSizeChange } onCaretVerticalPositionChange={ this.props.onCaretVerticalPositionChange } onSelectionChange={ this.onSelectionChangeFromAztec } - isSelected={ isSelected } blockType={ { tag: tagName } } - color={ 'black' } + color={ styles[ 'block-editor-rich-text' ].color } + linkTextColor={ styles[ 'block-editor-rich-text' ].textDecorationColor } maxImagesWidth={ 200 } fontFamily={ this.props.fontFamily || styles[ 'block-editor-rich-text' ].fontFamily } - fontSize={ this.props.fontSize } + fontSize={ this.props.fontSize || ( style && style.fontSize ) } fontWeight={ this.props.fontWeight } fontStyle={ this.props.fontStyle } disableEditingMenu={ this.props.disableEditingMenu } @@ -888,18 +866,10 @@ RichText.defaultProps = { const RichTextContainer = compose( [ withInstanceId, - withBlockEditContext( ( { clientId, onFocus, onCaretVerticalPositionChange, isSelected }, ownProps ) => { - // ownProps.onFocus and isSelected needs precedence over the block edit context - if ( ownProps.isSelected !== undefined ) { - isSelected = ownProps.isSelected; - } - if ( ownProps.onFocus !== undefined ) { - onFocus = ownProps.onFocus; - } + withBlockEditContext( ( { clientId, onCaretVerticalPositionChange, isSelected }, ownProps ) => { return { - isSelected, clientId, - onFocus, + blockIsSelected: ownProps.isSelected !== undefined ? ownProps.isSelected : isSelected, onCaretVerticalPositionChange, }; } ), @@ -908,11 +878,13 @@ const RichTextContainer = compose( [ instanceId, identifier = instanceId, isSelected, + blockIsSelected, } ) => { const { getFormatTypes } = select( 'core/rich-text' ); const { getSelectionStart, getSelectionEnd, + __unstableGetBlockWithoutInnerBlocks, } = select( 'core/block-editor' ); const selectionStart = getSelectionStart(); @@ -925,11 +897,19 @@ const RichTextContainer = compose( [ ); } + // If the block of this RichText is unmodified then it's a candidate for replacing when adding a new block. + // In order to fix https://github.com/wordpress-mobile/gutenberg-mobile/issues/1126, let's blur on unmount in that case. + // This apparently assumes functionality the BlockHlder actually + const block = clientId && __unstableGetBlockWithoutInnerBlocks( clientId ); + const shouldBlurOnUnmount = block && isSelected && isUnmodifiedDefaultBlock( block ); + return { formatTypes: getFormatTypes(), selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, + blockIsSelected, + shouldBlurOnUnmount, }; } ), withDispatch( ( dispatch, { diff --git a/packages/block-editor/src/components/rich-text/style.native.scss b/packages/block-editor/src/components/rich-text/style.native.scss index ef530f4d3c7817..77413c5be1f9e4 100644 --- a/packages/block-editor/src/components/rich-text/style.native.scss +++ b/packages/block-editor/src/components/rich-text/style.native.scss @@ -1,6 +1,11 @@ .block-editor-rich-text { font-family: $default-regular-font; - text-decoration-color: $gray; min-height: $min-height-paragraph; + color: $gray-900; + text-decoration-color: $blue-500; +} + +.block-editor-rich-text-placeholder { + color: $gray; } diff --git a/packages/block-editor/src/components/rich-text/test/index.native.js b/packages/block-editor/src/components/rich-text/test/index.native.js new file mode 100644 index 00000000000000..ec0cbb77195244 --- /dev/null +++ b/packages/block-editor/src/components/rich-text/test/index.native.js @@ -0,0 +1,56 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * Internal dependencies + */ +import { RichText } from '../index'; + +describe( 'RichText Native', () => { + let richText; + + beforeEach( () => { + richText = new RichText( { multiline: false } ); + } ); + + describe( 'willTrimSpaces', () => { + it( 'exists', () => { + expect( richText ).toHaveProperty( 'willTrimSpaces' ); + } ); + + it( 'is a function', () => { + expect( richText.willTrimSpaces ).toBeInstanceOf( Function ); + } ); + + it( 'reports false for styled text with no outer spaces', () => { + const html = '

Hello Hello WorldWorld!

'; + expect( richText.willTrimSpaces( html ) ).toBe( false ); + } ); + } ); + + describe( 'Adds new line on Enter', () => { + let newValue; + const wrapper = shallow( { + newValue = value; + } } + formatTypes={ [] } + onSelectionChange={ jest.fn() } + /> ); + + const event = { + nativeEvent: { + eventCount: 0, + }, + }; + wrapper.instance().onEnter( event ); + + it( ' Adds
tag to content after pressing Enter key', () => { + expect( newValue ).toEqual( '
' ); + } ); + } ); +} ); diff --git a/packages/block-editor/src/components/url-input/style.scss b/packages/block-editor/src/components/url-input/style.scss index de6d71b9facea3..54c1128281b72b 100644 --- a/packages/block-editor/src/components/url-input/style.scss +++ b/packages/block-editor/src/components/url-input/style.scss @@ -43,6 +43,7 @@ $input-size: 300px; .block-editor-url-input__suggestions { max-height: 200px; transition: all 0.15s ease-in-out; + @include reduce-motion("transition"); padding: 4px 0; // To match the url-input width: input width + padding + 2 buttons. width: $input-size + 2; diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 2b9e7adfd9dd1c..3f21ed3ef3b0a2 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { assign } from 'lodash'; +import { assign, has } from 'lodash'; /** * WordPress dependencies @@ -33,6 +33,10 @@ const ANCHOR_REGEX = /[\s#]/g; * @return {Object} Filtered block settings. */ export function addAttribute( settings ) { + // allow blocks to specify their own attribute definition with default values if needed. + if ( has( settings.attributes, [ 'anchor', 'type' ] ) ) { + return settings; + } if ( hasBlockSupport( settings, 'anchor' ) ) { // Use Lodash's assign to gracefully handle if attributes are undefined settings.attributes = assign( settings.attributes, { diff --git a/packages/block-editor/src/hooks/test/anchor.js b/packages/block-editor/src/hooks/test/anchor.js index 21a5c65be83cb0..d55a54fff332e2 100644 --- a/packages/block-editor/src/hooks/test/anchor.js +++ b/packages/block-editor/src/hooks/test/anchor.js @@ -39,6 +39,23 @@ describe( 'anchor', () => { expect( settings.attributes ).toHaveProperty( 'anchor' ); } ); + + it( 'should not override attributes defined in settings', () => { + const settings = registerBlockType( { + ...blockSettings, + supports: { + anchor: true, + }, + attributes: { + anchor: { + type: 'string', + default: 'testAnchor', + }, + }, + } ); + + expect( settings.attributes.anchor ).toEqual( { type: 'string', default: 'testAnchor' } ); + } ); } ); describe( 'addSaveProps', () => { diff --git a/packages/block-editor/src/index.js b/packages/block-editor/src/index.js index 1d8827e2a33b49..d554e6e577a385 100644 --- a/packages/block-editor/src/index.js +++ b/packages/block-editor/src/index.js @@ -14,5 +14,5 @@ import './hooks'; export * from './components'; export * from './utils'; - +export { storeConfig } from './store'; export { SETTINGS_DEFAULTS } from './store/defaults'; diff --git a/packages/block-editor/src/store/index.js b/packages/block-editor/src/store/index.js index 485238f46f606d..bfc7766a508762 100644 --- a/packages/block-editor/src/store/index.js +++ b/packages/block-editor/src/store/index.js @@ -17,6 +17,13 @@ import controls from './controls'; */ const MODULE_KEY = 'core/block-editor'; +/** + * Block editor data store configuration. + * + * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#registerStore + * + * @type {Object} + */ export const storeConfig = { reducer, selectors, diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index b0576be7c47fdd..d33c0a1ec92767 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -1,3 +1,7 @@ +## ## 2.6.0 (2019-06-12) + +- Fixed an issue with creating upgraded embed blocks that are not registered ([#15883](https://github.com/WordPress/gutenberg/issues/15883)). + ## 2.5.0 (2019-05-21) - Add vertical alignment controls to Columns Block ([#13899](https://github.com/WordPress/gutenberg/pull/13899/)). diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 4465a8dfefc496..f57f2988524cee 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "2.5.0", + "version": "2.6.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -36,6 +36,7 @@ "@wordpress/html-entities": "file:../html-entities", "@wordpress/i18n": "file:../i18n", "@wordpress/keycodes": "file:../keycodes", + "@wordpress/server-side-render": "file:../server-side-render", "@wordpress/viewport": "file:../viewport", "classnames": "^2.2.5", "fast-average-color": "4.3.0", diff --git a/packages/block-library/src/archives/edit.js b/packages/block-library/src/archives/edit.js index c67cdc912e5260..6909b70ccc4271 100644 --- a/packages/block-library/src/archives/edit.js +++ b/packages/block-library/src/archives/edit.js @@ -8,7 +8,7 @@ import { } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { InspectorControls } from '@wordpress/block-editor'; -import { ServerSideRender } from '@wordpress/editor'; +import ServerSideRender from '@wordpress/server-side-render'; export default function ArchivesEdit( { attributes, setAttributes } ) { const { showPostCounts, displayAsDropdown } = attributes; diff --git a/packages/block-library/src/audio/edit.js b/packages/block-library/src/audio/edit.js index 1545378ddc0c74..164b8c22c9a7ba 100644 --- a/packages/block-library/src/audio/edit.js +++ b/packages/block-library/src/audio/edit.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { getBlobByURL, isBlobURL } from '@wordpress/blob'; +import { compose } from '@wordpress/compose'; import { Disabled, IconButton, @@ -18,9 +19,9 @@ import { MediaPlaceholder, RichText, } from '@wordpress/block-editor'; -import { mediaUpload } from '@wordpress/editor'; import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -45,10 +46,16 @@ class AudioEdit extends Component { this.toggleAttribute = this.toggleAttribute.bind( this ); this.onSelectURL = this.onSelectURL.bind( this ); + this.onUploadError = this.onUploadError.bind( this ); } componentDidMount() { - const { attributes, noticeOperations, setAttributes } = this.props; + const { + attributes, + mediaUpload, + noticeOperations, + setAttributes, + } = this.props; const { id, src = '' } = attributes; if ( ! id && isBlobURL( src ) ) { @@ -98,13 +105,19 @@ class AudioEdit extends Component { this.setState( { editing: false } ); } + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + getAutoplayHelp( checked ) { return checked ? __( 'Note: Autoplaying audio may cause usability issues for some visitors.' ) : null; } render() { const { autoplay, caption, loop, preload, src } = this.props.attributes; - const { setAttributes, isSelected, className, noticeOperations, noticeUI } = this.props; + const { setAttributes, isSelected, className, noticeUI } = this.props; const { editing } = this.state; const switchToEditing = () => { this.setState( { editing: true } ); @@ -133,7 +146,7 @@ class AudioEdit extends Component { allowedTypes={ ALLOWED_MEDIA_TYPES } value={ this.props.attributes } notices={ noticeUI } - onError={ noticeOperations.createErrorNotice } + onError={ this.onUploadError } /> ); } @@ -200,5 +213,13 @@ class AudioEdit extends Component { /* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/onclick-has-role, jsx-a11y/click-events-have-key-events */ } } - -export default withNotices( AudioEdit ); +export default compose( [ + withSelect( ( select ) => { + const { getSettings } = select( 'core/block-editor' ); + const { __experimentalMediaUpload } = getSettings(); + return { + mediaUpload: __experimentalMediaUpload, + }; + } ), + withNotices, +] )( AudioEdit ); diff --git a/packages/block-library/src/block/edit-panel/style.scss b/packages/block-library/src/block/edit-panel/editor.scss similarity index 90% rename from packages/block-library/src/block/edit-panel/style.scss rename to packages/block-library/src/block/edit-panel/editor.scss index 6c11e19d5e227c..d5b5c81fd7e714 100644 --- a/packages/block-library/src/block/edit-panel/style.scss +++ b/packages/block-library/src/block/edit-panel/editor.scss @@ -11,6 +11,10 @@ margin: 0 (-$block-padding); padding: $grid-size $block-padding; + // Elevate the reusable blocks toolbar above the clickthrough overlay. + position: relative; + z-index: z-index(".block-editor-block-list__layout .reusable-block-edit-panel"); + // Use opacity to work in various editor styles. border: $border-width dashed $dark-opacity-light-500; border-bottom: none; diff --git a/packages/block-library/src/block/indicator/style.scss b/packages/block-library/src/block/indicator/editor.scss similarity index 100% rename from packages/block-library/src/block/indicator/style.scss rename to packages/block-library/src/block/indicator/editor.scss diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index 6d3f279542e009..142f9b0a06835f 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -130,6 +130,10 @@ class ButtonEdit extends Component { setAttributes( { url: value } ) } /> diff --git a/packages/block-library/src/calendar/edit.js b/packages/block-library/src/calendar/edit.js index f884490ec38c80..19027eecf099f7 100644 --- a/packages/block-library/src/calendar/edit.js +++ b/packages/block-library/src/calendar/edit.js @@ -61,9 +61,13 @@ class CalendarEdit extends Component { } export default withSelect( ( select ) => { + const coreEditorSelect = select( 'core/editor' ); + if ( ! coreEditorSelect ) { + return; + } const { getEditedPostAttribute, - } = select( 'core/editor' ); + } = coreEditorSelect; const postType = getEditedPostAttribute( 'type' ); // Dates are used to overwrite year and month used on the calendar. // This overwrite should only happen for 'post' post types. diff --git a/packages/block-library/src/classic/editor.scss b/packages/block-library/src/classic/editor.scss index 4313096afe14d8..3d4965146dcf2e 100644 --- a/packages/block-library/src/classic/editor.scss +++ b/packages/block-library/src/classic/editor.scss @@ -242,6 +242,7 @@ div[data-type="core/freeform"] { .block-editor-block-list__block-edit::before { transition: border-color 0.1s linear, box-shadow 0.1s linear; + @include reduce-motion("transition"); border: $border-width solid $light-gray-500; // Windows High Contrast mode will show this outline. diff --git a/packages/block-library/src/code/test/edit.native.js b/packages/block-library/src/code/test/edit.native.js new file mode 100644 index 00000000000000..62e9fa9adb48cc --- /dev/null +++ b/packages/block-library/src/code/test/edit.native.js @@ -0,0 +1,26 @@ +/** + * External dependencies + */ +import renderer from 'react-test-renderer'; + +/** + * Internal dependencies + */ +import Code from '../edit'; +import { TextInput } from 'react-native'; + +describe( 'Code', () => { + it( 'renders without crashing', () => { + const component = renderer.create( ); + const rendered = component.toJSON(); + expect( rendered ).toBeTruthy(); + } ); + + it( 'renders given text without crashing', () => { + const component = renderer.create( ); + const testInstance = component.root; + const textInput = testInstance.findByType( TextInput ); + expect( textInput ).toBeTruthy(); + expect( textInput.props.value ).toBe( 'sample text' ); + } ); +} ); diff --git a/packages/block-library/src/columns/editor.scss b/packages/block-library/src/columns/editor.scss index c771c79f5f9d3c..6e9e2e60659114 100644 --- a/packages/block-library/src/columns/editor.scss +++ b/packages/block-library/src/columns/editor.scss @@ -21,7 +21,7 @@ // Fullwide: show margin left/right to ensure there's room for the side UI. // This is not a 1:1 preview with the front-end where these margins would presumably be zero. -.editor-block-list__block[data-align="full"] [data-type="core/columns"][data-align="full"] .wp-block-columns > .editor-inner-blocks { +[data-type="core/columns"][data-align="full"] .wp-block-columns > .editor-inner-blocks { padding-left: $block-padding; padding-right: $block-padding; @@ -164,17 +164,13 @@ div.block-core-columns.is-vertically-aligned-bottom { */ [data-type="core/column"] > .editor-block-list__block-edit > .editor-block-list__breadcrumb { right: 0; + left: auto; } -// The empty state of a columns block has the default appenders. -// Since those appenders are not blocks, the parent, actual block, appears "hovered" when hovering the appenders. -// Because the column shouldn't be hovered as part of this temporary passthrough, we unset the hover style. -.wp-block-columns [data-type="core/column"].is-hovered { - > .block-editor-block-list__block-edit::before { - content: none; - } - - .block-editor-block-list__breadcrumb { - display: none; - } +/** + * Make single Column overlay not extend past boundaries of parent + */ +.block-core-columns > .block-editor-inner-blocks.has-overlay::after { + left: 0; + right: 0; } diff --git a/packages/block-library/src/cover/edit.js b/packages/block-library/src/cover/edit.js index f908be2423d654..7de37e53f3fc39 100644 --- a/packages/block-library/src/cover/edit.js +++ b/packages/block-library/src/cover/edit.js @@ -72,6 +72,7 @@ class CoverEdit extends Component { this.imageRef = createRef(); this.videoRef = createRef(); this.changeIsDarkIfRequired = this.changeIsDarkIfRequired.bind( this ); + this.onUploadError = this.onUploadError.bind( this ); } componentDidMount() { @@ -82,12 +83,17 @@ class CoverEdit extends Component { this.handleBackgroundMode( prevProps ); } + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + render() { const { attributes, setAttributes, className, - noticeOperations, noticeUI, overlayColor, setOverlayColor, @@ -237,13 +243,13 @@ class CoverEdit extends Component { className={ className } labels={ { title: label, - instructions: __( 'Drag an image or a video, upload a new one or select a file from your library.' ), + instructions: __( 'Upload an image or video file, or pick one from your media library.' ), } } onSelect={ onSelectMedia } accept="image/*,video/*" allowedTypes={ ALLOWED_MEDIA_TYPES } notices={ noticeUI } - onError={ noticeOperations.createErrorNotice } + onError={ this.onUploadError } /> ); @@ -256,6 +262,7 @@ class CoverEdit extends Component { 'is-dark-theme': this.state.isDark, 'has-background-dim': dimRatio !== 0, 'has-parallax': hasParallax, + [ overlayColor.class ]: overlayColor.class, } ); diff --git a/packages/block-library/src/editor.scss b/packages/block-library/src/editor.scss index c0c465d33ffbf3..99493394ed43f4 100644 --- a/packages/block-library/src/editor.scss +++ b/packages/block-library/src/editor.scss @@ -34,6 +34,11 @@ @import "./verse/editor.scss"; @import "./video/editor.scss"; +/** + * Import styles from internal editor components used by the blocks. + */ +@import "./block/edit-panel/editor.scss"; +@import "./block/indicator/editor.scss"; /** * Editor Normalization Styles diff --git a/packages/block-library/src/embed/editor.scss b/packages/block-library/src/embed/editor.scss index 507d38c256688f..342b4fe1b116b2 100644 --- a/packages/block-library/src/embed/editor.scss +++ b/packages/block-library/src/embed/editor.scss @@ -37,6 +37,10 @@ .components-placeholder__error { word-break: break-word; } + + .components-placeholder__learn-more { + margin-top: 1em; + } } .block-library-embed__interactive-overlay { diff --git a/packages/block-library/src/embed/embed-placeholder.js b/packages/block-library/src/embed/embed-placeholder.js index b532d7b1a84a78..7a2a6f000a01f6 100644 --- a/packages/block-library/src/embed/embed-placeholder.js +++ b/packages/block-library/src/embed/embed-placeholder.js @@ -2,13 +2,18 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; -import { Button, Placeholder } from '@wordpress/components'; +import { Button, Placeholder, ExternalLink } from '@wordpress/components'; import { BlockIcon } from '@wordpress/block-editor'; const EmbedPlaceholder = ( props ) => { const { icon, label, value, onSubmit, onChange, cannotEmbed, fallback, tryAgain } = props; return ( - } label={ label } className="wp-block-embed"> + } + label={ label } + className="wp-block-embed" + instructions={ __( 'Paste a link to the content you want to display on your site.' ) } + >
{

}
+
+ + { __( 'Learn more about embeds' ) } + +
); }; diff --git a/packages/block-library/src/embed/test/__snapshots__/index.js.snap b/packages/block-library/src/embed/test/__snapshots__/index.js.snap index 4b9c1f4de7f0e3..78923aa24c5f6b 100644 --- a/packages/block-library/src/embed/test/__snapshots__/index.js.snap +++ b/packages/block-library/src/embed/test/__snapshots__/index.js.snap @@ -27,6 +27,11 @@ exports[`core/embed block edit matches snapshot 1`] = ` Embed URL
+
+ Paste a link to the content you want to display on your site. +
@@ -45,6 +50,37 @@ exports[`core/embed block edit matches snapshot 1`] = ` Embed +
`; diff --git a/packages/block-library/src/embed/test/index.js b/packages/block-library/src/embed/test/index.js index 3931ae7b581e18..5f01d8ff77ee84 100644 --- a/packages/block-library/src/embed/test/index.js +++ b/packages/block-library/src/embed/test/index.js @@ -3,11 +3,16 @@ */ import { render } from 'enzyme'; +/** + * WordPress dependencies + */ +import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; + /** * Internal dependencies */ import { getEmbedEditComponent } from '../edit'; -import { findBlock, getClassNames } from '../util'; +import { findBlock, getClassNames, createUpgradedEmbedBlock } from '../util'; describe( 'core/embed', () => { test( 'block edit matches snapshot', () => { @@ -44,4 +49,29 @@ describe( 'core/embed', () => { const expected = 'lovely'; expect( getClassNames( html, 'lovely wp-embed-aspect-16-9 wp-has-aspect-ratio', false ) ).toEqual( expected ); } ); + + test( 'createUpgradedEmbedBlock bails early when block type does not exist', () => { + const youtubeURL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; + + expect( createUpgradedEmbedBlock( { attributes: { url: youtubeURL } }, {} ) ).toBeUndefined(); + } ); + + test( 'createUpgradedEmbedBlock returns a YouTube embed block when given a YouTube URL', () => { + const youtubeURL = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'; + + registerBlockType( + 'core-embed/youtube', + { + title: 'YouTube', + category: 'embed', + } + ); + + const result = createUpgradedEmbedBlock( { attributes: { url: youtubeURL } }, {} ); + + unregisterBlockType( 'core-embed/youtube' ); + + expect( result ).not.toBeUndefined(); + expect( result.name ).toBe( 'core-embed/youtube' ); + } ); } ); diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index f52716f2cb96f2..80bcbed310e9a0 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -15,7 +15,7 @@ import memoize from 'memize'; * WordPress dependencies */ import { renderToString } from '@wordpress/element'; -import { createBlock } from '@wordpress/blocks'; +import { createBlock, getBlockType } from '@wordpress/blocks'; /** * Returns true if any of the regular expressions match the URL. @@ -82,6 +82,10 @@ export const createUpgradedEmbedBlock = ( props, attributesFromPreview ) => { const matchingBlock = findBlock( url ); + if ( ! getBlockType( matchingBlock ) ) { + return; + } + // WordPress blocks can work on multiple sites, and so don't have patterns, // so if we're in a WordPress block, assume the user has chosen it for a WordPress URL. if ( WORDPRESS_EMBED_BLOCK !== name && DEFAULT_EMBED_BLOCK !== matchingBlock ) { diff --git a/packages/block-library/src/file/edit.js b/packages/block-library/src/file/edit.js index efa93424a5a9e6..2ab0baaa08f8bf 100644 --- a/packages/block-library/src/file/edit.js +++ b/packages/block-library/src/file/edit.js @@ -27,7 +27,6 @@ import { MediaPlaceholder, RichText, } from '@wordpress/block-editor'; -import { mediaUpload } from '@wordpress/editor'; import { Component } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; @@ -47,6 +46,7 @@ class FileEdit extends Component { this.changeLinkDestinationOption = this.changeLinkDestinationOption.bind( this ); this.changeOpenInNewWindow = this.changeOpenInNewWindow.bind( this ); this.changeShowDownloadButton = this.changeShowDownloadButton.bind( this ); + this.onUploadError = this.onUploadError.bind( this ); this.state = { hasError: false, @@ -55,7 +55,12 @@ class FileEdit extends Component { } componentDidMount() { - const { attributes, noticeOperations, setAttributes } = this.props; + const { + attributes, + mediaUpload, + noticeOperations, + setAttributes, + } = this.props; const { downloadButtonText, href } = attributes; // Upload a file drag-and-dropped into the editor @@ -100,6 +105,12 @@ class FileEdit extends Component { } } + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + confirmCopyURL() { this.setState( { showCopyConfirmation: true } ); } @@ -130,7 +141,6 @@ class FileEdit extends Component { attributes, setAttributes, noticeUI, - noticeOperations, media, } = this.props; const { @@ -151,11 +161,11 @@ class FileEdit extends Component { icon={ } labels={ { title: __( 'File' ), - instructions: __( 'Drag a file, upload a new one or select a file from your library.' ), + instructions: __( 'Upload a file or pick one from your media library.' ), } } onSelect={ this.onSelectFile } notices={ noticeUI } - onError={ noticeOperations.createErrorNotice } + onError={ this.onUploadError } accept="*" /> ); @@ -242,9 +252,12 @@ class FileEdit extends Component { export default compose( [ withSelect( ( select, props ) => { const { getMedia } = select( 'core' ); + const { getSettings } = select( 'core/block-editor' ); + const { __experimentalMediaUpload } = getSettings(); const { id } = props.attributes; return { media: id === undefined ? undefined : getMedia( id ), + mediaUpload: __experimentalMediaUpload, }; } ), withNotices, diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index cb1188c010ba1b..f8d1db665208f0 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -2,11 +2,12 @@ * External dependencies */ import classnames from 'classnames'; -import { filter, map } from 'lodash'; +import { every, filter, forEach, map } from 'lodash'; /** * WordPress dependencies */ +import { compose } from '@wordpress/compose'; import { IconButton, PanelBody, @@ -25,6 +26,8 @@ import { } from '@wordpress/block-editor'; import { Component } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; +import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { withSelect } from '@wordpress/data'; /** * Internal dependencies @@ -54,6 +57,7 @@ class GalleryEdit extends Component { this.onMoveForward = this.onMoveForward.bind( this ); this.onMoveBackward = this.onMoveBackward.bind( this ); this.onRemoveImage = this.onRemoveImage.bind( this ); + this.onUploadError = this.onUploadError.bind( this ); this.setImageAttributes = this.setImageAttributes.bind( this ); this.setAttributes = this.setAttributes.bind( this ); @@ -133,6 +137,12 @@ class GalleryEdit extends Component { } ); } + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + setLinkTo( value ) { this.setAttributes( { linkTo: value } ); } @@ -167,6 +177,20 @@ class GalleryEdit extends Component { } ); } + componentDidMount() { + const { attributes, mediaUpload } = this.props; + const { images } = attributes; + if ( every( images, ( { url } ) => isBlobURL( url ) ) ) { + const filesList = map( images, ( { url } ) => getBlobByURL( url ) ); + forEach( images, ( { url } ) => revokeBlobURL( url ) ); + mediaUpload( { + filesList, + onFileChange: this.onSelectImages, + allowedTypes: [ 'image' ], + } ); + } + } + componentDidUpdate( prevProps ) { // Deselect images when deselecting the block if ( ! this.props.isSelected && prevProps.isSelected ) { @@ -178,7 +202,7 @@ class GalleryEdit extends Component { } render() { - const { attributes, isSelected, className, noticeOperations, noticeUI } = this.props; + const { attributes, isSelected, className, noticeUI } = this.props; const { images, columns = defaultColumnsNumber( attributes ), align, imageCrop, linkTo } = attributes; const hasImages = !! images.length; @@ -223,7 +247,7 @@ class GalleryEdit extends Component { allowedTypes={ ALLOWED_MEDIA_TYPES } multiple value={ hasImages ? images : undefined } - onError={ noticeOperations.createErrorNotice } + onError={ this.onUploadError } notices={ hasImages ? undefined : noticeUI } /> ); @@ -305,5 +329,16 @@ class GalleryEdit extends Component { ); } } +export default compose( [ + withSelect( ( select ) => { + const { getSettings } = select( 'core/block-editor' ); + const { + __experimentalMediaUpload, + } = getSettings(); -export default withNotices( GalleryEdit ); + return { + mediaUpload: __experimentalMediaUpload, + }; + } ), + withNotices, +] )( GalleryEdit ); diff --git a/packages/block-library/src/gallery/transforms.js b/packages/block-library/src/gallery/transforms.js index af8d5d164c10e0..ea752fe296bd9e 100644 --- a/packages/block-library/src/gallery/transforms.js +++ b/packages/block-library/src/gallery/transforms.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { filter, every, map } from 'lodash'; +import { filter, every } from 'lodash'; /** * WordPress dependencies */ import { createBlock } from '@wordpress/blocks'; -import { mediaUpload } from '@wordpress/editor'; import { createBlobURL } from '@wordpress/blob'; /** @@ -89,25 +88,12 @@ const transforms = { isMatch( files ) { return files.length !== 1 && every( files, ( file ) => file.type.indexOf( 'image/' ) === 0 ); }, - transform( files, onChange ) { + transform( files ) { const block = createBlock( 'core/gallery', { images: files.map( ( file ) => pickRelevantMediaFiles( { url: createBlobURL( file ), } ) ), } ); - mediaUpload( { - filesList: files, - onFileChange: ( images ) => { - const imagesAttr = images.map( - pickRelevantMediaFiles, - ); - onChange( block.clientId, { - ids: map( imagesAttr, 'id' ), - images: imagesAttr, - } ); - }, - allowedTypes: [ 'image' ], - } ); return block; }, }, diff --git a/packages/block-library/src/group/icon.js b/packages/block-library/src/group/icon.js index 959b844350615f..f9d9dc7790ba95 100644 --- a/packages/block-library/src/group/icon.js +++ b/packages/block-library/src/group/icon.js @@ -4,5 +4,5 @@ import { Path, SVG } from '@wordpress/components'; export default ( - + ); diff --git a/packages/block-library/src/group/index.js b/packages/block-library/src/group/index.js index 9bab779f5863b5..844ddd8c821600 100644 --- a/packages/block-library/src/group/index.js +++ b/packages/block-library/src/group/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; +import { createBlock } from '@wordpress/blocks'; /** * Internal dependencies @@ -25,6 +26,45 @@ export const settings = { anchor: true, html: false, }, + + transforms: { + from: [ + { + type: 'block', + isMultiBlock: true, + blocks: [ '*' ], + __experimentalConvert( blocks ) { + // Avoid transforming a single `core/group` Block + if ( blocks.length === 1 && blocks[ 0 ].name === 'core/group' ) { + return; + } + + const alignments = [ 'wide', 'full' ]; + + // Determine the widest setting of all the blocks to be grouped + const widestAlignment = blocks.reduce( ( result, block ) => { + const { align } = block.attributes; + return alignments.indexOf( align ) > alignments.indexOf( result ) ? align : result; + }, undefined ); + + // Clone the Blocks to be Grouped + // Failing to create new block references causes the original blocks + // to be replaced in the switchToBlockType call thereby meaning they + // are removed both from their original location and within the + // new group block. + const groupInnerBlocks = blocks.map( ( block ) => { + return createBlock( block.name, block.attributes, block.innerBlocks ); + } ); + + return createBlock( 'core/group', { + align: widestAlignment, + }, groupInnerBlocks ); + }, + }, + + ], + }, + edit, save, }; diff --git a/packages/block-library/src/heading/deprecated.js b/packages/block-library/src/heading/deprecated.js new file mode 100644 index 00000000000000..4a3a7d2f6d3653 --- /dev/null +++ b/packages/block-library/src/heading/deprecated.js @@ -0,0 +1,79 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + getColorClassName, + RichText, +} from '@wordpress/block-editor'; + +const blockSupports = { + className: false, + anchor: true, +}; + +const blockAttributes = { + align: { + type: 'string', + }, + content: { + type: 'string', + source: 'html', + selector: 'h1,h2,h3,h4,h5,h6', + default: '', + }, + level: { + type: 'number', + default: 2, + }, + placeholder: { + type: 'string', + }, + textColor: { + type: 'string', + }, + customTextColor: { + type: 'string', + }, +}; + +const deprecated = [ + { + supports: blockSupports, + attributes: blockAttributes, + save( { attributes } ) { + const { + align, + level, + content, + textColor, + customTextColor, + } = attributes; + const tagName = 'h' + level; + + const textClass = getColorClassName( 'color', textColor ); + + const className = classnames( { + [ textClass ]: textClass, + } ); + + return ( + + ); + }, + }, +]; + +export default deprecated; diff --git a/packages/block-library/src/heading/edit.js b/packages/block-library/src/heading/edit.js index e8a396a147df57..7fc4721c9169ab 100644 --- a/packages/block-library/src/heading/edit.js +++ b/packages/block-library/src/heading/edit.js @@ -100,13 +100,13 @@ function HeadingEdit( { onReplace={ onReplace } onRemove={ () => onReplace( [] ) } className={ classnames( className, { + [ `has-text-align-${ align }` ]: align, 'has-text-color': textColor.color, [ textColor.class ]: textColor.class, } ) } placeholder={ placeholder || __( 'Write heading…' ) } style={ { color: textColor.color, - textAlign: align, } } /> diff --git a/packages/block-library/src/heading/edit.native.js b/packages/block-library/src/heading/edit.native.js index 97d2228346466a..389791663b0541 100644 --- a/packages/block-library/src/heading/edit.native.js +++ b/packages/block-library/src/heading/edit.native.js @@ -8,94 +8,56 @@ import styles from './editor.scss'; * External dependencies */ import { View } from 'react-native'; -import { isEmpty } from 'lodash'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; -import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; import { RichText, BlockControls } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; -import { create } from '@wordpress/rich-text'; -class HeadingEdit extends Component { - plainTextContent( html ) { - const result = create( { html } ); - if ( result ) { - return result.text; - } - return ''; - } - - render() { - const { - attributes, - setAttributes, - mergeBlocks, - style, - onReplace, - } = this.props; - - const { - level, - placeholder, - content, - } = attributes; - - const tagName = 'h' + level; - - return ( - ( + + + setAttributes( { level: newLevel } ) } + /> + + setAttributes( { content: value } ) } + onMerge={ mergeBlocks } + onSplit={ ( value ) => { + if ( ! value ) { + return createBlock( 'core/paragraph' ); } - onAccessibilityTap={ this.props.onFocus } - > - - setAttributes( { level: newLevel } ) } /> - - setAttributes( { content: value } ) } - onMerge={ mergeBlocks } - onSplit={ ( value ) => { - if ( ! value ) { - return createBlock( 'core/paragraph' ); - } - return createBlock( 'core/heading', { - ...attributes, - content: value, - } ); - } } - onReplace={ onReplace } - onRemove={ () => onReplace( [] ) } - placeholder={ placeholder || __( 'Write heading…' ) } - /> - - ); - } -} + return createBlock( 'core/heading', { + ...attributes, + content: value, + } ); + } } + onReplace={ onReplace } + onRemove={ () => onReplace( [] ) } + placeholder={ attributes.placeholder || __( 'Write heading…' ) } + /> + +); + export default HeadingEdit; diff --git a/packages/block-library/src/heading/index.js b/packages/block-library/src/heading/index.js index 7706f44bb54ce6..1dc024193e4a76 100644 --- a/packages/block-library/src/heading/index.js +++ b/packages/block-library/src/heading/index.js @@ -6,6 +6,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ +import deprecated from './deprecated'; import edit from './edit'; import metadata from './block.json'; import save from './save'; @@ -25,6 +26,7 @@ export const settings = { anchor: true, }, transforms, + deprecated, merge( attributes, attributesToMerge ) { return { content: ( attributes.content || '' ) + ( attributesToMerge.content || '' ), diff --git a/packages/block-library/src/heading/index.native.js b/packages/block-library/src/heading/index.native.js new file mode 100644 index 00000000000000..67a6d2d91ded01 --- /dev/null +++ b/packages/block-library/src/heading/index.native.js @@ -0,0 +1,39 @@ +/** + * External dependencies + */ +import { isEmpty } from 'lodash'; + +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; +import { create } from '@wordpress/rich-text'; + +/** + * Internal dependencies + */ +import { settings as webSettings } from './index.js'; + +export { metadata, name } from './index.js'; + +export const settings = { + ...webSettings, + __experimentalGetAccessibilityLabel( attributes ) { + const { content, level } = attributes; + + const plainTextContent = ( html ) => create( { html } ).text || ''; + + return isEmpty( content ) ? + sprintf( + /* translators: accessibility text. %s: heading level. */ + __( 'Level %s. Empty.' ), + level + ) : + sprintf( + /* translators: accessibility text. 1: heading level. 2: heading content. */ + __( 'Level %1$s. %2$s' ), + level, + plainTextContent( content ) + ); + }, +}; diff --git a/packages/block-library/src/heading/save.js b/packages/block-library/src/heading/save.js index f554ff815a121a..7cf0de59884828 100644 --- a/packages/block-library/src/heading/save.js +++ b/packages/block-library/src/heading/save.js @@ -14,10 +14,10 @@ import { export default function save( { attributes } ) { const { align, - level, content, - textColor, customTextColor, + level, + textColor, } = attributes; const tagName = 'h' + level; @@ -25,6 +25,7 @@ export default function save( { attributes } ) { const className = classnames( { [ textClass ]: textClass, + [ `has-text-align-${ align }` ]: align, } ); return ( @@ -32,7 +33,6 @@ export default function save( { attributes } ) { className={ className ? className : undefined } tagName={ tagName } style={ { - textAlign: align, color: textClass ? undefined : customTextColor, } } value={ content } diff --git a/packages/block-library/src/html/edit.js b/packages/block-library/src/html/edit.js index 4ddc146ccf3d66..4a8ddf014c8542 100644 --- a/packages/block-library/src/html/edit.js +++ b/packages/block-library/src/html/edit.js @@ -82,6 +82,7 @@ class HTMLEdit extends Component { onChange={ ( content ) => setAttributes( { content } ) } placeholder={ __( 'Write HTML…' ) } aria-label={ __( 'HTML' ) } + rows={ 3 } /> ) ) } @@ -90,6 +91,7 @@ class HTMLEdit extends Component { ); } } + export default withSelect( ( select ) => { const { getSettings } = select( 'core/block-editor' ); return { diff --git a/packages/block-library/src/html/editor.scss b/packages/block-library/src/html/editor.scss index 2990803cb8eaae..3457c0c98f1768 100644 --- a/packages/block-library/src/html/editor.scss +++ b/packages/block-library/src/html/editor.scss @@ -5,9 +5,11 @@ font-family: $editor-html-font; color: $dark-gray-800; padding: 0.8em 1em; + max-height: 250px; border: 1px solid $light-gray-500; border-radius: 4px; + /* Fonts smaller than 16px causes mobile safari to zoom. */ font-size: $mobile-text-min-font-size; @include break-small { diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index 645248bc702414..7103c495e7a230 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -43,7 +43,6 @@ import { MediaPlaceholder, RichText, } from '@wordpress/block-editor'; -import { mediaUpload } from '@wordpress/editor'; import { Component } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { getPath } from '@wordpress/url'; @@ -126,7 +125,12 @@ class ImageEdit extends Component { } componentDidMount() { - const { attributes, setAttributes, noticeOperations } = this.props; + const { + attributes, + mediaUpload, + noticeOperations, + setAttributes, + } = this.props; const { id, url = '' } = attributes; if ( isTemporaryImage( id, url ) ) { @@ -165,6 +169,7 @@ class ImageEdit extends Component { onUploadError( message ) { const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); noticeOperations.createErrorNotice( message ); this.setState( { isEditing: true, @@ -403,7 +408,7 @@ class ImageEdit extends Component { const src = isExternal ? url : undefined; const labels = { title: ! url ? __( 'Image' ) : __( 'Edit image' ), - instructions: __( 'Drag an image to upload, select a file from your library or add one from an URL.' ), + instructions: __( 'Upload an image, pick one from your media library, or add one with a URL.' ), }; const mediaPreview = ( !! url && {; + return ; } - return ; + return ; } render() { @@ -273,13 +274,6 @@ class ImageEdit extends React.Component { const getImageComponent = ( openMediaOptions, getMediaOptions ) => ( setAttributes( { caption: newCaption } ) } - onFocus={ this.onFocusCaption } + unstableOnFocus={ this.onFocusCaption } onBlur={ this.props.onBlur } // always assign onBlur as props isSelected={ this.state.isCaptionSelected } + __unstableMobileNoFocusOnMount fontSize={ 14 } underlineColorAndroid="transparent" textAlign={ 'center' } diff --git a/packages/block-library/src/image/icon-retry.js b/packages/block-library/src/image/icon-retry.js deleted file mode 100644 index 66baaa93dcfb38..00000000000000 --- a/packages/block-library/src/image/icon-retry.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * WordPress dependencies - */ -import { Path, SVG } from '@wordpress/components'; - -function svg( props ) { - return ; -} - -export default svg; diff --git a/packages/block-library/src/image/icon-retry.native.js b/packages/block-library/src/image/icon-retry.native.js new file mode 100644 index 00000000000000..bdd40f69efd64a --- /dev/null +++ b/packages/block-library/src/image/icon-retry.native.js @@ -0,0 +1,6 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; + +export default ; diff --git a/packages/block-library/src/image/icon.js b/packages/block-library/src/image/icon.js index ad8a11857013a0..b029bab8fbe98a 100644 --- a/packages/block-library/src/image/icon.js +++ b/packages/block-library/src/image/icon.js @@ -3,8 +3,4 @@ */ import { Path, SVG } from '@wordpress/components'; -function svg( props ) { - return ; -} - -export default svg; +export default ; diff --git a/packages/block-library/src/image/index.native.js b/packages/block-library/src/image/index.native.js new file mode 100644 index 00000000000000..d7e0d17b53a9cb --- /dev/null +++ b/packages/block-library/src/image/index.native.js @@ -0,0 +1,30 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { settings as webSettings } from './index.js'; + +export { metadata, name } from './index.js'; + +export const settings = { + ...webSettings, + __experimentalGetAccessibilityLabel( attributes ) { + const { caption, alt, url } = attributes; + + if ( ! url ) { + return __( 'Empty' ); + } + + if ( ! alt ) { + return caption || ''; + } + + // This is intended to be read by a screen reader. + // A period simply means a pause, no need to translate it. + return alt + ( caption ? '. ' + caption : '' ); + }, +}; diff --git a/packages/block-library/src/image/styles.native.scss b/packages/block-library/src/image/styles.native.scss index 4250ff170e328b..81578bd734ba3e 100644 --- a/packages/block-library/src/image/styles.native.scss +++ b/packages/block-library/src/image/styles.native.scss @@ -36,8 +36,12 @@ .iconRetry { fill: #fff; + width: 100%; + height: 100%; } .icon { fill: $gray-dark; + width: 100%; + height: 100%; } diff --git a/packages/block-library/src/image/test/media-upload-progress.native.js b/packages/block-library/src/image/test/media-upload-progress.native.js new file mode 100644 index 00000000000000..042f571aeefe58 --- /dev/null +++ b/packages/block-library/src/image/test/media-upload-progress.native.js @@ -0,0 +1,162 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; +import { + sendMediaUpload, +} from 'react-native-gutenberg-bridge'; + +/** + * Internal dependencies + */ +import { + MediaUploadProgress, + MEDIA_UPLOAD_STATE_UPLOADING, + MEDIA_UPLOAD_STATE_SUCCEEDED, + MEDIA_UPLOAD_STATE_FAILED, + MEDIA_UPLOAD_STATE_RESET, +} from '../media-upload-progress'; + +jest.mock( 'react-native-gutenberg-bridge', () => { + const callUploadCallback = ( payload ) => { + this.uploadCallBack( payload ); + }; + const subscribeMediaUpload = ( callback ) => { + this.uploadCallBack = callback; + }; + return { + subscribeMediaUpload, + sendMediaUpload: callUploadCallback, + }; +} ); + +const MEDIA_ID = 123; + +describe( 'MediaUploadProgress component', () => { + it( 'renders without crashing', () => { + const wrapper = shallow( + {} } /> + ); + expect( wrapper ).toBeTruthy(); + } ); + + it( 'listens media upload progress', () => { + const progress = 10; + const payload = { state: MEDIA_UPLOAD_STATE_UPLOADING, mediaId: MEDIA_ID, progress }; + + const onUpdateMediaProgress = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payload ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + expect( wrapper.instance().state.isUploadInProgress ).toEqual( true ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( false ); + expect( onUpdateMediaProgress ).toHaveBeenCalledTimes( 1 ); + expect( onUpdateMediaProgress ).toHaveBeenCalledWith( payload ); + } ); + + it( 'does not get affected by unrelated media uploads', () => { + const payload = { state: MEDIA_UPLOAD_STATE_UPLOADING, mediaId: 1, progress: 20 }; + const onUpdateMediaProgress = jest.fn(); + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payload ); + + expect( wrapper.instance().state.progress ).toEqual( 0 ); + expect( onUpdateMediaProgress ).toHaveBeenCalledTimes( 0 ); + } ); + + it( 'listens media upload success', () => { + const progress = 10; + const payloadSuccess = { state: MEDIA_UPLOAD_STATE_SUCCEEDED, mediaId: MEDIA_ID }; + const payloadUploading = { state: MEDIA_UPLOAD_STATE_UPLOADING, mediaId: MEDIA_ID, progress }; + + const onFinishMediaUploadWithSuccess = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadSuccess ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( onFinishMediaUploadWithSuccess ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaUploadWithSuccess ).toHaveBeenCalledWith( payloadSuccess ); + } ); + + it( 'listens media upload fail', () => { + const progress = 10; + const payloadFail = { state: MEDIA_UPLOAD_STATE_FAILED, mediaId: MEDIA_ID }; + const payloadUploading = { state: MEDIA_UPLOAD_STATE_UPLOADING, mediaId: MEDIA_ID, progress }; + + const onFinishMediaUploadWithFailure = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadFail ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( true ); + expect( onFinishMediaUploadWithFailure ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaUploadWithFailure ).toHaveBeenCalledWith( payloadFail ); + } ); + + it( 'listens media upload reset', () => { + const progress = 10; + const payloadReset = { state: MEDIA_UPLOAD_STATE_RESET, mediaId: MEDIA_ID }; + const payloadUploading = { state: MEDIA_UPLOAD_STATE_UPLOADING, mediaId: MEDIA_ID, progress }; + + const onMediaUploadStateReset = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadReset ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( false ); + expect( onMediaUploadStateReset ).toHaveBeenCalledTimes( 1 ); + expect( onMediaUploadStateReset ).toHaveBeenCalledWith( payloadReset ); + } ); +} ); diff --git a/packages/block-library/src/latest-comments/edit.js b/packages/block-library/src/latest-comments/edit.js index 61e7ec3e3aaabb..029778e24f2c59 100644 --- a/packages/block-library/src/latest-comments/edit.js +++ b/packages/block-library/src/latest-comments/edit.js @@ -8,7 +8,7 @@ import { RangeControl, ToggleControl, } from '@wordpress/components'; -import { ServerSideRender } from '@wordpress/editor'; +import ServerSideRender from '@wordpress/server-side-render'; import { Component } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; diff --git a/packages/block-library/src/legacy-widget/edit/index.js b/packages/block-library/src/legacy-widget/edit/index.js index 4b645318eb2277..31f36c2f7fac94 100644 --- a/packages/block-library/src/legacy-widget/edit/index.js +++ b/packages/block-library/src/legacy-widget/edit/index.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { map } from 'lodash'; - /** * WordPress dependencies */ @@ -11,23 +6,21 @@ import { Button, IconButton, PanelBody, - Placeholder, - SelectControl, Toolbar, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { withSelect } from '@wordpress/data'; import { BlockControls, - BlockIcon, InspectorControls, } from '@wordpress/block-editor'; -import { ServerSideRender } from '@wordpress/editor'; +import ServerSideRender from '@wordpress/server-side-render'; /** * Internal dependencies */ import LegacyWidgetEditHandler from './handler'; +import LegacyWidgetPlaceholder from './placeholder'; class LegacyWidgetEdit extends Component { constructor() { @@ -51,41 +44,17 @@ class LegacyWidgetEdit extends Component { const { identifier, isCallbackWidget } = attributes; const widgetObject = identifier && availableLegacyWidgets[ identifier ]; if ( ! widgetObject ) { - let placeholderContent; - - if ( ! hasPermissionsToManageWidgets ) { - placeholderContent = __( 'You don\'t have permissions to use widgets on this site.' ); - } else if ( availableLegacyWidgets.length === 0 ) { - placeholderContent = __( 'There are no widgets available.' ); - } else { - placeholderContent = ( - setAttributes( { - instance: {}, - identifier: value, - isCallbackWidget: availableLegacyWidgets[ value ].isCallbackWidget, - } ) } - options={ [ { value: 'none', label: 'Select widget' } ].concat( - map( availableLegacyWidgets, ( widget, key ) => { - return { - value: key, - label: widget.name, - }; - } ) - ) } - /> - ); - } - return ( - } - label={ __( 'Legacy Widget' ) } - > - { placeholderContent } - + setAttributes( { + instance: {}, + identifier: newWidget, + isCallbackWidget: availableLegacyWidgets[ newWidget ].isCallbackWidget, + } ) } + /> ); } @@ -109,12 +78,13 @@ class LegacyWidgetEdit extends Component { <> - - + { ! widgetObject.isHidden && ( + + ) } { ! isCallbackWidget && ( <>