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 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( '' + tag + '>$', 'gim' );
return html.replace( openingTagRegexp, '' ).replace( closingTagRegexp, '' );
}
+ removeTag( tag, html ) {
+ const openingTagRegexp = RegExp( '<' + tag + '>', 'gim' );
+ const closingTagRegexp = RegExp( '' + tag + '>', '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 && (
<>
pickBy(
+ availableLegacyWidgets,
+ ( { isHidden } ) => ! isHidden
+ ),
+ [ availableLegacyWidgets ]
+ );
+ let placeholderContent;
+ if ( ! hasPermissionsToManageWidgets ) {
+ placeholderContent = __( 'You don\'t have permissions to use widgets on this site.' );
+ }
+ if ( isEmpty( visibleLegacyWidgets ) ) {
+ placeholderContent = __( 'There are no widgets available.' );
+ }
+ placeholderContent = (
+ {
+ return {
+ value: key,
+ label: widget.name,
+ };
+ } )
+ ) }
+ />
+ );
+ return (
+ }
+ label={ __( 'Legacy Widget' ) }
+ >
+ { placeholderContent }
+
+ );
+}
diff --git a/packages/block-library/src/media-text/editor.scss b/packages/block-library/src/media-text/editor.scss
index fe6a552526971b..3d0ad0361bd947 100644
--- a/packages/block-library/src/media-text/editor.scss
+++ b/packages/block-library/src/media-text/editor.scss
@@ -3,7 +3,6 @@
"media-text-media media-text-content"
"resizer resizer";
align-items: center;
- word-break: break-all;
}
.wp-block-media-text.has-media-on-the-right {
diff --git a/packages/block-library/src/media-text/media-container.js b/packages/block-library/src/media-text/media-container.js
index 9afc28c58b3a7c..74d2062d524d9c 100644
--- a/packages/block-library/src/media-text/media-container.js
+++ b/packages/block-library/src/media-text/media-container.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { IconButton, ResizableBox, Toolbar } from '@wordpress/components';
+import { IconButton, ResizableBox, Toolbar, withNotices } from '@wordpress/components';
import {
BlockControls,
BlockIcon,
@@ -31,6 +31,17 @@ export function imageFillStyles( url, focalPoint ) {
}
class MediaContainer extends Component {
+ constructor() {
+ super( ...arguments );
+ this.onUploadError = this.onUploadError.bind( this );
+ }
+
+ onUploadError( message ) {
+ const { noticeOperations } = this.props;
+ noticeOperations.removeAllNotices();
+ noticeOperations.createErrorNotice( message );
+ }
+
renderToolbarEditButton() {
const { mediaId, onSelectMedia } = this.props;
return (
@@ -80,7 +91,7 @@ class MediaContainer extends Component {
}
renderPlaceholder() {
- const { onSelectMedia, className } = this.props;
+ const { onSelectMedia, className, noticeUI } = this.props;
return (
}
@@ -91,6 +102,8 @@ class MediaContainer extends Component {
onSelect={ onSelectMedia }
accept="image/*,video/*"
allowedTypes={ ALLOWED_MEDIA_TYPES }
+ notices={ noticeUI }
+ onError={ this.onUploadError }
/>
);
}
@@ -137,4 +150,4 @@ class MediaContainer extends Component {
}
}
-export default MediaContainer;
+export default withNotices( MediaContainer );
diff --git a/packages/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js
index 970975aa711418..838c64d88b56c4 100644
--- a/packages/block-library/src/missing/edit.native.js
+++ b/packages/block-library/src/missing/edit.native.js
@@ -10,7 +10,7 @@ import { Icon } from '@wordpress/components';
import { coreBlocks } from '@wordpress/block-library';
import { normalizeIconObject } from '@wordpress/blocks';
import { Component } from '@wordpress/element';
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
@@ -25,20 +25,7 @@ export default class UnsupportedBlockEdit extends Component {
const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins';
return (
-
+
{ title }
diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js
index e6d519c26b3042..8b369284cb5962 100644
--- a/packages/block-library/src/more/edit.native.js
+++ b/packages/block-library/src/more/edit.native.js
@@ -2,85 +2,43 @@
* External dependencies
*/
import { View } from 'react-native';
+import Hr from 'react-native-hr';
/**
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
-import {
- getDefaultBlockName,
- createBlock,
-} from '@wordpress/blocks';
/**
* Internal dependencies
*/
-import { PlainText } from '@wordpress/block-editor';
import styles from './editor.scss';
export default class MoreEdit extends Component {
constructor() {
super( ...arguments );
- this.onChangeInput = this.onChangeInput.bind( this );
this.state = {
defaultText: __( 'Read more' ),
};
}
- onChangeInput( newValue ) {
- // Detect Enter.key and add new empty block after.
- // Note: This is detected after the fact, and the newline could be visible on the block
- // for a very small time. This is OK for the alpha, we will revisit the logic later.
- // See https://github.com/wordpress-mobile/gutenberg-mobile/issues/324
- if ( newValue.indexOf( '\n' ) !== -1 ) {
- const { insertBlocksAfter } = this.props;
- insertBlocksAfter( [ createBlock( getDefaultBlockName() ) ] );
- return;
- }
- // Set defaultText to an empty string, allowing the user to clear/replace the input field's text
- this.setState( {
- defaultText: '',
- } );
- const value = newValue.length === 0 ? undefined : newValue;
- this.props.setAttributes( { customText: value } );
- }
-
- renderLine() {
- return ;
- }
-
- renderText() {
- const { attributes, onFocus, onBlur } = this.props;
- const { customText } = attributes;
+ render() {
+ const { customText } = this.props.attributes;
const { defaultText } = this.state;
- const value = customText !== undefined ? customText : defaultText;
+ const content = customText || defaultText;
return (
-
);
}
-
- render() {
- return (
-
- { this.renderLine() }
- { this.renderText() }
- { this.renderLine() }
-
- );
- }
}
diff --git a/packages/block-library/src/more/editor.native.scss b/packages/block-library/src/more/editor.native.scss
index beb5ef423776ff..eb4a1d60d9431f 100644
--- a/packages/block-library/src/more/editor.native.scss
+++ b/packages/block-library/src/more/editor.native.scss
@@ -1,23 +1,13 @@
// @format
-.block-library-more__container {
- align-items: center;
- padding: 4px;
- flex-direction: row;
-}
-
.block-library-more__line {
- background-color: #555d66;
+ background-color: $gray-lighten-20;
height: 2;
- flex: 1;
}
.block-library-more__text {
+ color: $gray;
text-decoration-style: solid;
- flex: 0;
- width: 200;
- text-align: center;
- margin-left: 15;
- margin-right: 15;
- margin-bottom: 5;
+ text-transform: uppercase;
}
+
diff --git a/packages/block-library/src/more/index.native.js b/packages/block-library/src/more/index.native.js
new file mode 100644
index 00000000000000..1525b6694abe52
--- /dev/null
+++ b/packages/block-library/src/more/index.native.js
@@ -0,0 +1,13 @@
+/**
+ * Internal dependencies
+ */
+import { settings as webSettings } from './index.js';
+
+export { metadata, name } from './index.js';
+
+export const settings = {
+ ...webSettings,
+ __experimentalGetAccessibilityLabel( attributes ) {
+ return attributes.customText;
+ },
+};
diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js
index ae887204538769..e3aa69b15e5e41 100644
--- a/packages/block-library/src/nextpage/edit.native.js
+++ b/packages/block-library/src/nextpage/edit.native.js
@@ -33,6 +33,8 @@ export default function NextPageEdit( { attributes, isSelected, onFocus } ) {
onAccessibilityTap={ onFocus }
>
diff --git a/packages/block-library/src/nextpage/editor.native.scss b/packages/block-library/src/nextpage/editor.native.scss
index ae6f1f3788df3b..869851fdd37c63 100644
--- a/packages/block-library/src/nextpage/editor.native.scss
+++ b/packages/block-library/src/nextpage/editor.native.scss
@@ -7,7 +7,6 @@
.block-library-nextpage__text {
color: $gray;
- font-family: $default-regular-font;
text-decoration-style: solid;
text-transform: uppercase;
}
diff --git a/packages/block-library/src/paragraph/edit.native.js b/packages/block-library/src/paragraph/edit.native.js
index a97885c213c3a0..6dbcaf8a1459dd 100644
--- a/packages/block-library/src/paragraph/edit.native.js
+++ b/packages/block-library/src/paragraph/edit.native.js
@@ -2,16 +2,14 @@
* External dependencies
*/
import { View } from 'react-native';
-import { isEmpty } from 'lodash';
/**
* WordPress dependencies
*/
-import { __, sprintf } from '@wordpress/i18n';
+import { __ } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { createBlock } from '@wordpress/blocks';
import { RichText } from '@wordpress/block-editor';
-import { create } from '@wordpress/rich-text';
/**
* Internal dependencies
@@ -39,14 +37,6 @@ class ParagraphEdit extends Component {
) ) );
}
- plainTextContent( html ) {
- const result = create( { html } );
- if ( result ) {
- return result.text;
- }
- return '';
- }
-
render() {
const {
attributes,
@@ -62,27 +52,11 @@ class ParagraphEdit extends Component {
} = attributes;
return (
-
+
{
diff --git a/packages/block-library/src/paragraph/index.native.js b/packages/block-library/src/paragraph/index.native.js
new file mode 100644
index 00000000000000..4de4948f251807
--- /dev/null
+++ b/packages/block-library/src/paragraph/index.native.js
@@ -0,0 +1,28 @@
+/**
+ * External dependencies
+ */
+import { isEmpty } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { __ } 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 } = attributes;
+
+ const plainTextContent = ( html ) => create( { html } ).text || '';
+
+ return isEmpty( content ) ? __( 'Empty' ) : plainTextContent( content );
+ },
+};
diff --git a/packages/block-library/src/paragraph/test/edit.native.js b/packages/block-library/src/paragraph/test/edit.native.js
new file mode 100644
index 00000000000000..3a7fea4e28e35d
--- /dev/null
+++ b/packages/block-library/src/paragraph/test/edit.native.js
@@ -0,0 +1,32 @@
+/**
+ * External dependencies
+ */
+import { shallow } from 'enzyme';
+
+/**
+ * Internal dependencies
+ */
+import Paragraph from '../edit';
+
+/**
+ * WordPress dependencies
+ */
+jest.mock( '@wordpress/blocks' );
+
+const getTestComponentWithContent = ( content ) => {
+ return shallow(
+
+ );
+};
+
+describe( 'Paragraph block', () => {
+ it( 'renders without crashing', () => {
+ const component = getTestComponentWithContent( '' );
+ expect( component.exists() ).toBe( true );
+ } );
+} );
diff --git a/packages/block-library/src/quote/edit.js b/packages/block-library/src/quote/edit.js
index 8955b78588bc3f..079db78e86493e 100644
--- a/packages/block-library/src/quote/edit.js
+++ b/packages/block-library/src/quote/edit.js
@@ -48,6 +48,7 @@ export default function QuoteEdit( { attributes, setAttributes, isSelected, merg
citation: nextCitation,
} )
}
+ __unstableMobileNoFocusOnMount
placeholder={
// translators: placeholder text used for the citation
__( 'Write citation…' )
diff --git a/packages/block-library/src/rss/index.php b/packages/block-library/src/rss/index.php
index 92406d09b6b1d6..1bf217be3a9b59 100644
--- a/packages/block-library/src/rss/index.php
+++ b/packages/block-library/src/rss/index.php
@@ -109,7 +109,8 @@ function render_block_core_rss( $attributes ) {
* Registers the `core/rss` block on server.
*/
function register_block_core_rss() {
- register_block_type( 'core/rss',
+ register_block_type(
+ 'core/rss',
array(
'attributes' => array(
'align' => array(
diff --git a/packages/block-library/src/spacer/edit.js b/packages/block-library/src/spacer/edit.js
index fdb1abcf9d2395..f6a3091eb57e26 100644
--- a/packages/block-library/src/spacer/edit.js
+++ b/packages/block-library/src/spacer/edit.js
@@ -12,7 +12,7 @@ import { InspectorControls } from '@wordpress/block-editor';
import { BaseControl, PanelBody, ResizableBox } from '@wordpress/components';
import { withInstanceId } from '@wordpress/compose';
-const SpacerEdit = ( { attributes, isSelected, setAttributes, toggleSelection, instanceId } ) => {
+const SpacerEdit = ( { attributes, isSelected, setAttributes, instanceId } ) => {
const { height } = attributes;
const id = `block-spacer-height-input-${ instanceId }`;
const [ inputHeightValue, setInputHeightValue ] = useState( height );
@@ -44,10 +44,6 @@ const SpacerEdit = ( { attributes, isSelected, setAttributes, toggleSelection, i
height: spacerHeight,
} );
setInputHeightValue( spacerHeight );
- toggleSelection( true );
- } }
- onResizeStart={ () => {
- toggleSelection( false );
} }
/>
diff --git a/packages/block-library/src/spacer/editor.scss b/packages/block-library/src/spacer/editor.scss
index 77fba2118222a7..b20657931c97ef 100644
--- a/packages/block-library/src/spacer/editor.scss
+++ b/packages/block-library/src/spacer/editor.scss
@@ -3,5 +3,6 @@
}
.block-library-spacer__resize-container {
+ clear: both;
margin-bottom: $default-block-margin;
}
diff --git a/packages/block-library/src/spacer/style.scss b/packages/block-library/src/spacer/style.scss
new file mode 100644
index 00000000000000..e6b5ee9d5085bd
--- /dev/null
+++ b/packages/block-library/src/spacer/style.scss
@@ -0,0 +1,3 @@
+.wp-block-spacer {
+ clear: both;
+}
diff --git a/packages/block-library/src/style.scss b/packages/block-library/src/style.scss
index 73325d5d9167c1..f9a2909ff360a8 100644
--- a/packages/block-library/src/style.scss
+++ b/packages/block-library/src/style.scss
@@ -1,6 +1,4 @@
@import "./audio/style.scss";
-@import "./block/edit-panel/style.scss";
-@import "./block/indicator/style.scss";
@import "./button/style.scss";
@import "./calendar/style.scss";
@import "./categories/style.scss";
@@ -19,6 +17,7 @@
@import "./rss/style.scss";
@import "./search/style.scss";
@import "./separator/style.scss";
+@import "./spacer/style.scss";
@import "./subhead/style.scss";
@import "./table/style.scss";
@import "./text-columns/style.scss";
@@ -148,6 +147,18 @@
font-size: 42px;
}
+// Text alignments.
+.has-text-align-center {
+ text-align: center;
+}
+
+.has-text-align-left {
+ text-align: left;
+}
+
+.has-text-align-right {
+ text-align: right;
+}
/**
* Vanilla Block Styles
diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js
index 79af531cd2929b..bdc6cfc253cebd 100644
--- a/packages/block-library/src/table/edit.js
+++ b/packages/block-library/src/table/edit.js
@@ -13,6 +13,7 @@ import {
RichText,
PanelColorSettings,
createCustomColorsHOC,
+ BlockIcon,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import {
@@ -22,6 +23,7 @@ import {
Button,
Toolbar,
DropdownMenu,
+ Placeholder,
} from '@wordpress/components';
/**
@@ -36,6 +38,7 @@ import {
deleteColumn,
toggleSection,
} from './state';
+import icon from './icon';
const BACKGROUND_COLORS = [
{
@@ -418,23 +421,32 @@ export class TableEdit extends Component {
if ( isEmpty ) {
return (
-
+ }
+ instructions={ __( 'Insert a table for sharing data.' ) }
+ isColumnLayout
+ >
+
+
);
}
diff --git a/packages/block-library/src/table/editor.scss b/packages/block-library/src/table/editor.scss
index 52f6a60e545d15..46ae863c31a0d9 100644
--- a/packages/block-library/src/table/editor.scss
+++ b/packages/block-library/src/table/editor.scss
@@ -40,4 +40,16 @@
&__cell-content {
padding: 0.5em;
}
+ // Extra specificity to override default Placeholder styles.
+ &__placeholder-form.wp-block-table__placeholder-form {
+ text-align: left;
+ align-items: center;
+ }
+ &__placeholder-input {
+ width: 100px;
+ }
+ &__placeholder-button {
+ min-width: 100px;
+ justify-content: center;
+ }
}
diff --git a/packages/block-library/src/tag-cloud/index.php b/packages/block-library/src/tag-cloud/index.php
index 6786e076b80f1d..cf9c4d24537dbd 100644
--- a/packages/block-library/src/tag-cloud/index.php
+++ b/packages/block-library/src/tag-cloud/index.php
@@ -30,7 +30,14 @@ function render_block_core_tag_cloud( $attributes ) {
$tag_cloud = wp_tag_cloud( $args );
if ( ! $tag_cloud ) {
- $tag_cloud = esc_html( __( 'No terms to show.' ) );
+ $labels = get_taxonomy_labels( get_taxonomy( $attributes['taxonomy'] ) );
+ $tag_cloud = esc_html(
+ sprintf(
+ /* translators: %s: taxonomy name */
+ __( 'Your site doesn’t have any %s, so there’s nothing to display here at the moment.' ),
+ strtolower( $labels->name )
+ )
+ );
}
return sprintf(
diff --git a/packages/block-library/src/video/edit.js b/packages/block-library/src/video/edit.js
index 0f2939fe1f6a5e..9931508d2c6c3f 100644
--- a/packages/block-library/src/video/edit.js
+++ b/packages/block-library/src/video/edit.js
@@ -22,7 +22,6 @@ import {
MediaUploadCheck,
RichText,
} from '@wordpress/block-editor';
-import { mediaUpload } from '@wordpress/editor';
import { Component, createRef } from '@wordpress/element';
import {
__,
@@ -32,6 +31,9 @@ import {
compose,
withInstanceId,
} from '@wordpress/compose';
+import {
+ withSelect,
+} from '@wordpress/data';
/**
* Internal dependencies
@@ -57,10 +59,16 @@ class VideoEdit extends Component {
this.onSelectURL = this.onSelectURL.bind( this );
this.onSelectPoster = this.onSelectPoster.bind( this );
this.onRemovePoster = this.onRemovePoster.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 ) ) {
const file = getBlobByURL( src );
@@ -126,6 +134,12 @@ class VideoEdit extends Component {
this.posterImageButton.current.focus();
}
+ onUploadError( message ) {
+ const { noticeOperations } = this.props;
+ noticeOperations.removeAllNotices();
+ noticeOperations.createErrorNotice( message );
+ }
+
getAutoplayHelp( checked ) {
return checked ? __( 'Note: Autoplaying videos may cause usability issues for some visitors.' ) : null;
}
@@ -146,7 +160,6 @@ class VideoEdit extends Component {
className,
instanceId,
isSelected,
- noticeOperations,
noticeUI,
setAttributes,
} = this.props;
@@ -179,7 +192,7 @@ class VideoEdit extends Component {
allowedTypes={ ALLOWED_MEDIA_TYPES }
value={ this.props.attributes }
notices={ noticeUI }
- onError={ noticeOperations.createErrorNotice }
+ onError={ this.onUploadError }
/>
);
}
@@ -306,6 +319,13 @@ class VideoEdit extends Component {
}
export default compose( [
+ withSelect( ( select ) => {
+ const { getSettings } = select( 'core/block-editor' );
+ const { __experimentalMediaUpload } = getSettings();
+ return {
+ mediaUpload: __experimentalMediaUpload,
+ };
+ } ),
withNotices,
withInstanceId,
] )( VideoEdit );
diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js
index 17b6c416f016d9..1b7fc518313440 100644
--- a/packages/block-library/src/video/edit.native.js
+++ b/packages/block-library/src/video/edit.native.js
@@ -17,6 +17,7 @@ import {
* WordPress dependencies
*/
import {
+ Icon,
Toolbar,
ToolbarButton,
} from '@wordpress/components';
@@ -130,10 +131,10 @@ class VideoEdit extends React.Component {
getIcon( isRetryIcon, isUploadInProgress ) {
if ( isRetryIcon ) {
- return ;
+ return ;
}
- return ;
+ return ;
}
render() {
@@ -179,11 +180,11 @@ class VideoEdit extends React.Component {
{ toolbarEditButton }
- ( null ) }
- />
+ /> }
{ showVideo && isURL( src ) &&
-
+
+
+
}
{ ! showVideo &&
-
+
{ videoContainerHeight > 0 && iconContainer }
{ isUploadFailed && { retryMessage } }
diff --git a/packages/block-library/src/video/icon-retry.js b/packages/block-library/src/video/icon-retry.js
deleted file mode 100644
index 82a4be4805c455..00000000000000
--- a/packages/block-library/src/video/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/video/icon-retry.native.js b/packages/block-library/src/video/icon-retry.native.js
new file mode 100644
index 00000000000000..d56dff2cac124e
--- /dev/null
+++ b/packages/block-library/src/video/icon-retry.native.js
@@ -0,0 +1,7 @@
+/**
+ * WordPress dependencies
+ */
+import { Path, SVG } from '@wordpress/components';
+
+export default ;
+
diff --git a/packages/block-library/src/video/icon.js b/packages/block-library/src/video/icon.js
index 9cf0a1b9989874..8c43e80279f78f 100644
--- a/packages/block-library/src/video/icon.js
+++ b/packages/block-library/src/video/icon.js
@@ -3,8 +3,5 @@
*/
import { Path, SVG } from '@wordpress/components';
-function svg( props ) {
- return ;
-}
+export default ;
-export default svg;
diff --git a/packages/block-library/src/video/style.ios.scss b/packages/block-library/src/video/style.ios.scss
deleted file mode 100644
index d13a8ad1801291..00000000000000
--- a/packages/block-library/src/video/style.ios.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-// @format
-
-@import "./style.native.scss";
-
-.video {
- background-color: #000;
-}
-
diff --git a/packages/block-library/src/video/style.native.scss b/packages/block-library/src/video/style.native.scss
index 9825ae09babe83..e7b2a3014b579e 100644
--- a/packages/block-library/src/video/style.native.scss
+++ b/packages/block-library/src/video/style.native.scss
@@ -4,6 +4,11 @@
width: 100%;
}
+.videoContainer {
+ flex: 1;
+ background-color: #000;
+}
+
.placeholder {
flex: 1;
justify-content: center;
@@ -54,8 +59,12 @@
.icon {
fill: $gray-dark;
+ width: 100%;
+ height: 100%;
}
.iconUploading {
fill: $gray-lighten-20;
+ width: 100%;
+ height: 100%;
}
diff --git a/packages/block-library/src/video/video-player.android.js b/packages/block-library/src/video/video-player.android.js
deleted file mode 100644
index efe91ceda019fa..00000000000000
--- a/packages/block-library/src/video/video-player.android.js
+++ /dev/null
@@ -1,27 +0,0 @@
-/**
- * External dependencies
- */
-import { View } from 'react-native';
-import { default as VideoPlayer } from 'react-native-video';
-
-/**
- * Internal dependencies
- */
-import styles from './video-player.scss';
-
-const Video = ( props ) => {
- const { isSelected, ...videoProps } = props;
-
- return (
-
-
-
- );
-};
-
-export default Video;
diff --git a/packages/block-library/src/video/video-player.ios.js b/packages/block-library/src/video/video-player.ios.js
deleted file mode 100644
index 0713afc404b1a3..00000000000000
--- a/packages/block-library/src/video/video-player.ios.js
+++ /dev/null
@@ -1,73 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { Component } from '@wordpress/element';
-import { Dashicon } from '@wordpress/components';
-
-/**
- * External dependencies
- */
-import { View, TouchableOpacity } from 'react-native';
-import { default as VideoPlayer } from 'react-native-video';
-
-/**
- * Internal dependencies
- */
-import styles from './video-player.scss';
-
-class Video extends Component {
- constructor() {
- super( ...arguments );
- this.state = {
- isLoaded: false,
- };
- this.onPressPlay = this.onPressPlay.bind( this );
- this.onLoad = this.onLoad.bind( this );
- this.onLoadStart = this.onLoadStart.bind( this );
- }
-
- onLoad() {
- this.setState( { isLoaded: true } );
- }
-
- onLoadStart() {
- this.setState( { isLoaded: false } );
- }
-
- onPressPlay() {
- if ( this.player ) {
- this.player.presentFullscreenPlayer();
- }
- }
-
- render() {
- const { isSelected, style } = this.props;
- const { isLoaded } = this.state;
-
- return (
-
- {
- this.player = ref;
- } }
- // Using built-in player controls is messing up the layout on iOS.
- // So we are setting controls=false and adding a play button that
- // will trigger presentFullscreenPlayer()
- controls={ false }
- onLoad={ this.onLoad }
- onLoadStart={ this.onLoadStart }
- />
- { isLoaded &&
-
-
-
-
-
- }
-
- );
- }
-}
-
-export default Video;
diff --git a/packages/block-library/src/video/video-player.native.js b/packages/block-library/src/video/video-player.native.js
new file mode 100644
index 00000000000000..5b67d4673b4192
--- /dev/null
+++ b/packages/block-library/src/video/video-player.native.js
@@ -0,0 +1,106 @@
+/**
+ * WordPress dependencies
+ */
+import { Component } from '@wordpress/element';
+import { Dashicon } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+
+/**
+ * External dependencies
+ */
+import { View, TouchableOpacity, Platform, Linking, Alert } from 'react-native';
+import { default as VideoPlayer } from 'react-native-video';
+
+/**
+ * Internal dependencies
+ */
+import styles from './video-player.scss';
+
+class Video extends Component {
+ constructor() {
+ super( ...arguments );
+ this.isIOS = Platform.OS === 'ios';
+ this.state = {
+ isFullScreen: false,
+ videoContainerHeight: 0,
+ };
+ this.onPressPlay = this.onPressPlay.bind( this );
+ this.onVideoLayout = this.onVideoLayout.bind( this );
+ }
+
+ onVideoLayout( event ) {
+ const { height } = event.nativeEvent.layout;
+ if ( height !== this.state.videoContainerHeight ) {
+ this.setState( { videoContainerHeight: height } );
+ }
+ }
+
+ onPressPlay() {
+ if ( this.isIOS ) {
+ if ( this.player ) {
+ this.player.presentFullscreenPlayer();
+ }
+ } else {
+ const { source } = this.props;
+ if ( source && source.uri ) {
+ this.openURL( source.uri );
+ }
+ }
+ }
+
+ // Tries opening the URL outside of the app
+ openURL( url ) {
+ Linking.canOpenURL( url ).then( ( supported ) => {
+ if ( ! supported ) {
+ Alert.alert( __( 'Problem opening the video' ), __( 'No application can handle this request. Please install a Web browser.' ) );
+ window.console.warn( 'No application found that can open the video with URL: ' + url );
+ } else {
+ return Linking.openURL( url );
+ }
+ } ).catch( ( err ) => {
+ Alert.alert( __( 'Problem opening the video' ), __( 'An unknown error occurred. Please try again.' ) );
+ window.console.error( 'An error occurred while opening the video URL: ' + url, err );
+ } );
+ }
+
+ render() {
+ const { isSelected, style } = this.props;
+ const { isFullScreen, videoContainerHeight } = this.state;
+ const showPlayButton = videoContainerHeight > 0;
+
+ return (
+
+ {
+ this.player = ref;
+ } }
+ // Using built-in player controls is messing up the layout on iOS.
+ // So we are setting controls=false and adding a play button that
+ // will trigger presentFullscreenPlayer()
+ controls={ false }
+ ignoreSilentSwitch={ 'ignore' }
+ paused={ ! isFullScreen }
+ onLayout={ this.onVideoLayout }
+ onFullscreenPlayerWillPresent={ () => {
+ this.setState( { isFullScreen: true } );
+ } }
+ onFullscreenPlayerDidDismiss={ () => {
+ this.setState( { isFullScreen: false } );
+ } }
+ />
+ { showPlayButton &&
+ // If we add the play icon as a subview to VideoPlayer then react-native-video decides to show control buttons
+ // even if we set controls={ false }, so we are adding our play button as a sibling overlay view.
+
+
+
+
+
+ }
+
+ );
+ }
+}
+
+export default Video;
diff --git a/packages/block-library/src/video/video-player.native.scss b/packages/block-library/src/video/video-player.native.scss
index ec746de8288916..ae0dc6382f91f9 100644
--- a/packages/block-library/src/video/video-player.native.scss
+++ b/packages/block-library/src/video/video-player.native.scss
@@ -13,7 +13,7 @@ $play-icon-size: 50;
align-items: center;
align-self: center;
position: absolute;
- background-color: #e9eff3;
+ background-color: transparent;
opacity: 0.3;
}
diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md
index b13c58601e1e99..8f50c3899591da 100644
--- a/packages/blocks/CHANGELOG.md
+++ b/packages/blocks/CHANGELOG.md
@@ -3,6 +3,7 @@
### New Feature
- Added a default implementation for `save` setting in `registerBlockType` which saves no markup in the post content.
+- Added wildcard block transforms which allows for transforming all/any blocks in another block.
## 6.1.0 (2019-03-06)
diff --git a/packages/blocks/README.md b/packages/blocks/README.md
index 9bc199a54452f2..b7a8e5fd70c9dd 100644
--- a/packages/blocks/README.md
+++ b/packages/blocks/README.md
@@ -679,7 +679,7 @@ _Parameters_
_Returns_
-- `Array`: Array of blocks.
+- `?Array`: Array of blocks or null.
# **synchronizeBlocksWithTemplate**
diff --git a/packages/blocks/package.json b/packages/blocks/package.json
index a00b6b505ab298..72f5fc9e3af87d 100644
--- a/packages/blocks/package.json
+++ b/packages/blocks/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/blocks",
- "version": "6.3.0",
+ "version": "6.4.0",
"description": "Block API for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/blocks/src/api/factory.js b/packages/blocks/src/api/factory.js
index c7bd01f2c699c4..92074c35a6bafd 100644
--- a/packages/blocks/src/api/factory.js
+++ b/packages/blocks/src/api/factory.js
@@ -11,6 +11,7 @@ import {
filter,
first,
flatMap,
+ has,
uniq,
isFunction,
isEmpty,
@@ -117,26 +118,41 @@ const isPossibleTransformForSource = ( transform, direction, blocks ) => {
return false;
}
- // If multiple blocks are selected, only multi block transforms are allowed.
+ // If multiple blocks are selected, only multi block transforms
+ // or wildcard transforms are allowed.
const isMultiBlock = blocks.length > 1;
- const isValidForMultiBlocks = ! isMultiBlock || transform.isMultiBlock;
+ const firstBlockName = first( blocks ).name;
+ const isValidForMultiBlocks = isWildcardBlockTransform( transform ) || ! isMultiBlock || transform.isMultiBlock;
if ( ! isValidForMultiBlocks ) {
return false;
}
+ // Check non-wildcard transforms to ensure that transform is valid
+ // for a block selection of multiple blocks of different types
+ if ( ! isWildcardBlockTransform( transform ) && ! every( blocks, { name: firstBlockName } ) ) {
+ return false;
+ }
+
// Only consider 'block' type transforms as valid.
const isBlockType = transform.type === 'block';
if ( ! isBlockType ) {
return false;
}
- // Check if the transform's block name matches the source block only if this is a transform 'from'.
+ // Check if the transform's block name matches the source block (or is a wildcard)
+ // only if this is a transform 'from'.
const sourceBlock = first( blocks );
- const hasMatchingName = direction !== 'from' || transform.blocks.indexOf( sourceBlock.name ) !== -1;
+ const hasMatchingName = direction !== 'from' || transform.blocks.indexOf( sourceBlock.name ) !== -1 || isWildcardBlockTransform( transform );
if ( ! hasMatchingName ) {
return false;
}
+ // Don't allow single 'core/group' blocks to be transformed into
+ // a 'core/group' block.
+ if ( ! isMultiBlock && isContainerGroupBlock( sourceBlock.name ) && isContainerGroupBlock( transform.blockName ) ) {
+ return false;
+ }
+
// If the transform has a `isMatch` function specified, check that it returns true.
if ( isFunction( transform.isMatch ) ) {
const attributes = transform.isMultiBlock ? blocks.map( ( block ) => block.attributes ) : sourceBlock.attributes;
@@ -171,7 +187,9 @@ const getBlockTypesForPossibleFromTransforms = ( blocks ) => {
return !! findTransform(
fromTransforms,
- ( transform ) => isPossibleTransformForSource( transform, 'from', blocks )
+ ( transform ) => {
+ return isPossibleTransformForSource( transform, 'from', blocks );
+ }
);
},
);
@@ -199,7 +217,9 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => {
// filter all 'to' transforms to find those that are possible.
const possibleTransforms = filter(
transformsTo,
- ( transform ) => isPossibleTransformForSource( transform, 'to', blocks )
+ ( transform ) => {
+ return transform && isPossibleTransformForSource( transform, 'to', blocks );
+ }
);
// Build a list of block names using the possible 'to' transforms.
@@ -212,6 +232,45 @@ const getBlockTypesForPossibleToTransforms = ( blocks ) => {
return blockNames.map( ( name ) => getBlockType( name ) );
};
+/**
+ * Determines whether transform is a "block" type
+ * and if so whether it is a "wildcard" transform
+ * ie: targets "any" block type
+ *
+ * @param {Object} t the Block transform object
+ *
+ * @return {boolean} whether transform is a wildcard transform
+ */
+export const isWildcardBlockTransform = ( t ) => t && t.type === 'block' && Array.isArray( t.blocks ) && t.blocks.includes( '*' );
+
+/**
+ * Determines whether the given Block is the core Block which
+ * acts as a container Block for other Blocks as part of the
+ * Grouping mechanics
+ *
+ * @param {string} name the name of the Block to test against
+ *
+ * @return {boolean} whether or not the Block is the container Block type
+ */
+export const isContainerGroupBlock = ( name ) => name === 'core/group';
+
+/**
+ * Determines whether the provided Blocks are of the same type
+ * (eg: all `core/paragraph`).
+ *
+ * @param {Array} blocksArray the Block definitions
+ *
+ * @return {boolean} whether or not the given Blocks pass the criteria
+ */
+export const isBlockSelectionOfSameType = ( blocksArray = [] ) => {
+ if ( ! blocksArray.length ) {
+ return false;
+ }
+ const sourceName = blocksArray[ 0 ].name;
+
+ return every( blocksArray, [ 'name', sourceName ] );
+};
+
/**
* Returns an array of block types that the set of blocks received as argument
* can be transformed into.
@@ -225,12 +284,6 @@ export function getPossibleBlockTransformations( blocks ) {
return [];
}
- const sourceBlock = first( blocks );
- const isMultiBlock = blocks.length > 1;
- if ( isMultiBlock && ! every( blocks, { name: sourceBlock.name } ) ) {
- return [];
- }
-
const blockTypesForFromTransforms = getBlockTypesForPossibleFromTransforms( blocks );
const blockTypesForToTransforms = getBlockTypesForPossibleToTransforms( blocks );
@@ -313,7 +366,7 @@ export function getBlockTransforms( direction, blockTypeOrName ) {
* @param {Array|Object} blocks Blocks array or block object.
* @param {string} name Block name.
*
- * @return {Array} Array of blocks.
+ * @return {?Array} Array of blocks or null.
*/
export function switchToBlockType( blocks, name ) {
const blocksArray = castArray( blocks );
@@ -321,7 +374,10 @@ export function switchToBlockType( blocks, name ) {
const firstBlock = blocksArray[ 0 ];
const sourceName = firstBlock.name;
- if ( isMultiBlock && ! every( blocksArray, ( block ) => ( block.name === sourceName ) ) ) {
+ // Unless it's a `core/group` Block then for multi block selections
+ // check that all Blocks are of the same type otherwise
+ // we can't run a conversion
+ if ( ! isContainerGroupBlock( name ) && isMultiBlock && ! isBlockSelectionOfSameType( blocksArray ) ) {
return null;
}
@@ -329,14 +385,15 @@ export function switchToBlockType( blocks, name ) {
// transformation.
const transformationsFrom = getBlockTransforms( 'from', name );
const transformationsTo = getBlockTransforms( 'to', sourceName );
+
const transformation =
findTransform(
transformationsTo,
- ( t ) => t.type === 'block' && t.blocks.indexOf( name ) !== -1 && ( ! isMultiBlock || t.isMultiBlock )
+ ( t ) => t.type === 'block' && ( ( isWildcardBlockTransform( t ) ) || t.blocks.indexOf( name ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock )
) ||
findTransform(
transformationsFrom,
- ( t ) => t.type === 'block' && t.blocks.indexOf( sourceName ) !== -1 && ( ! isMultiBlock || t.isMultiBlock )
+ ( t ) => t.type === 'block' && ( ( isWildcardBlockTransform( t ) ) || t.blocks.indexOf( sourceName ) !== -1 ) && ( ! isMultiBlock || t.isMultiBlock )
);
// Stop if there is no valid transformation.
@@ -345,11 +402,18 @@ export function switchToBlockType( blocks, name ) {
}
let transformationResults;
+
if ( transformation.isMultiBlock ) {
- transformationResults = transformation.transform(
- blocksArray.map( ( currentBlock ) => currentBlock.attributes ),
- blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks )
- );
+ if ( has( transformation, '__experimentalConvert' ) ) {
+ transformationResults = transformation.__experimentalConvert( blocksArray );
+ } else {
+ transformationResults = transformation.transform(
+ blocksArray.map( ( currentBlock ) => currentBlock.attributes ),
+ blocksArray.map( ( currentBlock ) => currentBlock.innerBlocks ),
+ );
+ }
+ } else if ( has( transformation, '__experimentalConvert' ) ) {
+ transformationResults = transformation.__experimentalConvert( firstBlock );
} else {
transformationResults = transformation.transform( firstBlock.attributes, firstBlock.innerBlocks );
}
diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js
index 123be1e132a992..b99fccc531c217 100644
--- a/packages/blocks/src/api/index.native.js
+++ b/packages/blocks/src/api/index.native.js
@@ -21,8 +21,12 @@ export {
getUnregisteredTypeHandlerName,
getBlockType,
getBlockTypes,
+ getBlockSupport,
hasBlockSupport,
isReusableBlock,
+ getChildBlockNames,
+ hasChildBlocks,
+ hasChildBlocksWithInserterSupport,
setDefaultBlockName,
getDefaultBlockName,
} from './registration';
diff --git a/packages/blocks/src/api/test/factory.js b/packages/blocks/src/api/test/factory.js
index 11249b08d36ced..973964bfe05bae 100644
--- a/packages/blocks/src/api/test/factory.js
+++ b/packages/blocks/src/api/test/factory.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import deepFreeze from 'deep-freeze';
-import { noop } from 'lodash';
+import { noop, times } from 'lodash';
/**
* Internal dependencies
@@ -14,6 +14,9 @@ import {
switchToBlockType,
getBlockTransforms,
findTransform,
+ isWildcardBlockTransform,
+ isContainerGroupBlock,
+ isBlockSelectionOfSameType,
} from '../factory';
import {
getBlockType,
@@ -776,6 +779,81 @@ describe( 'block factory', () => {
expect( isMatch ).toHaveBeenCalledWith( [ { value: 'ribs' }, { value: 'halloumi' } ] );
} );
+
+ describe( 'wildcard block transforms', () => {
+ beforeEach( () => {
+ registerBlockType( 'core/group', {
+ attributes: {
+ value: {
+ type: 'string',
+ },
+ },
+ transforms: {
+ from: [ {
+ type: 'block',
+ blocks: [ '*' ],
+ transform: noop,
+ } ],
+ },
+ save: noop,
+ category: 'common',
+ title: 'A block that groups other blocks.',
+ } );
+ } );
+
+ it( 'should should show wildcard "from" transformation as available for multiple blocks of the same type', () => {
+ registerBlockType( 'core/text-block', defaultBlockSettings );
+ registerBlockType( 'core/image-block', defaultBlockSettings );
+
+ const textBlocks = times( 4, ( index ) => {
+ return createBlock( 'core/text-block', {
+ value: `textBlock${ index + 1 }`,
+ } );
+ } );
+
+ const availableBlocks = getPossibleBlockTransformations( textBlocks );
+
+ expect( availableBlocks ).toHaveLength( 1 );
+ expect( availableBlocks[ 0 ].name ).toBe( 'core/group' );
+ } );
+
+ it( 'should should show wildcard "from" transformation as available for multiple blocks of different types', () => {
+ registerBlockType( 'core/text-block', defaultBlockSettings );
+ registerBlockType( 'core/image-block', defaultBlockSettings );
+
+ const textBlocks = times( 2, ( index ) => {
+ return createBlock( 'core/text-block', {
+ value: `textBlock${ index + 1 }`,
+ } );
+ } );
+
+ const imageBlocks = times( 2, ( index ) => {
+ return createBlock( 'core/image-block', {
+ value: `imageBlock${ index + 1 }`,
+ } );
+ } );
+
+ const availableBlocks = getPossibleBlockTransformations( [ ...textBlocks, ...imageBlocks ] );
+
+ expect( availableBlocks ).toHaveLength( 1 );
+ expect( availableBlocks[ 0 ].name ).toBe( 'core/group' );
+ } );
+
+ it( 'should should show wildcard "from" transformation as available for single blocks', () => {
+ registerBlockType( 'core/text-block', defaultBlockSettings );
+
+ const blocks = times( 1, ( index ) => {
+ return createBlock( 'core/text-block', {
+ value: `textBlock${ index + 1 }`,
+ } );
+ } );
+
+ const availableBlocks = getPossibleBlockTransformations( blocks );
+
+ expect( availableBlocks ).toHaveLength( 1 );
+ expect( availableBlocks[ 0 ].name ).toBe( 'core/group' );
+ } );
+ } );
} );
describe( 'switchToBlockType()', () => {
@@ -1222,6 +1300,94 @@ describe( 'block factory', () => {
expect( transformedBlocks[ 1 ].innerBlocks ).toHaveLength( 1 );
expect( transformedBlocks[ 1 ].innerBlocks[ 0 ].attributes.value ).toBe( 'after1' );
} );
+
+ it( 'should pass entire block object(s) to the "__experimentalConvert" method if defined', () => {
+ registerBlockType( 'core/test-group-block', {
+ attributes: {
+ value: {
+ type: 'string',
+ },
+ },
+ transforms: {
+ from: [ {
+ type: 'block',
+ blocks: [ '*' ],
+ isMultiBlock: true,
+ __experimentalConvert( blocks ) {
+ const groupInnerBlocks = blocks.map( ( { name, attributes, innerBlocks } ) => {
+ return createBlock( name, attributes, innerBlocks );
+ } );
+
+ return createBlock( 'core/test-group-block', {}, groupInnerBlocks );
+ },
+ } ],
+ },
+ save: noop,
+ category: 'common',
+ title: 'Test Group Block',
+ } );
+
+ registerBlockType( 'core/text-block', defaultBlockSettings );
+
+ const numOfBlocksToGroup = 4;
+ const blocks = times( numOfBlocksToGroup, ( index ) => {
+ return createBlock( 'core/text-block', {
+ value: `textBlock${ index + 1 }`,
+ } );
+ } );
+
+ const transformedBlocks = switchToBlockType( blocks, 'core/test-group-block' );
+
+ expect( transformedBlocks ).toHaveLength( 1 );
+ expect( transformedBlocks[ 0 ].name ).toBe( 'core/test-group-block' );
+ expect( transformedBlocks[ 0 ].innerBlocks ).toHaveLength( numOfBlocksToGroup );
+ } );
+
+ it( 'should prefer "__experimentalConvert" method over "transform" method when running a transformation', () => {
+ const convertSpy = jest.fn( ( blocks ) => {
+ const groupInnerBlocks = blocks.map( ( { name, attributes, innerBlocks } ) => {
+ return createBlock( name, attributes, innerBlocks );
+ } );
+
+ return createBlock( 'core/test-group-block', {}, groupInnerBlocks );
+ } );
+ const transformSpy = jest.fn();
+
+ registerBlockType( 'core/test-group-block', {
+ attributes: {
+ value: {
+ type: 'string',
+ },
+ },
+ transforms: {
+ from: [ {
+ type: 'block',
+ blocks: [ '*' ],
+ isMultiBlock: true,
+ __experimentalConvert: convertSpy,
+ transform: transformSpy,
+ } ],
+ },
+ save: noop,
+ category: 'common',
+ title: 'Test Group Block',
+ } );
+
+ registerBlockType( 'core/text-block', defaultBlockSettings );
+
+ const numOfBlocksToGroup = 4;
+ const blocks = times( numOfBlocksToGroup, ( index ) => {
+ return createBlock( 'core/text-block', {
+ value: `textBlock${ index + 1 }`,
+ } );
+ } );
+
+ const transformedBlocks = switchToBlockType( blocks, 'core/test-group-block' );
+
+ expect( transformedBlocks ).toHaveLength( 1 );
+ expect( convertSpy.mock.calls ).toHaveLength( 1 );
+ expect( transformSpy.mock.calls ).toHaveLength( 0 );
+ } );
} );
describe( 'getBlockTransforms', () => {
@@ -1336,4 +1502,107 @@ describe( 'block factory', () => {
expect( transform ).toBe( null );
} );
} );
+
+ describe( 'isWildcardBlockTransform', () => {
+ it( 'should return true for transforms with type of block and "*" alias as blocks', () => {
+ const validWildcardBlockTransform = {
+ type: 'block',
+ blocks: [
+ 'core/some-other-block-first', // unlikely to happen but...
+ '*',
+ ],
+ blockName: 'core/test-block',
+ };
+
+ expect( isWildcardBlockTransform( validWildcardBlockTransform ) ).toBe( true );
+ } );
+
+ it( 'should return false for transforms with a type which is not "block"', () => {
+ const invalidWildcardBlockTransform = {
+ type: 'file',
+ blocks: [
+ '*',
+ ],
+ blockName: 'core/test-block',
+ };
+
+ expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false );
+ } );
+
+ it( 'should return false for transforms which do not include "*" alias in "block" array', () => {
+ const invalidWildcardBlockTransform = {
+ type: 'block',
+ blocks: [
+ 'core/some-block',
+ 'core/another-block',
+ ],
+ blockName: 'core/test-block',
+ };
+
+ expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false );
+ } );
+
+ it( 'should return false for transforms which do not provide an array as the "blocks" option', () => {
+ const invalidWildcardBlockTransform = {
+ type: 'block',
+ blocks: noop,
+ blockName: 'core/test-block',
+ };
+
+ expect( isWildcardBlockTransform( invalidWildcardBlockTransform ) ).toBe( false );
+ } );
+ } );
+
+ describe( 'isContainerGroupBlock', () => {
+ it( 'should return true when passed block name matches "core/group"', () => {
+ expect( isContainerGroupBlock( 'core/group' ) ).toBe( true );
+ } );
+
+ it( 'should return false when passed block name does not match "core/group"', () => {
+ expect( isContainerGroupBlock( 'core/some-test-name' ) ).toBe( false );
+ } );
+ } );
+
+ describe( 'isBlockSelectionOfSameType', () => {
+ it( 'should return false when all blocks do not match the name of the first block', () => {
+ const blocks = [
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/another-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ ];
+
+ expect( isBlockSelectionOfSameType( blocks ) ).toBe( false );
+ } );
+
+ it( 'should return true when all blocks match the name of the first block', () => {
+ const blocks = [
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ {
+ name: 'core/test-block',
+ },
+ ];
+
+ expect( isBlockSelectionOfSameType( blocks ) ).toBe( true );
+ } );
+ } );
} );
diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json
index a1480a4a821de0..9e70302edeb817 100644
--- a/packages/browserslist-config/package.json
+++ b/packages/browserslist-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/browserslist-config",
- "version": "2.4.0",
+ "version": "2.5.0",
"description": "WordPress Browserslist shared configuration.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -21,6 +21,9 @@
"engines": {
"node": ">=8"
},
+ "files": [
+ "index.js"
+ ],
"main": "index.js",
"publishConfig": {
"access": "public"
diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md
index e6c72c7dbb190d..38e44d1c10cfba 100644
--- a/packages/components/CHANGELOG.md
+++ b/packages/components/CHANGELOG.md
@@ -1,8 +1,25 @@
-## Master
+## 8.0.0 (2019-06-12)
### New Feature
- Add new `BlockQuotation` block to the primitives folder to support blockquote in a multiplatform way. [#15482](https://github.com/WordPress/gutenberg/pull/15482).
+- `DropdownMenu` now supports passing a [render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) as children for more advanced customization.
+
+### Internal
+
+- `MenuGroup` no longer uses `NavigableMenu` internally. It needs to be explicitly wrapped with `NavigableMenu` to bring back the same behavior.
+
+### Documentation
+
+- Added missing documentation for `DropdownMenu` props `menuLabel`, `position`, `className`.
+
+### Breaking Change
+
+- `ServerSideRender` is no longer part of components. It was extracted to an independent package `@wordpress/server-side-render`.
+
+### Bug Fix
+
+- Although `DateTimePicker` does not allow picking the seconds, passed the current seconds as the selected value for seconds when calling `onChange`. Now it passes zero.
## 7.4.0 (2019-05-21)
diff --git a/packages/components/package.json b/packages/components/package.json
index 1dafbbe3e018df..99c44a249b7e42 100644
--- a/packages/components/package.json
+++ b/packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/components",
- "version": "7.4.0",
+ "version": "8.0.0",
"description": "UI components for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -23,7 +23,6 @@
"dependencies": {
"@babel/runtime": "^7.4.4",
"@wordpress/a11y": "file:../a11y",
- "@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/compose": "file:../compose",
"@wordpress/dom": "file:../dom",
"@wordpress/element": "file:../element",
@@ -44,6 +43,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"
diff --git a/packages/components/src/animate/style.scss b/packages/components/src/animate/style.scss
index 1fa4e95d0cf59a..814e1bc3701122 100644
--- a/packages/components/src/animate/style.scss
+++ b/packages/components/src/animate/style.scss
@@ -1,7 +1,7 @@
.components-animate__appear {
animation: components-animate__appear-animation 0.1s cubic-bezier(0, 0, 0.2, 1) 0s;
animation-fill-mode: forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
&.is-from-top,
&.is-from-top.is-from-left {
@@ -31,7 +31,7 @@
.components-animate__slide-in {
animation: components-animate__slide-in-animation 0.1s cubic-bezier(0, 0, 0.2, 1);
animation-fill-mode: forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
&.is-from-left {
transform: translateX(+100%);
diff --git a/packages/components/src/button/style.scss b/packages/components/src/button/style.scss
index 1d44f3bb9cfcb2..69776322ede4ee 100644
--- a/packages/components/src/button/style.scss
+++ b/packages/components/src/button/style.scss
@@ -144,6 +144,7 @@
transition-property: border, background, color;
transition-duration: 0.05s;
transition-timing-function: ease-in-out;
+ @include reduce-motion("transition");
&:hover,
&:active {
diff --git a/packages/components/src/color-palette/style.scss b/packages/components/src/color-palette/style.scss
index 030b3e220c375c..97f7502a53e5d9 100644
--- a/packages/components/src/color-palette/style.scss
+++ b/packages/components/src/color-palette/style.scss
@@ -21,6 +21,7 @@ $color-palette-circle-spacing: 14px;
vertical-align: top;
transform: scale(1);
transition: 100ms transform ease;
+ @include reduce-motion("transition");
&:hover {
transform: scale(1.2);
}
@@ -42,6 +43,7 @@ $color-palette-circle-spacing: 14px;
background: transparent;
box-shadow: inset 0 0 0 ($color-palette-circle-size / 2);
transition: 100ms box-shadow ease;
+ @include reduce-motion("transition");
cursor: pointer;
&.is-active {
diff --git a/packages/components/src/color-picker/style.scss b/packages/components/src/color-picker/style.scss
index 8abda3ac275896..576e3d25ff7683 100644
--- a/packages/components/src/color-picker/style.scss
+++ b/packages/components/src/color-picker/style.scss
@@ -167,6 +167,7 @@
.components-color-picker__hue-pointer,
.components-color-picker__saturation-pointer {
transition: box-shadow 0.1s linear;
+ @include reduce-motion("transition");
}
.components-color-picker__saturation-pointer:focus {
diff --git a/packages/components/src/date-time/README.md b/packages/components/src/date-time/README.md
index f0fa63c69b6ce4..530a67e0e8e576 100644
--- a/packages/components/src/date-time/README.md
+++ b/packages/components/src/date-time/README.md
@@ -52,8 +52,7 @@ The current date and time at initialization. Optionally pass in a `null` value t
The function called when a new date or time has been selected. It is passed the `currentDate` as an argument.
- Type: `Function`
-- Required: No
-- Default: `noop`
+- Required: Yes
### is12Hour
diff --git a/packages/components/src/date-time/date.js b/packages/components/src/date-time/date.js
index a8e4130e739c8c..0d5058789f17f6 100644
--- a/packages/components/src/date-time/date.js
+++ b/packages/components/src/date-time/date.js
@@ -30,7 +30,7 @@ class DatePicker extends Component {
const momentTime = {
hours: momentDate.hours(),
minutes: momentDate.minutes(),
- seconds: momentDate.seconds(),
+ seconds: 0,
};
onChange( newDate.set( momentTime ).format( TIMEZONELESS_FORMAT ) );
diff --git a/packages/components/src/date-time/style.scss b/packages/components/src/date-time/style.scss
index 3b0609258f88ca..d25d923dd24d94 100644
--- a/packages/components/src/date-time/style.scss
+++ b/packages/components/src/date-time/style.scss
@@ -19,6 +19,21 @@
margin-right: $grid-size;
margin-top: 0.5em;
}
+
+ fieldset {
+ border: 0;
+ padding: 0;
+ margin: 0;
+ }
+
+ select,
+ input {
+ box-sizing: border-box;
+ height: 28px;
+ vertical-align: middle;
+ padding: 0;
+ @include input-style__neutral();
+ }
}
.components-datetime__date {
@@ -119,9 +134,6 @@
}
.components-datetime__time-field {
- align-self: center;
- flex: 0 1 auto;
- order: 1;
&.am-pm button {
font-size: 11px;
@@ -199,7 +211,7 @@
// Hack to center the datepicker component within the popover.
// It sets its own styles so centering is tricky.
.components-popover .components-datetime__date {
- padding-left: 6px;
+ padding-left: 4px;
}
// Used to prevent z-index issues on mobile.
diff --git a/packages/components/src/date-time/time.js b/packages/components/src/date-time/time.js
index 55937b10170319..0cfc8c7dc8ea68 100644
--- a/packages/components/src/date-time/time.js
+++ b/packages/components/src/date-time/time.js
@@ -33,6 +33,7 @@ class TimePicker extends Component {
am: true,
date: null,
};
+ this.changeDate = this.changeDate.bind( this );
this.updateMonth = this.updateMonth.bind( this );
this.onChangeMonth = this.onChangeMonth.bind( this );
this.updateDay = this.updateDay.bind( this );
@@ -61,6 +62,17 @@ class TimePicker extends Component {
this.syncState( this.props );
}
}
+ /**
+ * Function that sets the date state and calls the onChange with a new date.
+ * The date is truncated at the minutes.
+ *
+ * @param {Object} newDate The date object.
+ */
+ changeDate( newDate ) {
+ const dateWithStartOfMinutes = newDate.clone().startOf( 'minute' );
+ this.setState( { date: dateWithStartOfMinutes } );
+ this.props.onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ }
getMaxHours() {
return this.props.is12Hour ? 12 : 23;
@@ -83,7 +95,7 @@ class TimePicker extends Component {
}
updateHours() {
- const { is12Hour, onChange } = this.props;
+ const { is12Hour } = this.props;
const { am, hours, date } = this.state;
const value = parseInt( hours, 10 );
if (
@@ -98,12 +110,10 @@ class TimePicker extends Component {
const newDate = is12Hour ?
date.clone().hours( am === 'AM' ? value % 12 : ( ( ( value % 12 ) + 12 ) % 24 ) ) :
date.clone().hours( value );
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
}
updateMinutes() {
- const { onChange } = this.props;
const { minutes, date } = this.state;
const value = parseInt( minutes, 10 );
if ( ! isInteger( value ) || value < 0 || value > 59 ) {
@@ -111,12 +121,10 @@ class TimePicker extends Component {
return;
}
const newDate = date.clone().minutes( value );
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
}
updateDay() {
- const { onChange } = this.props;
const { day, date } = this.state;
const value = parseInt( day, 10 );
if ( ! isInteger( value ) || value < 1 || value > 31 ) {
@@ -124,12 +132,10 @@ class TimePicker extends Component {
return;
}
const newDate = date.clone().date( value );
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
}
updateMonth() {
- const { onChange } = this.props;
const { month, date } = this.state;
const value = parseInt( month, 10 );
if ( ! isInteger( value ) || value < 1 || value > 12 ) {
@@ -137,12 +143,10 @@ class TimePicker extends Component {
return;
}
const newDate = date.clone().month( value - 1 );
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
}
updateYear() {
- const { onChange } = this.props;
const { year, date } = this.state;
const value = parseInt( year, 10 );
if ( ! isInteger( value ) || value < 0 || value > 9999 ) {
@@ -150,13 +154,11 @@ class TimePicker extends Component {
return;
}
const newDate = date.clone().year( value );
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
}
updateAmPm( value ) {
return () => {
- const { onChange } = this.props;
const { am, date, hours } = this.state;
if ( am === value ) {
return;
@@ -167,8 +169,7 @@ class TimePicker extends Component {
} else {
newDate = date.clone().hours( parseInt( hours, 10 ) % 12 );
}
- this.setState( { date: newDate } );
- onChange( newDate.format( TIMEZONELESS_FORMAT ) );
+ this.changeDate( newDate );
};
}
diff --git a/packages/components/src/drop-zone/style.scss b/packages/components/src/drop-zone/style.scss
index 4b36a7fb9a2bc3..414918b3a44a54 100644
--- a/packages/components/src/drop-zone/style.scss
+++ b/packages/components/src/drop-zone/style.scss
@@ -8,6 +8,7 @@
visibility: hidden;
opacity: 0;
transition: 0.3s opacity, 0.3s background-color, 0s visibility 0.3s;
+ @include reduce-motion("transition");
border: 2px solid $blue-dark-900;
border-radius: 2px;
@@ -15,6 +16,7 @@
opacity: 1;
visibility: visible;
transition: 0.3s opacity, 0.3s background-color;
+ @include reduce-motion("transition");
}
&.is-dragging-over-element {
@@ -33,6 +35,7 @@
text-align: center;
color: $white;
transition: transform 0.2s ease-in-out;
+ @include reduce-motion("transition");
}
.components-drop-zone.is-dragging-over-element .components-drop-zone__content {
diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md
index 9346140bb27ef0..4e2243c653b2f4 100644
--- a/packages/components/src/dropdown-menu/README.md
+++ b/packages/components/src/dropdown-menu/README.md
@@ -91,6 +91,47 @@ const MyDropdownMenu = () => (
);
```
+Alternatively, specify a `children` function which returns elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`.
+
+```jsx
+import { Fragment } from '@wordpress/element';
+import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components';
+
+const MyDropdownMenu = () => (
+
+ { ( { onClose } ) => (
+
+
+
+ Move Up
+
+
+ Move Down
+
+
+
+
+ Remove
+
+
+
+ ) }
+
+);
+```
+
### Props
The component accepts the following props:
@@ -112,13 +153,46 @@ A human-readable label to present as accessibility text on the focused collapsed
- Type: `String`
- Required: Yes
+#### menuLabel
+
+A human-readable label to present as accessibility text on the expanded menu container.
+
+- Type: `String`
+- Required: No
+
+#### position
+
+The direction in which the menu should open. Specify y- and x-axis as a space-separated string. Supports `"top"`, `"middle"`, `"bottom"` y axis, and `"left"`, `"center"`, `"right"` x axis.
+
+- Type: `String`
+- Required: No
+- Default: `"top center"`
+
#### controls
An array of objects describing the options to be shown in the expanded menu.
Each object should include an `icon` [Dashicon](https://developer.wordpress.org/resource/dashicons/) slug string, a human-readable `title` string, `isDisabled` boolean flag and an `onClick` function callback to invoke when the option is selected.
+A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.
+
- Type: `Array`
-- Required: Yes
+- Required: No
+
+#### children
+
+A [function render prop](https://reactjs.org/docs/render-props.html#using-props-other-than-render) which should return an element or elements valid for use in a DropdownMenu: `MenuItem`, `MenuItemsChoice`, or `MenuGroup`. Its first argument is a props object including the same values as given to a [`Dropdown`'s `renderContent`](/packages/components/src/dropdown#rendercontent) (`isOpen`, `onToggle`, `onClose`).
+
+A valid DropdownMenu must specify one or the other of a `controls` or `children` prop.
+
+- Type: `Function`
+- Required: No
See also: [https://developer.wordpress.org/resource/dashicons/](https://developer.wordpress.org/resource/dashicons/)
+
+#### className
+
+A class name to apply to the dropdown wrapper element.
+
+- Type: `String`
+- Required: No
diff --git a/packages/components/src/dropdown-menu/index.js b/packages/components/src/dropdown-menu/index.js
index e301935a0ac60d..8195a309ec0241 100644
--- a/packages/components/src/dropdown-menu/index.js
+++ b/packages/components/src/dropdown-menu/index.js
@@ -2,7 +2,7 @@
* External dependencies
*/
import classnames from 'classnames';
-import { flatMap } from 'lodash';
+import { flatMap, isEmpty, isFunction } from 'lodash';
/**
* WordPress dependencies
@@ -17,27 +17,35 @@ import Dropdown from '../dropdown';
import { NavigableMenu } from '../navigable-container';
function DropdownMenu( {
+ children,
+ className,
+ controls,
icon = 'menu',
label,
menuLabel,
- controls,
- className,
position,
+ __unstableLabelPosition,
+ __unstableMenuClassName,
+ __unstablePopoverClassName,
+ __unstableToggleClassName,
} ) {
- if ( ! controls || ! controls.length ) {
+ if ( isEmpty( controls ) && ! isFunction( children ) ) {
return null;
}
// Normalize controls to nested array of objects (sets of controls)
- let controlSets = controls;
- if ( ! Array.isArray( controlSets[ 0 ] ) ) {
- controlSets = [ controlSets ];
+ let controlSets;
+ if ( ! isEmpty( controls ) ) {
+ controlSets = controls;
+ if ( ! Array.isArray( controlSets[ 0 ] ) ) {
+ controlSets = [ controlSets ];
+ }
}
return (
{
const openOnArrowDown = ( event ) => {
@@ -47,35 +55,44 @@ function DropdownMenu( {
onToggle();
}
};
+
return (
-
+ { ! icon && }
);
} }
- renderContent={ ( { onClose } ) => {
+ renderContent={ ( props ) => {
return (
+ {
+ isFunction( children ) ?
+ children( props ) :
+ null
+ }
{ flatMap( controlSets, ( controlSet, indexOfSet ) => (
controlSet.map( ( control, indexOfControl ) => (
{
event.stopPropagation();
- onClose();
+ props.onClose();
if ( control.onClick ) {
control.onClick();
}
diff --git a/packages/components/src/dropdown-menu/style.scss b/packages/components/src/dropdown-menu/style.scss
index 3a1ac01f332e6c..34f58970bf9f40 100644
--- a/packages/components/src/dropdown-menu/style.scss
+++ b/packages/components/src/dropdown-menu/style.scss
@@ -36,18 +36,20 @@
}
}
}
+
.components-dropdown-menu__popover .components-popover__content {
width: 200px;
}
.components-dropdown-menu__menu {
width: 100%;
- padding: 9px;
+ padding: ($grid-size - $border-width) 0;
font-family: $default-font;
font-size: $default-font-size;
line-height: $default-line-height;
- .components-dropdown-menu__menu-item {
+ .components-dropdown-menu__menu-item,
+ .components-menu-item {
width: 100%;
padding: 6px;
outline: none;
@@ -92,4 +94,16 @@
@include formatting-button-style__active();
}
}
+
+ .components-menu-group:not(:last-child) {
+ border-bottom: $border-width solid $light-gray-500;
+ }
+
+ .components-menu-item__button {
+ padding-left: 2rem;
+
+ &.has-icon {
+ padding-left: 0.5rem;
+ }
+ }
}
diff --git a/packages/components/src/dropdown-menu/test/index.js b/packages/components/src/dropdown-menu/test/index.js
index ef8e900de24f9f..74e0089eed68bb 100644
--- a/packages/components/src/dropdown-menu/test/index.js
+++ b/packages/components/src/dropdown-menu/test/index.js
@@ -1,21 +1,22 @@
/**
* External dependencies
*/
-import { shallow } from 'enzyme';
-import TestUtils from 'react-dom/test-utils';
+import { shallow, mount } from 'enzyme';
/**
* WordPress dependencies
*/
import { DOWN } from '@wordpress/keycodes';
-import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
import DropdownMenu from '../';
+import { IconButton, MenuItem, NavigableMenu } from '../../';
describe( 'DropdownMenu', () => {
+ const children = ( { onClose } ) => ;
+
let controls;
beforeEach( () => {
controls = [
@@ -43,42 +44,48 @@ describe( 'DropdownMenu', () => {
} );
describe( 'basic rendering', () => {
- it( 'should render a null element when controls are not assigned', () => {
+ it( 'should render a null element when neither controls nor children are assigned', () => {
const wrapper = shallow( );
expect( wrapper.type() ).toBeNull();
} );
- it( 'should render a null element when controls are empty', () => {
+ it( 'should render a null element when controls are empty and children is not specified', () => {
const wrapper = shallow( );
expect( wrapper.type() ).toBeNull();
} );
- it( 'should open menu on arrow down', () => {
- // needed because TestUtils.renderIntoDocument returns null for stateless
- // components
- class Menu extends Component {
- render() {
- return ;
- }
- }
- const wrapper = TestUtils.renderIntoDocument( );
- const buttonElement = TestUtils.findRenderedDOMComponentWithClass(
- wrapper,
- 'components-dropdown-menu__toggle'
- );
- // Close menu by keyup
- TestUtils.Simulate.keyDown(
- buttonElement,
- {
- stopPropagation: () => {},
- preventDefault: () => {},
- keyCode: DOWN,
- }
- );
-
- expect( TestUtils.scryRenderedDOMComponentsWithClass( wrapper, 'components-popover' ) ).toHaveLength( 1 );
+ it( 'should open menu on arrow down (controls)', () => {
+ const wrapper = mount( );
+ const button = wrapper.find( IconButton ).filter( '.components-dropdown-menu__toggle' );
+
+ button.simulate( 'keydown', {
+ stopPropagation: () => {},
+ preventDefault: () => {},
+ keyCode: DOWN,
+ } );
+
+ expect( wrapper.find( NavigableMenu ) ).toHaveLength( 1 );
+ expect( wrapper.find( IconButton ).filter( '.components-dropdown-menu__menu-item' ) ).toHaveLength( controls.length );
+ } );
+
+ it( 'should open menu on arrow down (children)', () => {
+ const wrapper = mount( );
+ const button = wrapper.find( IconButton ).filter( '.components-dropdown-menu__toggle' );
+
+ button.simulate( 'keydown', {
+ stopPropagation: () => {},
+ preventDefault: () => {},
+ keyCode: DOWN,
+ } );
+
+ expect( wrapper.find( NavigableMenu ) ).toHaveLength( 1 );
+
+ wrapper.find( MenuItem ).props().onClick();
+ wrapper.update();
+
+ expect( wrapper.find( NavigableMenu ) ).toHaveLength( 0 );
} );
} );
} );
diff --git a/packages/components/src/form-toggle/style.scss b/packages/components/src/form-toggle/style.scss
index 51280da0ac8da9..ebe43dc70441a6 100644
--- a/packages/components/src/form-toggle/style.scss
+++ b/packages/components/src/form-toggle/style.scss
@@ -36,6 +36,7 @@ $toggle-border-width: 2px;
height: $toggle-height;
border-radius: $toggle-height / 2;
transition: 0.2s background ease;
+ @include reduce-motion("transition");
}
.components-form-toggle__thumb {
@@ -48,6 +49,7 @@ $toggle-border-width: 2px;
height: $toggle-height - ($toggle-border-width * 4);
border-radius: 50%;
transition: 0.1s transform ease;
+ @include reduce-motion("transition");
background-color: $dark-gray-300;
border: 5px solid $dark-gray-300; // Has explicit border to act as a fill in Windows High Contrast Mode.
}
diff --git a/packages/components/src/form-token-field/style.scss b/packages/components/src/form-token-field/style.scss
index 5480cdef635409..35f9569f268b11 100644
--- a/packages/components/src/form-token-field/style.scss
+++ b/packages/components/src/form-token-field/style.scss
@@ -134,6 +134,7 @@
line-height: 24px;
background: $light-gray-500;
transition: all 0.2s cubic-bezier(0.4, 1, 0.4, 1);
+ @include reduce-motion;
}
.components-form-token-field__token-text {
@@ -164,6 +165,7 @@
max-height: 9em;
overflow-y: scroll;
transition: all 0.15s ease-in-out;
+ @include reduce-motion("transition");
list-style: none;
border-top: $border-width solid $dark-gray-300;
margin: $grid-size-small (-$grid-size-small) (-$grid-size-small);
diff --git a/packages/components/src/higher-order/with-focus-return/README.md b/packages/components/src/higher-order/with-focus-return/README.md
index 15ec5ef09ef258..f0872f15600f3d 100644
--- a/packages/components/src/higher-order/with-focus-return/README.md
+++ b/packages/components/src/higher-order/with-focus-return/README.md
@@ -2,7 +2,7 @@
`withFocusReturn` is a higher-order component used typically in scenarios of short-lived elements (modals, dropdowns) where, upon the element's unmounting, focus should be restored to the focused element which had initiated it being rendered.
-Optionally, it can be used in combination with a `FocusRenderProvider` which, when rendered toward the top of an application, will remember a history of elements focused during a session. This can provide safeguards for scenarios where one short-lived element triggers the creation of another (e.g. a dropdown menu triggering a modal display). The combined effect of `FocusRenderProvider` and `withFocusReturn` is that focus will be returned to the most recent focused element which is still present in the document.
+Optionally, it can be used in combination with a `FocusReturnProvider` which, when rendered toward the top of an application, will remember a history of elements focused during a session. This can provide safeguards for scenarios where one short-lived element triggers the creation of another (e.g. a dropdown menu triggering a modal display). The combined effect of `FocusReturnProvider` and `withFocusReturn` is that focus will be returned to the most recent focused element which is still present in the document.
## Usage
diff --git a/packages/components/src/higher-order/with-spoken-messages/README.md b/packages/components/src/higher-order/with-spoken-messages/README.md
index 1cb09651b7919d..5e539b3f3d5725 100644
--- a/packages/components/src/higher-order/with-spoken-messages/README.md
+++ b/packages/components/src/higher-order/with-spoken-messages/README.md
@@ -7,8 +7,8 @@ import { withSpokenMessages, Button } from '@wordpress/components';
const MyComponentWithSpokenMessages = withSpokenMessages( ( { speak, debouncedSpeak } ) => (
- Speak
- Debounced Speak
+ speak( 'Spoken message' ) }>Speak
+ debouncedSpeak( 'Delayed message' ) }>Debounced Speak
) );
```
diff --git a/packages/components/src/icon/README.md b/packages/components/src/icon/README.md
index de36accebd3753..9fbf69609ccbcb 100644
--- a/packages/components/src/icon/README.md
+++ b/packages/components/src/icon/README.md
@@ -57,7 +57,7 @@ const MyIcon = () => (
## Props
-The component accepts the following props:
+The component accepts the following props. Any additional props are passed through to the underlying icon element.
### icon
@@ -74,11 +74,3 @@ The size (width and height) of the icon.
- Type: `Number`
- Required: No
- Default: `20` when a Dashicon is rendered, `24` for all other icons.
-
-### className
-
-An optional additional class name to apply to the rendered icon.
-
-- Type: `String`
-- Required: No
-- Default: `null`
diff --git a/packages/components/src/icon/index.js b/packages/components/src/icon/index.js
index f106f7c5615114..61a4fb0a2d6c47 100644
--- a/packages/components/src/icon/index.js
+++ b/packages/components/src/icon/index.js
@@ -8,13 +8,13 @@ import { cloneElement, createElement, Component, isValidElement } from '@wordpre
*/
import { Dashicon, SVG } from '../';
-function Icon( { icon = null, size, className } ) {
+function Icon( { icon = null, size, ...additionalProps } ) {
let iconSize;
if ( 'string' === typeof icon ) {
// Dashicons should be 20x20 by default
iconSize = size || 20;
- return ;
+ return ;
}
// Any other icons should be 24x24 by default
@@ -22,18 +22,18 @@ function Icon( { icon = null, size, className } ) {
if ( 'function' === typeof icon ) {
if ( icon.prototype instanceof Component ) {
- return createElement( icon, { className, size: iconSize } );
+ return createElement( icon, { size: iconSize, ...additionalProps } );
}
- return icon();
+ return icon( { size: iconSize, ...additionalProps } );
}
if ( icon && ( icon.type === 'svg' || icon.type === SVG ) ) {
const appliedProps = {
- className,
width: iconSize,
height: iconSize,
...icon.props,
+ ...additionalProps,
};
return ;
@@ -41,8 +41,8 @@ function Icon( { icon = null, size, className } ) {
if ( isValidElement( icon ) ) {
return cloneElement( icon, {
- className,
size: iconSize,
+ ...additionalProps,
} );
}
diff --git a/packages/components/src/icon/test/index.js b/packages/components/src/icon/test/index.js
index ec632ef74677b3..053b0cf390ff58 100644
--- a/packages/components/src/icon/test/index.js
+++ b/packages/components/src/icon/test/index.js
@@ -17,6 +17,7 @@ import { Path, SVG } from '../../';
describe( 'Icon', () => {
const className = 'example-class';
const svg = ;
+ const style = { fill: 'red' };
it( 'renders nothing when icon omitted', () => {
const wrapper = shallow( );
@@ -30,24 +31,12 @@ describe( 'Icon', () => {
expect( wrapper.find( 'Dashicon' ).prop( 'icon' ) ).toBe( 'format-image' );
} );
- it( 'renders a dashicon and passes the classname to it', () => {
- const wrapper = shallow( );
-
- expect( wrapper.find( 'Dashicon' ).prop( 'className' ) ).toBe( 'example-class' );
- } );
-
it( 'renders a dashicon and with a default size of 20', () => {
const wrapper = shallow( );
expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 );
} );
- it( 'renders a dashicon and passes the size to it', () => {
- const wrapper = shallow( );
-
- expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 32 );
- } );
-
it( 'renders a function', () => {
const wrapper = shallow( } /> );
@@ -60,30 +49,12 @@ describe( 'Icon', () => {
expect( wrapper.name() ).toBe( 'span' );
} );
- it( 'renders an element and passes the classname to it', () => {
- const wrapper = shallow( } className={ className } /> );
-
- expect( wrapper.prop( 'className' ) ).toBe( 'example-class' );
- } );
-
- it( 'renders an element and passes the size to it', () => {
- const wrapper = shallow( );
-
- expect( wrapper.prop( 'size' ) ).toBe( 32 );
- } );
-
it( 'renders an svg element', () => {
const wrapper = shallow( );
expect( wrapper.name() ).toBe( 'SVG' );
} );
- it( 'renders an svg element and passes the classname to it', () => {
- const wrapper = shallow( );
-
- expect( wrapper.prop( 'className' ) ).toBe( 'example-class' );
- } );
-
it( 'renders an svg element with a default width and height of 24', () => {
const wrapper = shallow( );
@@ -118,29 +89,38 @@ describe( 'Icon', () => {
expect( wrapper.name() ).toBe( 'MyComponent' );
} );
- it( 'renders a component and passes the classname to it', () => {
+ describe( 'props passing', () => {
class MyComponent extends Component {
render( ) {
return ;
}
}
- const wrapper = shallow(
-
- );
-
- expect( wrapper.prop( 'className' ) ).toBe( 'example-class' );
- } );
-
- it( 'renders a component and passes the size to it', () => {
- class MyComponent extends Component {
- render( ) {
- return ;
- }
- }
- const wrapper = shallow(
-
- );
- expect( wrapper.prop( 'size' ) ).toBe( 32 );
+ describe.each( [
+ [ 'dashicon', { icon: 'format-image' } ],
+ [ 'element', { icon: } ],
+ [ 'svg element', { icon: svg } ],
+ [ 'component', { icon: MyComponent } ],
+ ] )( '%s', ( label, props ) => {
+ it( 'should pass through size', () => {
+ if ( label === 'svg element' ) {
+ // Custom logic for SVG elements tested separately.
+ //
+ // See: `renders an svg element and passes the size as its width and height`
+ return;
+ }
+
+ const wrapper = shallow( );
+
+ expect( wrapper.prop( 'size' ) ).toBe( 32 );
+ } );
+
+ it( 'should pass through all other props', () => {
+ const wrapper = shallow( );
+
+ expect( wrapper.prop( 'style' ) ).toBe( style );
+ expect( wrapper.prop( 'className' ) ).toBe( className );
+ } );
+ } );
} );
} );
diff --git a/packages/components/src/index.js b/packages/components/src/index.js
index ab0540fa2f232f..6395ea3d843a42 100644
--- a/packages/components/src/index.js
+++ b/packages/components/src/index.js
@@ -53,7 +53,6 @@ export { default as SelectControl } from './select-control';
export { default as Snackbar } from './snackbar';
export { default as SnackbarList } from './snackbar/list';
export { default as Spinner } from './spinner';
-export { default as ServerSideRender } from './server-side-render';
export { default as TabPanel } from './tab-panel';
export { default as TextControl } from './text-control';
export { default as TextareaControl } from './textarea-control';
diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js
index f3bd5ba6951b16..881b011c8a08ab 100644
--- a/packages/components/src/index.native.js
+++ b/packages/components/src/index.native.js
@@ -22,3 +22,6 @@ export { default as withSpokenMessages } from './higher-order/with-spoken-messag
// Mobile Components
export { default as BottomSheet } from './mobile/bottom-sheet';
export { default as Picker } from './mobile/picker';
+export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view';
+export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list';
+export { default as ReadableContentView } from './mobile/readable-content-view';
diff --git a/packages/components/src/keyboard-shortcuts/README.md b/packages/components/src/keyboard-shortcuts/README.md
index 4e1bb1a9dd73f3..7c1046087d9b38 100644
--- a/packages/components/src/keyboard-shortcuts/README.md
+++ b/packages/components/src/keyboard-shortcuts/README.md
@@ -48,13 +48,13 @@ Elements to render, upon whom key events are to be monitored.
An object of shortcut bindings, where each key is a keyboard combination, the value of which is the callback to be invoked when the key combination is pressed.
- Type: `Object`
-- Required: No
+- Required: Yes
__Note:__ The value of each shortcut should be a consistent function reference, not an anonymous function. Otherwise, the callback will not be correctly unbound when the component unmounts.
__Note:__ The `KeyboardShortcuts` component will not update to reflect a changed `shortcuts` prop. If you need to change shortcuts, mount a separate `KeyboardShortcuts` element, which can be achieved by assigning a unique `key` prop.
-## bindGlobal
+### bindGlobal
By default, a callback will not be invoked if the key combination occurs in an editable field. Pass `bindGlobal` as `true` if the key events should be observed globally, including within editable fields.
@@ -63,9 +63,9 @@ By default, a callback will not be invoked if the key combination occurs in an e
_Tip:_ If you need some but not all keyboard events to be observed globally, simply render two distinct `KeyboardShortcuts` elements, one with and one without the `bindGlobal` prop.
-## event
+### eventName
-By default, a callback is invoked in response to the `keydown` event. To override this, pass `event` with the name of a specific keyboard event.
+By default, a callback is invoked in response to the `keydown` event. To override this, pass `eventName` with the name of a specific keyboard event.
- Type: `String`
- Required: No
diff --git a/packages/components/src/menu-group/index.js b/packages/components/src/menu-group/index.js
index f5b2aff60337ed..ba98c5fea4fd22 100644
--- a/packages/components/src/menu-group/index.js
+++ b/packages/components/src/menu-group/index.js
@@ -9,11 +9,6 @@ import classnames from 'classnames';
import { Children } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
-/**
- * Internal dependencies
- */
-import { NavigableMenu } from '../navigable-container';
-
export function MenuGroup( {
children,
className = '',
@@ -33,11 +28,17 @@ export function MenuGroup( {
return (
{ label &&
-
{ label }
+
+ { label }
+
}
-
+
{ children }
-
+
);
}
diff --git a/packages/components/src/menu-group/test/__snapshots__/index.js.snap b/packages/components/src/menu-group/test/__snapshots__/index.js.snap
index c33b2fcad917cd..4a0c0e751a2667 100644
--- a/packages/components/src/menu-group/test/__snapshots__/index.js.snap
+++ b/packages/components/src/menu-group/test/__snapshots__/index.js.snap
@@ -5,18 +5,19 @@ exports[`MenuGroup should match snapshot 1`] = `
className="components-menu-group"
>
-
My item
-
+
`;
diff --git a/packages/components/src/menu-item/README.md b/packages/components/src/menu-item/README.md
index 38251ccedf2614..1741f8102992f3 100644
--- a/packages/components/src/menu-item/README.md
+++ b/packages/components/src/menu-item/README.md
@@ -32,8 +32,6 @@ MenuItem supports the following props. Any additional props are passed through t
Element to render as child of button.
-Element
-
### `info`
- Type: `string`
@@ -50,6 +48,13 @@ Refer to documentation for [`label`](#label).
Refer to documentation for [IconButton's `icon` prop](/packages/components/src/icon-button/README.md#icon).
+### `isSelected`
+
+- Type: `boolean`
+- Required: No
+
+Whether or not the menu item is currently selected.
+
### `shortcut`
- Type: `string`
diff --git a/packages/components/src/menu-item/style.scss b/packages/components/src/menu-item/style.scss
index 8f750618c2a159..8424da18a6f02d 100644
--- a/packages/components/src/menu-item/style.scss
+++ b/packages/components/src/menu-item/style.scss
@@ -4,12 +4,13 @@
padding: $grid-size ($grid-size-large - $border-width);
text-align: left;
color: $dark-gray-600;
+ @include menu-style__neutral;
// Target plugin icons that can have arbitrary classes by using an aggressive selector.
.dashicon,
.components-menu-items__item-icon,
> span > svg {
- margin-right: 4px;
+ margin-right: 5px;
}
.components-menu-items__item-icon {
@@ -18,7 +19,6 @@
}
&:hover:not(:disabled):not([aria-disabled="true"]) {
- color: $dark-gray-500;
// Disable hover style on mobile to prevent odd scroll behaviour.
// See: https://github.com/WordPress/gutenberg/pull/10333
@include break-medium() {
@@ -26,7 +26,7 @@
}
.components-menu-item__shortcut {
- opacity: 1;
+ color: $dark-gray-600;
}
}
@@ -43,12 +43,12 @@
.components-menu-item__info {
margin-top: $grid-size-small;
font-size: $default-font-size - 1px;
- opacity: 0.84;
+ color: $dark-gray-300;
}
.components-menu-item__shortcut {
align-self: center;
- opacity: 0.84;
+ color: $dark-gray-300;
margin-right: 0;
margin-left: auto;
padding-left: $grid-size;
diff --git a/packages/components/src/mobile/keyboard-avoiding-view/index.android.js b/packages/components/src/mobile/keyboard-avoiding-view/index.android.js
new file mode 100644
index 00000000000000..10641b91b73cd1
--- /dev/null
+++ b/packages/components/src/mobile/keyboard-avoiding-view/index.android.js
@@ -0,0 +1,8 @@
+/**
+ * External dependencies
+ */
+import { KeyboardAvoidingView as AndroidKeyboardAvoidingView } from 'react-native';
+
+export const KeyboardAvoidingView = AndroidKeyboardAvoidingView;
+
+export default KeyboardAvoidingView;
diff --git a/packages/components/src/mobile/keyboard-avoiding-view/index.ios.js b/packages/components/src/mobile/keyboard-avoiding-view/index.ios.js
new file mode 100644
index 00000000000000..55fac41b263e99
--- /dev/null
+++ b/packages/components/src/mobile/keyboard-avoiding-view/index.ios.js
@@ -0,0 +1,15 @@
+/**
+ * External dependencies
+ */
+import { KeyboardAvoidingView as IOSKeyboardAvoidingView, Dimensions } from 'react-native';
+
+export const KeyboardAvoidingView = ( { parentHeight, ...otherProps } ) => {
+ const { height: fullHeight } = Dimensions.get( 'window' );
+ const keyboardVerticalOffset = fullHeight - parentHeight;
+
+ return (
+
+ );
+};
+
+export default KeyboardAvoidingView;
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js
new file mode 100644
index 00000000000000..0774c845ebcf54
--- /dev/null
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+import { FlatList } from 'react-native';
+
+/**
+ * Internal dependencies
+ */
+import KeyboardAvoidingView from '../keyboard-avoiding-view';
+
+export const KeyboardAwareFlatList = ( props ) => (
+
+
+
+);
+
+KeyboardAwareFlatList.handleCaretVerticalPositionChange = () => {
+ //no need to handle on Android, it is system managed
+};
+
+export default KeyboardAwareFlatList;
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
new file mode 100644
index 00000000000000..341737b41bef2b
--- /dev/null
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
+import { FlatList } from 'react-native';
+
+export const KeyboardAwareFlatList = ( {
+ blockToolbarHeight,
+ innerToolbarHeight,
+ shouldPreventAutomaticScroll,
+ innerRef,
+ ...listProps
+} ) => (
+ {
+ this.scrollViewRef = ref;
+ innerRef( ref );
+ } }
+ onKeyboardWillHide={ () => {
+ this.keyboardWillShowIndicator = false;
+ } }
+ onKeyboardDidHide={ () => {
+ setTimeout( () => {
+ if ( ! this.keyboardWillShowIndicator &&
+ this.latestContentOffsetY !== undefined &&
+ ! shouldPreventAutomaticScroll() ) {
+ // Reset the content position if keyboard is still closed
+ this.scrollViewRef.props.scrollToPosition( 0, this.latestContentOffsetY, true );
+ }
+ }, 50 );
+ } }
+ onKeyboardWillShow={ () => {
+ this.keyboardWillShowIndicator = true;
+ } }
+ onScroll={ ( event ) => {
+ this.latestContentOffsetY = event.nativeEvent.contentOffset.y;
+ } } >
+
+
+);
+
+KeyboardAwareFlatList.handleCaretVerticalPositionChange = ( scrollView, targetId, caretY, previousCaretY ) => {
+ if ( previousCaretY ) { //if this is not the first tap
+ scrollView.props.refreshScrollForField( targetId );
+ }
+};
+
+export default KeyboardAwareFlatList;
+
diff --git a/packages/components/src/mobile/readable-content-view/index.native.js b/packages/components/src/mobile/readable-content-view/index.native.js
new file mode 100644
index 00000000000000..bc550f93de9262
--- /dev/null
+++ b/packages/components/src/mobile/readable-content-view/index.native.js
@@ -0,0 +1,26 @@
+/**
+ * External dependencies
+ */
+import { View, Dimensions } from 'react-native';
+
+/**
+ * Internal dependencies
+ */
+import styles from './style.scss';
+
+const ReadableContentView = ( { children } ) => (
+
+
+ { children }
+
+
+);
+
+const isContentMaxWidth = () => {
+ const { width } = Dimensions.get( 'window' );
+ return width > styles.centeredContent.maxWidth;
+};
+
+ReadableContentView.isContentMaxWidth = isContentMaxWidth;
+
+export default ReadableContentView;
diff --git a/packages/components/src/mobile/readable-content-view/style.native.scss b/packages/components/src/mobile/readable-content-view/style.native.scss
new file mode 100644
index 00000000000000..611d4054a470fe
--- /dev/null
+++ b/packages/components/src/mobile/readable-content-view/style.native.scss
@@ -0,0 +1,8 @@
+.container {
+ align-items: center;
+}
+
+.centeredContent {
+ width: 100%;
+ max-width: 580;
+}
diff --git a/packages/components/src/modal/index.js b/packages/components/src/modal/index.js
index d82c6928a1073c..ff35faf9222f99 100644
--- a/packages/components/src/modal/index.js
+++ b/packages/components/src/modal/index.js
@@ -2,7 +2,6 @@
* External dependencies
*/
import classnames from 'classnames';
-import { noop } from 'lodash';
/**
* WordPress dependencies
@@ -166,7 +165,6 @@ Modal.defaultProps = {
bodyOpenClassName: 'modal-open',
role: 'dialog',
title: null,
- onRequestClose: noop,
focusOnMount: true,
shouldCloseOnEsc: true,
shouldCloseOnClickOutside: true,
diff --git a/packages/components/src/modal/style.scss b/packages/components/src/modal/style.scss
index 5746f8047a03cd..d877fd2bf16dc4 100644
--- a/packages/components/src/modal/style.scss
+++ b/packages/components/src/modal/style.scss
@@ -42,7 +42,7 @@
// Animate the modal frame/contents appearing on the page.
animation: components-modal__appear-animation 0.1s ease-out;
animation-fill-mode: forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
}
}
diff --git a/packages/components/src/panel/style.scss b/packages/components/src/panel/style.scss
index 5b10bbdf8e358e..220249d69e942a 100644
--- a/packages/components/src/panel/style.scss
+++ b/packages/components/src/panel/style.scss
@@ -64,6 +64,7 @@
margin-top: 0;
margin-bottom: 0;
transition: 0.1s background ease-in-out;
+ @include reduce-motion("transition");
}
.components-panel__body.is-opened > .components-panel__body-title {
margin: -1 * $panel-padding;
@@ -86,6 +87,7 @@
color: $dark-gray-900;
@include menu-style__neutral;
transition: 0.1s background ease-in-out;
+ @include reduce-motion("transition");
&:focus:not(:disabled):not([aria-disabled="true"]) {
@include menu-style__focus;
@@ -99,6 +101,7 @@
color: $dark-gray-900;
fill: currentColor;
transition: 0.1s color ease-in-out;
+ @include reduce-motion("transition");
}
// mirror the arrow horizontally in RTL languages
@@ -114,6 +117,7 @@
.components-panel__icon {
color: $dark-gray-500;
+ filter: grayscale(1) !important;
margin: -2px 0 -2px 6px;
}
diff --git a/packages/components/src/placeholder/README.md b/packages/components/src/placeholder/README.md
index 5d4c64a7f64f19..2a12363e7fde01 100644
--- a/packages/components/src/placeholder/README.md
+++ b/packages/components/src/placeholder/README.md
@@ -11,3 +11,12 @@ const MyPlaceholder = () => (
/>
);
```
+
+### Props
+
+Name | Type | Default | Description
+--- | --- | --- | ---
+`icon` | `string, ReactElement` | `undefined` | If provided, renders an icon next to the label.
+`label` | `string` | `undefined` | Renders a label for the placeholder.
+`instructions` | `string` | `undefined` | Renders instruction text below label.
+`isColumnLayout` | `bool` | `false` | Changes placeholder children layout from flex-row to flex-column.
diff --git a/packages/components/src/placeholder/index.js b/packages/components/src/placeholder/index.js
index e5a5b323f60c7d..8e49b70d5226ec 100644
--- a/packages/components/src/placeholder/index.js
+++ b/packages/components/src/placeholder/index.js
@@ -15,9 +15,9 @@ import Dashicon from '../dashicon';
* @param {Object} props The component props.
* @return {Object} The rendered placeholder.
*/
-function Placeholder( { icon, children, label, instructions, className, notices, preview, ...additionalProps } ) {
+function Placeholder( { icon, children, label, instructions, className, notices, preview, isColumnLayout, ...additionalProps } ) {
const classes = classnames( 'components-placeholder', className );
-
+ const fieldsetClasses = classnames( 'components-placeholder__fieldset', { 'is-column-layout': isColumnLayout } );
return (
{ notices }
@@ -31,7 +31,7 @@ function Placeholder( { icon, children, label, instructions, className, notices,
{ label }
{ !! instructions && { instructions }
}
-
diff --git a/packages/components/src/placeholder/style.scss b/packages/components/src/placeholder/style.scss
index 63938e69498e95..50e1da3d93af56 100644
--- a/packages/components/src/placeholder/style.scss
+++ b/packages/components/src/placeholder/style.scss
@@ -54,6 +54,11 @@
}
}
+.components-placeholder__fieldset.is-column-layout,
+.components-placeholder__fieldset.is-column-layout form {
+ flex-direction: column;
+}
+
.components-placeholder__input {
margin-right: 8px;
flex: 1 1 auto;
diff --git a/packages/components/src/primitives/block-quotation/index.native.js b/packages/components/src/primitives/block-quotation/index.native.js
index eea86435b5ebcd..abbafa09cae949 100644
--- a/packages/components/src/primitives/block-quotation/index.native.js
+++ b/packages/components/src/primitives/block-quotation/index.native.js
@@ -2,15 +2,28 @@
* External dependencies
*/
import { View } from 'react-native';
+/**
+ * WordPress dependencies
+ */
+import { Children, cloneElement } from '@wordpress/element';
/**
* Internal dependencies
*/
import styles from './style.scss';
export const BlockQuotation = ( props ) => {
+ const newChildren = Children.map( props.children, ( child ) => {
+ if ( child && child.props.identifier === 'citation' ) {
+ return cloneElement( child, { style: styles.wpBlockQuoteCitation } );
+ }
+ if ( child && child.props.identifier === 'value' ) {
+ return cloneElement( child, { tagsToEliminate: [ 'div' ] } );
+ }
+ return child;
+ } );
return (
- { props.children }
+ { newChildren }
);
};
diff --git a/packages/components/src/primitives/block-quotation/style.native.scss b/packages/components/src/primitives/block-quotation/style.native.scss
index 02d315e1560b6b..2e88c68cf26fa9 100644
--- a/packages/components/src/primitives/block-quotation/style.native.scss
+++ b/packages/components/src/primitives/block-quotation/style.native.scss
@@ -3,5 +3,10 @@
border-left-color: $black;
border-left-style: solid;
padding-left: 8px;
- margin-left: 8px;
+ margin-left: 0;
+}
+
+.wpBlockQuoteCitation {
+ margin-top: 16px;
+ font-size: 14px;
}
diff --git a/packages/components/src/resizable-box/style.scss b/packages/components/src/resizable-box/style.scss
index f8f2e40cdbde6e..4297038291124a 100644
--- a/packages/components/src/resizable-box/style.scss
+++ b/packages/components/src/resizable-box/style.scss
@@ -39,6 +39,7 @@
top: calc(50% - 4px);
right: calc(50% - 4px);
transition: transform 0.1s ease-in;
+ @include reduce-motion("transition");
opacity: 0;
}
@@ -87,7 +88,7 @@
.components-resizable-box__side-handle.components-resizable-box__handle-bottom:active::before {
animation: components-resizable-box__top-bottom-animation 0.1s ease-out 0s;
animation-fill-mode: forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
}
.components-resizable-box__side-handle.components-resizable-box__handle-left:hover::before,
@@ -96,7 +97,7 @@
.components-resizable-box__side-handle.components-resizable-box__handle-right:active::before {
animation: components-resizable-box__left-right-animation 0.1s ease-out 0s;
animation-fill-mode: forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
}
@keyframes components-resizable-box__top-bottom-animation {
diff --git a/packages/components/src/snackbar/index.js b/packages/components/src/snackbar/index.js
index c5d5674ff408da..a62924219a9b5c 100644
--- a/packages/components/src/snackbar/index.js
+++ b/packages/components/src/snackbar/index.js
@@ -7,7 +7,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
-import { useEffect } from '@wordpress/element';
+import { useEffect, forwardRef } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
/**
@@ -22,7 +22,7 @@ function Snackbar( {
children,
actions = [],
onRemove = noop,
-} ) {
+}, ref ) {
useEffect( () => {
// This rule doesn't account yet for React Hooks
// eslint-disable-next-line @wordpress/react-no-unsafe-timeout
@@ -34,9 +34,17 @@ function Snackbar( {
}, [] );
const classes = classnames( className, 'components-snackbar' );
+ if ( actions && actions.length > 1 ) {
+ // we need to inform developers that snackbar only accepts 1 action
+ // eslint-disable-next-line no-console
+ console.warn( 'Snackbar can only have 1 action, use Notice if your message require many messages' );
+ // return first element only while keeping it inside an array
+ actions = [ actions[ 0 ] ];
+ }
return (
{ label }
@@ -83,4 +88,4 @@ function Snackbar( {
);
}
-export default Snackbar;
+export default forwardRef( Snackbar );
diff --git a/packages/components/src/snackbar/list.js b/packages/components/src/snackbar/list.js
index c7cff210a138a3..b44aa8d24c93a3 100644
--- a/packages/components/src/snackbar/list.js
+++ b/packages/components/src/snackbar/list.js
@@ -3,6 +3,13 @@
*/
import classnames from 'classnames';
import { omit, noop } from 'lodash';
+import { useTransition, animated } from 'react-spring';
+
+/**
+ * WordPress dependencies
+ */
+import { useReducedMotion } from '@wordpress/compose';
+import { useState } from '@wordpress/element';
/**
* Internal dependencies
@@ -20,20 +27,42 @@ import Snackbar from './';
* @return {Object} The rendered notices list.
*/
function SnackbarList( { notices, className, children, onRemove = noop } ) {
+ const isReducedMotion = useReducedMotion();
+ const [ refMap ] = useState( () => new WeakMap() );
+ const transitions = useTransition(
+ notices,
+ ( notice ) => notice.id,
+ {
+ from: { opacity: 0, height: 0 },
+ enter: ( item ) => async ( next ) => await next( { opacity: 1, height: refMap.get( item ).offsetHeight } ),
+ leave: () => async ( next ) => {
+ await next( { opacity: 0 } );
+ await next( { height: 0 } );
+ },
+ immediate: isReducedMotion,
+ }
+ );
+
className = classnames( 'components-snackbar-list', className );
- const removeNotice = ( id ) => () => onRemove( id );
+ const removeNotice = ( notice ) => () => onRemove( notice.id );
return (
{ children }
- { notices.map( ( notice ) => (
-
- { notice.content }
-
+ { transitions.map( ( { item: notice, key, props: style } ) => (
+
+ ref && refMap.set( notice, ref ) }
+ >
+
+ { notice.content }
+
+
+
) ) }
);
diff --git a/packages/components/src/snackbar/style.scss b/packages/components/src/snackbar/style.scss
index 40e8f75837ae6b..4357685ffa0cbd 100644
--- a/packages/components/src/snackbar/style.scss
+++ b/packages/components/src/snackbar/style.scss
@@ -8,7 +8,7 @@
padding: 16px 24px;
width: 100%;
max-width: 600px;
- margin: 8px 0 0;
+ cursor: pointer;
@include break-small() {
width: fit-content;
@@ -55,3 +55,8 @@
z-index: z-index(".components-snackbar-list");
width: 100%;
}
+
+.components-snackbar-list__notice-container {
+ position: relative;
+ padding-top: 8px;
+}
diff --git a/packages/components/src/tab-panel/README.md b/packages/components/src/tab-panel/README.md
index 89c6b095722505..def626516b3a39 100644
--- a/packages/components/src/tab-panel/README.md
+++ b/packages/components/src/tab-panel/README.md
@@ -114,15 +114,15 @@ The function called when a tab has been selected. It is passed the `tabName` as
#### tabs
-A list of tabs where each tab is defined by an object with the following fields:
+An array of objects containing the following properties:
-1. name: String. Defines the key for the tab
-2. title: String. Defines the translated text for the tab
-3. className: String. Defines the class to put on the tab.
+- `name`: `(string)` Defines the key for the tab.
+- `title`:`(string)` Defines the translated text for the tab.
+- `className`:`(string)` Optional. Defines the class to put on the tab.
-Other fields may be added to the object and accessed from the child function if desired.
+>> **Note:** Other fields may be added to the object and accessed from the child function if desired.
-- Type: Array
+- Type: `Array`
- Required: Yes
#### activeClass
diff --git a/packages/components/src/tab-panel/index.js b/packages/components/src/tab-panel/index.js
index 5558272f2ffc4a..71f4683bcbef80 100644
--- a/packages/components/src/tab-panel/index.js
+++ b/packages/components/src/tab-panel/index.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+import classnames from 'classnames';
import { partial, noop, find } from 'lodash';
/**
@@ -74,7 +75,7 @@ class TabPanel extends Component {
className="components-tab-panel__tabs"
>
{ tabs.map( ( tab ) => (
-
# **useMediaQuery**
+
+Runs a media query and returns its value when it changes.
+
+_Parameters_
+
+- _query_ `string`: Media Query.
+
+_Returns_
+
+- `boolean`: return value of the media query.
+
+# **useReducedMotion**
+
+Hook returning whether the user has a preference for reduced motion.
+
+_Returns_
+
+- `boolean`: Reduced motion preference value.
+
# **withGlobalEvents**
Higher-order component creator which, given an object of DOM event types and
diff --git a/packages/compose/package.json b/packages/compose/package.json
index f04e6d753ce11b..57eac024c3f47a 100644
--- a/packages/compose/package.json
+++ b/packages/compose/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/compose",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "WordPress higher-order components (HOCs).",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/compose/src/if-condition/README.md b/packages/compose/src/higher-order/if-condition/README.md
similarity index 100%
rename from packages/compose/src/if-condition/README.md
rename to packages/compose/src/higher-order/if-condition/README.md
diff --git a/packages/compose/src/if-condition/index.js b/packages/compose/src/higher-order/if-condition/index.js
similarity index 86%
rename from packages/compose/src/if-condition/index.js
rename to packages/compose/src/higher-order/if-condition/index.js
index afc45c8e5be2f1..79d23b01ba4218 100644
--- a/packages/compose/src/if-condition/index.js
+++ b/packages/compose/src/higher-order/if-condition/index.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
/**
* Higher-order component creator, creating a new component which renders if
diff --git a/packages/compose/src/pure/index.js b/packages/compose/src/higher-order/pure/index.js
similarity index 93%
rename from packages/compose/src/pure/index.js
rename to packages/compose/src/higher-order/pure/index.js
index 16d0b19e9da705..e721fbb7964796 100644
--- a/packages/compose/src/pure/index.js
+++ b/packages/compose/src/higher-order/pure/index.js
@@ -7,7 +7,7 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
/**
* Given a component returns the enhanced component augmented with a component
diff --git a/packages/compose/src/pure/test/index.js b/packages/compose/src/higher-order/pure/test/index.js
similarity index 100%
rename from packages/compose/src/pure/test/index.js
rename to packages/compose/src/higher-order/pure/test/index.js
diff --git a/packages/compose/src/with-global-events/README.md b/packages/compose/src/higher-order/with-global-events/README.md
similarity index 100%
rename from packages/compose/src/with-global-events/README.md
rename to packages/compose/src/higher-order/with-global-events/README.md
diff --git a/packages/compose/src/with-global-events/index.js b/packages/compose/src/higher-order/with-global-events/index.js
similarity index 96%
rename from packages/compose/src/with-global-events/index.js
rename to packages/compose/src/higher-order/with-global-events/index.js
index bf32f27743232d..ac8aabd4639f6a 100644
--- a/packages/compose/src/with-global-events/index.js
+++ b/packages/compose/src/higher-order/with-global-events/index.js
@@ -11,7 +11,7 @@ import { Component, forwardRef } from '@wordpress/element';
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
import Listener from './listener';
/**
diff --git a/packages/compose/src/with-global-events/listener.js b/packages/compose/src/higher-order/with-global-events/listener.js
similarity index 100%
rename from packages/compose/src/with-global-events/listener.js
rename to packages/compose/src/higher-order/with-global-events/listener.js
diff --git a/packages/compose/src/with-global-events/test/index.js b/packages/compose/src/higher-order/with-global-events/test/index.js
similarity index 100%
rename from packages/compose/src/with-global-events/test/index.js
rename to packages/compose/src/higher-order/with-global-events/test/index.js
diff --git a/packages/compose/src/with-global-events/test/listener.js b/packages/compose/src/higher-order/with-global-events/test/listener.js
similarity index 100%
rename from packages/compose/src/with-global-events/test/listener.js
rename to packages/compose/src/higher-order/with-global-events/test/listener.js
diff --git a/packages/compose/src/with-instance-id/README.md b/packages/compose/src/higher-order/with-instance-id/README.md
similarity index 100%
rename from packages/compose/src/with-instance-id/README.md
rename to packages/compose/src/higher-order/with-instance-id/README.md
diff --git a/packages/compose/src/with-instance-id/index.js b/packages/compose/src/higher-order/with-instance-id/index.js
similarity index 88%
rename from packages/compose/src/with-instance-id/index.js
rename to packages/compose/src/higher-order/with-instance-id/index.js
index 8bc363ddacba4d..53c09ec56f8882 100644
--- a/packages/compose/src/with-instance-id/index.js
+++ b/packages/compose/src/higher-order/with-instance-id/index.js
@@ -6,7 +6,7 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
/**
* A Higher Order Component used to be provide a unique instance ID by
diff --git a/packages/compose/src/with-instance-id/test/index.js b/packages/compose/src/higher-order/with-instance-id/test/index.js
similarity index 100%
rename from packages/compose/src/with-instance-id/test/index.js
rename to packages/compose/src/higher-order/with-instance-id/test/index.js
diff --git a/packages/compose/src/with-safe-timeout/README.md b/packages/compose/src/higher-order/with-safe-timeout/README.md
similarity index 100%
rename from packages/compose/src/with-safe-timeout/README.md
rename to packages/compose/src/higher-order/with-safe-timeout/README.md
diff --git a/packages/compose/src/with-safe-timeout/index.js b/packages/compose/src/higher-order/with-safe-timeout/index.js
similarity index 94%
rename from packages/compose/src/with-safe-timeout/index.js
rename to packages/compose/src/higher-order/with-safe-timeout/index.js
index 5fff43e7bb5dd9..910dd94fddc19d 100644
--- a/packages/compose/src/with-safe-timeout/index.js
+++ b/packages/compose/src/higher-order/with-safe-timeout/index.js
@@ -11,7 +11,7 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
/**
* A higher-order component used to provide and manage delayed function calls
diff --git a/packages/compose/src/with-state/README.md b/packages/compose/src/higher-order/with-state/README.md
similarity index 100%
rename from packages/compose/src/with-state/README.md
rename to packages/compose/src/higher-order/with-state/README.md
diff --git a/packages/compose/src/with-state/index.js b/packages/compose/src/higher-order/with-state/index.js
similarity index 90%
rename from packages/compose/src/with-state/index.js
rename to packages/compose/src/higher-order/with-state/index.js
index e1957482d8c8ee..19e639043bd28b 100644
--- a/packages/compose/src/with-state/index.js
+++ b/packages/compose/src/higher-order/with-state/index.js
@@ -6,7 +6,7 @@ import { Component } from '@wordpress/element';
/**
* Internal dependencies
*/
-import createHigherOrderComponent from '../create-higher-order-component';
+import createHigherOrderComponent from '../../utils/create-higher-order-component';
/**
* A Higher Order Component used to provide and manage internal component state
diff --git a/packages/compose/src/with-state/test/index.js b/packages/compose/src/higher-order/with-state/test/index.js
similarity index 100%
rename from packages/compose/src/with-state/test/index.js
rename to packages/compose/src/higher-order/with-state/test/index.js
diff --git a/packages/compose/src/hooks/use-media-query/index.js b/packages/compose/src/hooks/use-media-query/index.js
new file mode 100644
index 00000000000000..2571da9e23deae
--- /dev/null
+++ b/packages/compose/src/hooks/use-media-query/index.js
@@ -0,0 +1,25 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+
+/**
+ * Runs a media query and returns its value when it changes.
+ *
+ * @param {string} query Media Query.
+ * @return {boolean} return value of the media query.
+ */
+export default function useMediaQuery( query ) {
+ const [ match, setMatch ] = useState( false );
+ useEffect( () => {
+ const updateMatch = () => setMatch( window.matchMedia( query ).matches );
+ updateMatch();
+ const list = window.matchMedia( query );
+ list.addListener( updateMatch );
+ return () => {
+ list.removeListener( updateMatch );
+ };
+ }, [ query ] );
+
+ return match;
+}
diff --git a/packages/compose/src/hooks/use-reduced-motion/index.js b/packages/compose/src/hooks/use-reduced-motion/index.js
new file mode 100644
index 00000000000000..7b1e22792d37c5
--- /dev/null
+++ b/packages/compose/src/hooks/use-reduced-motion/index.js
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import useMediaQuery from '../use-media-query';
+
+/**
+ * Hook returning whether the user has a preference for reduced motion.
+ *
+ * @return {boolean} Reduced motion preference value.
+ */
+const useReducedMotion =
+ process.env.FORCE_REDUCED_MOTION ?
+ () => true :
+ () => useMediaQuery( '(prefers-reduced-motion: reduce)' );
+
+export default useReducedMotion;
diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js
index 379ed6a4ab54de..9ea31b2d351aba 100644
--- a/packages/compose/src/index.js
+++ b/packages/compose/src/index.js
@@ -3,13 +3,8 @@
*/
import { flowRight } from 'lodash';
-export { default as createHigherOrderComponent } from './create-higher-order-component';
-export { default as ifCondition } from './if-condition';
-export { default as pure } from './pure';
-export { default as withGlobalEvents } from './with-global-events';
-export { default as withInstanceId } from './with-instance-id';
-export { default as withSafeTimeout } from './with-safe-timeout';
-export { default as withState } from './with-state';
+// Utils
+export { default as createHigherOrderComponent } from './utils/create-higher-order-component';
/**
* Composes multiple higher-order components into a single higher-order component. Performs right-to-left function
@@ -20,3 +15,15 @@ export { default as withState } from './with-state';
* @return {Function} Returns the new composite function.
*/
export { flowRight as compose };
+
+// Higher-order components
+export { default as ifCondition } from './higher-order/if-condition';
+export { default as pure } from './higher-order/pure';
+export { default as withGlobalEvents } from './higher-order/with-global-events';
+export { default as withInstanceId } from './higher-order/with-instance-id';
+export { default as withSafeTimeout } from './higher-order/with-safe-timeout';
+export { default as withState } from './higher-order/with-state';
+
+// Hooks
+export { default as useMediaQuery } from './hooks/use-media-query';
+export { default as useReducedMotion } from './hooks/use-reduced-motion';
diff --git a/packages/compose/src/create-higher-order-component/index.js b/packages/compose/src/utils/create-higher-order-component/index.js
similarity index 100%
rename from packages/compose/src/create-higher-order-component/index.js
rename to packages/compose/src/utils/create-higher-order-component/index.js
diff --git a/packages/compose/src/create-higher-order-component/test/index.js b/packages/compose/src/utils/create-higher-order-component/test/index.js
similarity index 100%
rename from packages/compose/src/create-higher-order-component/test/index.js
rename to packages/compose/src/utils/create-higher-order-component/test/index.js
diff --git a/packages/core-data/package.json b/packages/core-data/package.json
index 96b18edd614019..55f219865f0e69 100644
--- a/packages/core-data/package.json
+++ b/packages/core-data/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/core-data",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "Access to and manipulation of core WordPress entities.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/custom-templated-path-webpack-plugin/CHANGELOG.md b/packages/custom-templated-path-webpack-plugin/CHANGELOG.md
index 7b34a52f1071ad..d5bb664a3bfe58 100644
--- a/packages/custom-templated-path-webpack-plugin/CHANGELOG.md
+++ b/packages/custom-templated-path-webpack-plugin/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Master
+## 1.4.0 (2019-06-12)
### Bug Fixes
diff --git a/packages/custom-templated-path-webpack-plugin/package.json b/packages/custom-templated-path-webpack-plugin/package.json
index b861d700441de3..463e59991aa46b 100644
--- a/packages/custom-templated-path-webpack-plugin/package.json
+++ b/packages/custom-templated-path-webpack-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/custom-templated-path-webpack-plugin",
- "version": "1.3.0",
+ "version": "1.4.0",
"description": "Webpack plugin for creating custom path template tags.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -24,6 +24,7 @@
"files": [
"index.js"
],
+ "main": "index.js",
"dependencies": {
"escape-string-regexp": "^1.0.5"
},
diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json
index 9ffbf80cd5165e..e54574c9eec93b 100644
--- a/packages/data-controls/package.json
+++ b/packages/data-controls/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/data-controls",
- "version": "1.0.0-beta.1",
+ "version": "1.0.0",
"description": "A set of common controls for the @wordpress/data api.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md
index 391d68abe4c463..a7a342725906e2 100644
--- a/packages/data/CHANGELOG.md
+++ b/packages/data/CHANGELOG.md
@@ -1,12 +1,14 @@
-## Master
+## 4.6.0 (2019-06-12)
### New Feature
- Expose `useSelect` hook for usage in functional components. ([#15737](https://github.com/WordPress/gutenberg/pull/15737))
+- Expose `useDispatch` hook for usage in functional components. ([#15896](https://github.com/WordPress/gutenberg/pull/15896))
### Enhancements
- `withSelect` internally uses the new `useSelect` hook. ([#15737](https://github.com/WordPress/gutenberg/pull/15737). **Note:** This _could_ impact performance of code using `withSelect` in edge-cases. To avoid impact, memoize passed in `mapSelectToProps` callbacks or implement `useSelect` directly with dependencies.
+- `withDispatch` internally uses a new `useDispatchWithMap` hook (an internal only api) ([#15896](https://github.com/WordPress/gutenberg/pull/15896))
## 4.5.0 (2019-05-21)
diff --git a/packages/data/README.md b/packages/data/README.md
index a5bdfd30a5c375..e126a85781e4a6 100644
--- a/packages/data/README.md
+++ b/packages/data/README.md
@@ -358,7 +358,15 @@ _Returns_
# **plugins**
-Undocumented declaration.
+Object of available plugins to use with a registry.
+
+_Related_
+
+- [use](#use)
+
+_Type_
+
+- `Object`
# **registerGenericStore**
@@ -470,7 +478,59 @@ _Parameters_
# **use**
-Undocumented declaration.
+Extends a registry to inherit functionality provided by a given plugin. A
+plugin is an object with properties aligning to that of a registry, merged
+to extend the default registry behavior.
+
+_Parameters_
+
+- _plugin_ `Object`: Plugin object.
+
+# **useDispatch**
+
+A custom react hook returning the current registry dispatch actions creators.
+
+Note: The component using this hook must be within the context of a
+RegistryProvider.
+
+_Usage_
+
+This illustrates a pattern where you may need to retrieve dynamic data from
+the server via the `useSelect` hook to use in combination with the dispatch
+action.
+
+```jsx
+const { useDispatch, useSelect } = wp.data;
+const { useCallback } = wp.element;
+
+function Button( { onClick, children } ) {
+ return { children }
+}
+
+const SaleButton = ( { children } ) => {
+ const { stockNumber } = useSelect(
+ ( select ) => select( 'my-shop' ).getStockNumber()
+ );
+ const { startSale } = useDispatch( 'my-shop' );
+ const onClick = useCallback( () => {
+ const discountPercent = stockNumber > 50 ? 10: 20;
+ startSale( discountPercent );
+ }, [ stockNumber ] );
+ return { children }
+}
+
+// Rendered somewhere in the application:
+//
+// Start Sale!
+```
+
+_Parameters_
+
+- _storeName_ `[string]`: Optionally provide the name of the store from which to retrieve action creators. If not provided, the registry.dispatch function is returned instead.
+
+_Returns_
+
+- `Function`: A custom react hook.
# **useRegistry**
@@ -559,53 +619,64 @@ _Returns_
# **withDispatch**
-Higher-order component used to add dispatch props using registered action creators.
+Higher-order component used to add dispatch props using registered action
+creators.
_Usage_
```jsx
function Button( { onClick, children } ) {
- return { children } ;
+ return { children } ;
}
const { withDispatch } = wp.data;
const SaleButton = withDispatch( ( dispatch, ownProps ) => {
- const { startSale } = dispatch( 'my-shop' );
- const { discountPercent } = ownProps;
-
- return {
- onClick() {
- startSale( discountPercent );
- },
- };
+ const { startSale } = dispatch( 'my-shop' );
+ const { discountPercent } = ownProps;
+
+ return {
+ onClick() {
+ startSale( discountPercent );
+ },
+ };
} )( Button );
// Rendered in the application:
//
-// Start Sale!
+// Start Sale!
```
-In the majority of cases, it will be sufficient to use only two first params passed to `mapDispatchToProps` as illustrated in the previous example. However, there might be some very advanced use cases where using the `registry` object might be used as a tool to optimize the performance of your component. Using `select` function from the registry might be useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the `withSelect` higher order component to compute such prop, which might lead to unnecessary re-renders of your component caused by its frequent value change. Keep in mind, that `mapDispatchToProps` must return an object with functions only.
+In the majority of cases, it will be sufficient to use only two first params
+passed to `mapDispatchToProps` as illustrated in the previous example.
+However, there might be some very advanced use cases where using the
+`registry` object might be used as a tool to optimize the performance of
+your component. Using `select` function from the registry might be useful
+when you need to fetch some dynamic data from the store at the time when the
+event is fired, but at the same time, you never use it to render your
+component. In such scenario, you can avoid using the `withSelect` higher
+order component to compute such prop, which might lead to unnecessary
+re-renders of your component caused by its frequent value change.
+Keep in mind, that `mapDispatchToProps` must return an object with functions
+only.
```jsx
function Button( { onClick, children } ) {
- return { children } ;
+ return { children } ;
}
const { withDispatch } = wp.data;
const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => {
- // Stock number changes frequently.
- const { getStockNumber } = select( 'my-shop' );
- const { startSale } = dispatch( 'my-shop' );
-
- return {
- onClick() {
- const dicountPercent = getStockNumber() > 50 ? 10 : 20;
- startSale( discountPercent );
- },
- };
+ // Stock number changes frequently.
+ const { getStockNumber } = select( 'my-shop' );
+ const { startSale } = dispatch( 'my-shop' );
+ return {
+ onClick() {
+ const discountPercent = getStockNumber() > 50 ? 10 : 20;
+ startSale( discountPercent );
+ },
+ };
} )( Button );
// Rendered in the application:
@@ -613,11 +684,13 @@ const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => {
// Start Sale!
```
-_Note:_ It is important that the `mapDispatchToProps` function always returns an object with the same keys. For example, it should not contain conditions under which a different value would be returned.
+_Note:_ It is important that the `mapDispatchToProps` function always
+returns an object with the same keys. For example, it should not contain
+conditions under which a different value would be returned.
_Parameters_
-- _mapDispatchToProps_ `Object`: Object of prop names where value is a dispatch-bound action creator, or a function to be called with the component's props and returning an action creator.
+- _mapDispatchToProps_ `Function`: A function of returning an object of prop names where value is a dispatch-bound action creator, or a function to be called with the component's props and returning an action creator.
_Returns_
diff --git a/packages/data/package.json b/packages/data/package.json
index d0f2d74f354fc6..30d331281569c7 100644
--- a/packages/data/package.json
+++ b/packages/data/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/data",
- "version": "4.5.0",
+ "version": "4.6.0",
"description": "Data module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/data/src/components/use-dispatch/index.js b/packages/data/src/components/use-dispatch/index.js
new file mode 100644
index 00000000000000..f4f4fed37d38ca
--- /dev/null
+++ b/packages/data/src/components/use-dispatch/index.js
@@ -0,0 +1,2 @@
+export { default as useDispatch } from './use-dispatch';
+export { default as useDispatchWithMap } from './use-dispatch-with-map';
diff --git a/packages/data/src/components/use-dispatch/test/use-dispatch.js b/packages/data/src/components/use-dispatch/test/use-dispatch.js
new file mode 100644
index 00000000000000..8770d04144b06d
--- /dev/null
+++ b/packages/data/src/components/use-dispatch/test/use-dispatch.js
@@ -0,0 +1,137 @@
+/**
+ * External dependencies
+ */
+import TestRenderer, { act } from 'react-test-renderer';
+
+/**
+ * Internal dependencies
+ */
+import useDispatch from '../use-dispatch';
+import { createRegistry } from '../../../registry';
+import { RegistryProvider } from '../../registry-provider';
+
+describe( 'useDispatch', () => {
+ let registry;
+ beforeEach( () => {
+ registry = createRegistry();
+ } );
+
+ it( 'returns dispatch function from store with no store name provided', () => {
+ registry.registerStore( 'demoStore', {
+ reducer: ( state ) => state,
+ actions: {
+ foo: () => 'bar',
+ },
+ } );
+ const TestComponent = () => {
+ return
;
+ };
+ const Component = () => {
+ const dispatch = useDispatch();
+ return ;
+ };
+
+ let testRenderer;
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
+
+ const testInstance = testRenderer.root;
+
+ expect( testInstance.findByType( TestComponent ).props.dispatch )
+ .toBe( registry.dispatch );
+ } );
+ it( 'returns expected action creators from store for given storeName', () => {
+ const noop = () => ( { type: '__INERT__' } );
+ const testAction = jest.fn().mockImplementation( noop );
+ registry.registerStore( 'demoStore', {
+ reducer: ( state ) => state,
+ actions: {
+ foo: testAction,
+ },
+ } );
+ const TestComponent = () => {
+ const { foo } = useDispatch( 'demoStore' );
+ return ;
+ };
+
+ let testRenderer;
+
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
+
+ const testInstance = testRenderer.root;
+
+ act( () => {
+ testInstance.findByType( 'button' ).props.onClick();
+ } );
+
+ expect( testAction ).toHaveBeenCalledTimes( 1 );
+ } );
+ it( 'returns dispatch from correct registry if registries change', () => {
+ const reducer = ( state ) => state;
+ const noop = () => ( { type: '__INERT__' } );
+ const firstRegistryAction = jest.fn().mockImplementation( noop );
+ const secondRegistryAction = jest.fn().mockImplementation( noop );
+
+ const firstRegistry = registry;
+ firstRegistry.registerStore( 'demo', {
+ reducer,
+ actions: {
+ noop: firstRegistryAction,
+ },
+ } );
+
+ const TestComponent = () => {
+ const dispatch = useDispatch();
+ return dispatch( 'demo' ).noop() } />;
+ };
+
+ let testRenderer;
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
+ const testInstance = testRenderer.root;
+
+ act( () => {
+ testInstance.findByType( 'button' ).props.onClick();
+ } );
+
+ expect( firstRegistryAction ).toHaveBeenCalledTimes( 1 );
+ expect( secondRegistryAction ).toHaveBeenCalledTimes( 0 );
+
+ const secondRegistry = createRegistry();
+ secondRegistry.registerStore( 'demo', {
+ reducer,
+ actions: {
+ noop: secondRegistryAction,
+ },
+ } );
+
+ act( () => {
+ testRenderer.update(
+
+
+
+ );
+ } );
+ act( () => {
+ testInstance.findByType( 'button' ).props.onClick();
+ } );
+ expect( firstRegistryAction ).toHaveBeenCalledTimes( 1 );
+ expect( secondRegistryAction ).toHaveBeenCalledTimes( 1 );
+ } );
+} );
diff --git a/packages/data/src/components/use-dispatch/use-dispatch-with-map.js b/packages/data/src/components/use-dispatch/use-dispatch-with-map.js
new file mode 100644
index 00000000000000..057ddab06191e9
--- /dev/null
+++ b/packages/data/src/components/use-dispatch/use-dispatch-with-map.js
@@ -0,0 +1,71 @@
+/**
+ * External dependencies
+ */
+import { mapValues } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { useMemo, useRef, useEffect, useLayoutEffect } from '@wordpress/element';
+
+/**
+ * Internal dependencies
+ */
+import useRegistry from '../registry-provider/use-registry';
+
+/**
+ * Favor useLayoutEffect to ensure the store subscription callback always has
+ * the dispatchMap from the latest render. If a store update happens between
+ * render and the effect, this could cause missed/stale updates or
+ * inconsistent state.
+ *
+ * Fallback to useEffect for server rendered components because currently React
+ * throws a warning when using useLayoutEffect in that environment.
+ */
+const useIsomorphicLayoutEffect =
+ typeof window !== 'undefined' ? useLayoutEffect : useEffect;
+
+/**
+ * Custom react hook for returning aggregate dispatch actions using the provided
+ * dispatchMap.
+ *
+ * Currently this is an internal api only and is implemented by `withDispatch`
+ *
+ * @param {Function} dispatchMap Receives the `registry.dispatch` function as
+ * the first argument and the `registry` object
+ * as the second argument. Should return an
+ * object mapping props to functions.
+ * @param {Array} deps An array of dependencies for the hook.
+ * @return {Object} An object mapping props to functions created by the passed
+ * in dispatchMap.
+ */
+const useDispatchWithMap = ( dispatchMap, deps ) => {
+ const registry = useRegistry();
+ const currentDispatchMap = useRef( dispatchMap );
+
+ useIsomorphicLayoutEffect( () => {
+ currentDispatchMap.current = dispatchMap;
+ } );
+
+ return useMemo( () => {
+ const currentDispatchProps = currentDispatchMap.current(
+ registry.dispatch,
+ registry
+ );
+ return mapValues(
+ currentDispatchProps,
+ ( dispatcher, propName ) => {
+ if ( typeof dispatcher !== 'function' ) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ `Property ${ propName } returned from dispatchMap in useDispatchWithMap must be a function.`
+ );
+ }
+ return ( ...args ) => currentDispatchMap
+ .current( registry.dispatch, registry )[ propName ]( ...args );
+ }
+ );
+ }, [ registry, ...deps ] );
+};
+
+export default useDispatchWithMap;
diff --git a/packages/data/src/components/use-dispatch/use-dispatch.js b/packages/data/src/components/use-dispatch/use-dispatch.js
new file mode 100644
index 00000000000000..985869d8057fb2
--- /dev/null
+++ b/packages/data/src/components/use-dispatch/use-dispatch.js
@@ -0,0 +1,53 @@
+/**
+ * Internal dependencies
+ */
+import useRegistry from '../registry-provider/use-registry';
+
+/**
+ * A custom react hook returning the current registry dispatch actions creators.
+ *
+ * Note: The component using this hook must be within the context of a
+ * RegistryProvider.
+ *
+ * @param {string} [storeName] Optionally provide the name of the store from
+ * which to retrieve action creators. If not
+ * provided, the registry.dispatch function is
+ * returned instead.
+ *
+ * @example
+ * This illustrates a pattern where you may need to retrieve dynamic data from
+ * the server via the `useSelect` hook to use in combination with the dispatch
+ * action.
+ *
+ * ```jsx
+ * const { useDispatch, useSelect } = wp.data;
+ * const { useCallback } = wp.element;
+ *
+ * function Button( { onClick, children } ) {
+ * return { children }
+ * }
+ *
+ * const SaleButton = ( { children } ) => {
+ * const { stockNumber } = useSelect(
+ * ( select ) => select( 'my-shop' ).getStockNumber()
+ * );
+ * const { startSale } = useDispatch( 'my-shop' );
+ * const onClick = useCallback( () => {
+ * const discountPercent = stockNumber > 50 ? 10: 20;
+ * startSale( discountPercent );
+ * }, [ stockNumber ] );
+ * return { children }
+ * }
+ *
+ * // Rendered somewhere in the application:
+ * //
+ * // Start Sale!
+ * ```
+ * @return {Function} A custom react hook.
+ */
+const useDispatch = ( storeName ) => {
+ const { dispatch } = useRegistry();
+ return storeName === void 0 ? dispatch : dispatch( storeName );
+};
+
+export default useDispatch;
diff --git a/packages/data/src/components/with-dispatch/index.js b/packages/data/src/components/with-dispatch/index.js
index 1da0aa95f3de7a..2247a253a189d5 100644
--- a/packages/data/src/components/with-dispatch/index.js
+++ b/packages/data/src/components/with-dispatch/index.js
@@ -1,138 +1,101 @@
-/**
- * External dependencies
- */
-import { mapValues } from 'lodash';
-
/**
* WordPress dependencies
*/
-import { Component } from '@wordpress/element';
import { createHigherOrderComponent } from '@wordpress/compose';
/**
* Internal dependencies
*/
-import { RegistryConsumer } from '../registry-provider';
+import { useDispatchWithMap } from '../use-dispatch';
/**
- * Higher-order component used to add dispatch props using registered action creators.
+ * Higher-order component used to add dispatch props using registered action
+ * creators.
*
- * @param {Object} mapDispatchToProps Object of prop names where value is a
- * dispatch-bound action creator, or a
- * function to be called with the
- * component's props and returning an
- * action creator.
+ * @param {Function} mapDispatchToProps A function of returning an object of
+ * prop names where value is a
+ * dispatch-bound action creator, or a
+ * function to be called with the
+ * component's props and returning an
+ * action creator.
*
* @example
* ```jsx
* function Button( { onClick, children } ) {
- * return { children } ;
+ * return { children } ;
* }
*
* const { withDispatch } = wp.data;
*
* const SaleButton = withDispatch( ( dispatch, ownProps ) => {
- * const { startSale } = dispatch( 'my-shop' );
- * const { discountPercent } = ownProps;
+ * const { startSale } = dispatch( 'my-shop' );
+ * const { discountPercent } = ownProps;
*
- * return {
- * onClick() {
- * startSale( discountPercent );
- * },
- * };
+ * return {
+ * onClick() {
+ * startSale( discountPercent );
+ * },
+ * };
* } )( Button );
*
* // Rendered in the application:
* //
- * // Start Sale!
+ * // Start Sale!
* ```
*
* @example
- * In the majority of cases, it will be sufficient to use only two first params passed to `mapDispatchToProps` as illustrated in the previous example. However, there might be some very advanced use cases where using the `registry` object might be used as a tool to optimize the performance of your component. Using `select` function from the registry might be useful when you need to fetch some dynamic data from the store at the time when the event is fired, but at the same time, you never use it to render your component. In such scenario, you can avoid using the `withSelect` higher order component to compute such prop, which might lead to unnecessary re-renders of your component caused by its frequent value change. Keep in mind, that `mapDispatchToProps` must return an object with functions only.
+ * In the majority of cases, it will be sufficient to use only two first params
+ * passed to `mapDispatchToProps` as illustrated in the previous example.
+ * However, there might be some very advanced use cases where using the
+ * `registry` object might be used as a tool to optimize the performance of
+ * your component. Using `select` function from the registry might be useful
+ * when you need to fetch some dynamic data from the store at the time when the
+ * event is fired, but at the same time, you never use it to render your
+ * component. In such scenario, you can avoid using the `withSelect` higher
+ * order component to compute such prop, which might lead to unnecessary
+ * re-renders of your component caused by its frequent value change.
+ * Keep in mind, that `mapDispatchToProps` must return an object with functions
+ * only.
*
* ```jsx
* function Button( { onClick, children } ) {
- * return { children } ;
+ * return { children } ;
* }
*
* const { withDispatch } = wp.data;
*
* const SaleButton = withDispatch( ( dispatch, ownProps, { select } ) => {
- * // Stock number changes frequently.
- * const { getStockNumber } = select( 'my-shop' );
- * const { startSale } = dispatch( 'my-shop' );
- *
- * return {
- * onClick() {
- * const dicountPercent = getStockNumber() > 50 ? 10 : 20;
- * startSale( discountPercent );
- * },
- * };
+ * // Stock number changes frequently.
+ * const { getStockNumber } = select( 'my-shop' );
+ * const { startSale } = dispatch( 'my-shop' );
+ * return {
+ * onClick() {
+ * const discountPercent = getStockNumber() > 50 ? 10 : 20;
+ * startSale( discountPercent );
+ * },
+ * };
* } )( Button );
*
* // Rendered in the application:
* //
* // Start Sale!
* ```
- * _Note:_ It is important that the `mapDispatchToProps` function always returns an object with the same keys. For example, it should not contain conditions under which a different value would be returned.
+ *
+ * _Note:_ It is important that the `mapDispatchToProps` function always
+ * returns an object with the same keys. For example, it should not contain
+ * conditions under which a different value would be returned.
*
* @return {Component} Enhanced component with merged dispatcher props.
*/
const withDispatch = ( mapDispatchToProps ) => createHigherOrderComponent(
- ( WrappedComponent ) => {
- class ComponentWithDispatch extends Component {
- constructor( props ) {
- super( ...arguments );
-
- this.proxyProps = {};
-
- this.setProxyProps( props );
- }
-
- proxyDispatch( propName, ...args ) {
- // Original dispatcher is a pre-bound (dispatching) action creator.
- mapDispatchToProps( this.props.registry.dispatch, this.props.ownProps, this.props.registry )[ propName ]( ...args );
- }
-
- setProxyProps( props ) {
- // Assign as instance property so that in subsequent render
- // reconciliation, the prop values are referentially equal.
- // Importantly, note that while `mapDispatchToProps` is
- // called, it is done only to determine the keys for which
- // proxy functions should be created. The actual registry
- // dispatch does not occur until the function is called.
- const propsToDispatchers = mapDispatchToProps( this.props.registry.dispatch, props.ownProps, this.props.registry );
- this.proxyProps = mapValues( propsToDispatchers, ( dispatcher, propName ) => {
- if ( typeof dispatcher !== 'function' ) {
- // eslint-disable-next-line no-console
- console.warn( `Property ${ propName } returned from mapDispatchToProps in withDispatch must be a function.` );
- }
- // Prebind with prop name so we have reference to the original
- // dispatcher to invoke. Track between re-renders to avoid
- // creating new function references every render.
- if ( this.proxyProps.hasOwnProperty( propName ) ) {
- return this.proxyProps[ propName ];
- }
-
- return this.proxyDispatch.bind( this, propName );
- } );
- }
-
- render() {
- return ;
- }
- }
-
- return ( ownProps ) => (
-
- { ( registry ) => (
-
- ) }
-
+ ( WrappedComponent ) => ( ownProps ) => {
+ const mapDispatch = ( dispatch, registry ) => mapDispatchToProps(
+ dispatch,
+ ownProps,
+ registry
);
+ const dispatchProps = useDispatchWithMap( mapDispatch, [] );
+ return ;
},
'withDispatch'
);
diff --git a/packages/data/src/components/with-dispatch/test/index.js b/packages/data/src/components/with-dispatch/test/index.js
index 4bde9b30810ac1..66fba4028e84f8 100644
--- a/packages/data/src/components/with-dispatch/test/index.js
+++ b/packages/data/src/components/with-dispatch/test/index.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import TestRenderer from 'react-test-renderer';
+import TestRenderer, { act } from 'react-test-renderer';
/**
* Internal dependencies
@@ -45,27 +45,34 @@ describe( 'withDispatch', () => {
};
} )( ( props ) => );
- const testRenderer = TestRenderer.create(
-
-
-
- );
+ let testRenderer;
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
const testInstance = testRenderer.root;
const incrementBeforeSetProps = testInstance.findByType( 'button' ).props.onClick;
// Verify that dispatch respects props at the time of being invoked by
// changing props after the initial mount.
- testRenderer.update(
-
-
-
- );
+ act( () => {
+ testRenderer.update(
+
+
+
+ );
+ } );
// Function value reference should not have changed in props update.
expect( testInstance.findByType( 'button' ).props.onClick ).toBe( incrementBeforeSetProps );
- incrementBeforeSetProps();
+ act( () => {
+ incrementBeforeSetProps();
+ } );
expect( store.getState() ).toBe( 2 );
} );
@@ -95,14 +102,19 @@ describe( 'withDispatch', () => {
};
} )( ( props ) => );
- const testRenderer = TestRenderer.create(
-
-
-
- );
+ let testRenderer;
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
const testInstance = testRenderer.root;
- testInstance.findByType( 'button' ).props.onClick();
+ act( () => {
+ testInstance.findByType( 'button' ).props.onClick();
+ } );
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 0 );
@@ -114,13 +126,16 @@ describe( 'withDispatch', () => {
},
} );
- testRenderer.update(
-
-
-
- );
-
- testInstance.findByType( 'button' ).props.onClick();
+ act( () => {
+ testRenderer.update(
+
+
+
+ );
+ } );
+ act( () => {
+ testInstance.findByType( 'button' ).props.onClick();
+ } );
expect( firstRegistryAction ).toHaveBeenCalledTimes( 2 );
expect( secondRegistryAction ).toHaveBeenCalledTimes( 2 );
} );
@@ -159,21 +174,30 @@ describe( 'withDispatch', () => {
};
} )( ( props ) => );
- const testRenderer = TestRenderer.create(
-
-
-
- );
+ let testRenderer;
+ act( () => {
+ testRenderer = TestRenderer.create(
+
+
+
+ );
+ } );
const counterUpdateHandler = testRenderer.root.findByType( 'button' ).props.onClick;
- counterUpdateHandler();
+ act( () => {
+ counterUpdateHandler();
+ } );
expect( store.getState() ).toBe( 1 );
- counterUpdateHandler();
+ act( () => {
+ counterUpdateHandler();
+ } );
expect( store.getState() ).toBe( 2 );
- counterUpdateHandler();
+ act( () => {
+ counterUpdateHandler();
+ } );
expect( store.getState() ).toBe( 3 );
} );
@@ -184,13 +208,15 @@ describe( 'withDispatch', () => {
};
} )( () => null );
- TestRenderer.create(
-
-
-
- );
+ act( () => {
+ TestRenderer.create(
+
+
+
+ );
+ } );
expect( console ).toHaveWarnedWith(
- 'Property count returned from mapDispatchToProps in withDispatch must be a function.'
+ 'Property count returned from dispatchMap in useDispatchWithMap must be a function.'
);
} );
} );
diff --git a/packages/data/src/index.js b/packages/data/src/index.js
index 46c830289b91d7..da00faec053d41 100644
--- a/packages/data/src/index.js
+++ b/packages/data/src/index.js
@@ -18,13 +18,22 @@ export {
useRegistry,
} from './components/registry-provider';
export { default as useSelect } from './components/use-select';
+export { useDispatch } from './components/use-dispatch';
export {
AsyncModeProvider as __experimentalAsyncModeProvider,
} from './components/async-mode-provider';
export { createRegistry } from './registry';
-export { plugins };
export { createRegistrySelector, createRegistryControl } from './factory';
+/**
+ * Object of available plugins to use with a registry.
+ *
+ * @see [use](#use)
+ *
+ * @type {Object}
+ */
+export { plugins };
+
/**
* The combineReducers helper function turns an object whose values are different
* reducing functions into a single reducing function you can pass to registerReducer.
@@ -140,4 +149,12 @@ export const registerGenericStore = defaultRegistry.registerGenericStore;
* @return {Object} Registered store object.
*/
export const registerStore = defaultRegistry.registerStore;
+
+/**
+ * Extends a registry to inherit functionality provided by a given plugin. A
+ * plugin is an object with properties aligning to that of a registry, merged
+ * to extend the default registry behavior.
+ *
+ * @param {Object} plugin Plugin object.
+ */
export const use = defaultRegistry.use;
diff --git a/packages/deprecated/package.json b/packages/deprecated/package.json
index 033c6ae472cf83..4315c6d81039aa 100644
--- a/packages/deprecated/package.json
+++ b/packages/deprecated/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/deprecated",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "Deprecation utility for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/dom-ready/package.json b/packages/dom-ready/package.json
index 8213415b081db5..827accd9b47dd7 100644
--- a/packages/dom-ready/package.json
+++ b/packages/dom-ready/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/dom-ready",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "Execute callback after the DOM is loaded.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -19,6 +19,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
+ "react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.4.4"
},
diff --git a/packages/e2e-test-utils/README.md b/packages/e2e-test-utils/README.md
index 7cb439d46fc4eb..d8f14e5e6c343b 100644
--- a/packages/e2e-test-utils/README.md
+++ b/packages/e2e-test-utils/README.md
@@ -141,6 +141,21 @@ _Parameters_
Disables Pre-publish checks.
+# **dragAndResize**
+
+Clicks an element, drags a particular distance and releases the mouse button.
+
+_Parameters_
+
+- _element_ `Object`: The puppeteer element handle.
+- _delta_ `Object`: Object containing movement distances.
+- _delta.x_ `number`: Horizontal distance to drag.
+- _delta.y_ `number`: Vertical distance to drag.
+
+_Returns_
+
+- `Promise`: Promise resolving when drag completes.
+
# **enablePageDialogAccept**
Enables even listener which accepts a page dialog which
@@ -192,7 +207,7 @@ _Returns_
# **getAllBlocks**
-Returns an array with all blocks; Equivalent to calling wp.data.select( 'core/editor' ).getBlocks();
+Returns an array with all blocks; Equivalent to calling wp.data.select( 'core/block-editor' ).getBlocks();
_Returns_
diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json
index 4ab119dbc0557c..ab69632a58f6ee 100644
--- a/packages/e2e-test-utils/package.json
+++ b/packages/e2e-test-utils/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/e2e-test-utils",
- "version": "2.0.0",
+ "version": "2.1.0",
"description": "End-To-End (E2E) test utils for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/e2e-test-utils/src/click-on-more-menu-item.js b/packages/e2e-test-utils/src/click-on-more-menu-item.js
index 0465441ec64965..2e9ef6c946499b 100644
--- a/packages/e2e-test-utils/src/click-on-more-menu-item.js
+++ b/packages/e2e-test-utils/src/click-on-more-menu-item.js
@@ -10,7 +10,7 @@ import { first } from 'lodash';
*/
export async function clickOnMoreMenuItem( buttonLabel ) {
await expect( page ).toClick(
- '.edit-post-more-menu [aria-label="Show more tools & options"]'
+ '.edit-post-more-menu [aria-label="More tools & options"]'
);
const moreMenuContainerSelector =
'//*[contains(concat(" ", @class, " "), " edit-post-more-menu__content ")]';
diff --git a/packages/e2e-test-utils/src/drag-and-resize.js b/packages/e2e-test-utils/src/drag-and-resize.js
new file mode 100644
index 00000000000000..f8f0b3e996b261
--- /dev/null
+++ b/packages/e2e-test-utils/src/drag-and-resize.js
@@ -0,0 +1,26 @@
+/**
+ * Clicks an element, drags a particular distance and releases the mouse button.
+ *
+ * @param {Object} element The puppeteer element handle.
+ * @param {Object} delta Object containing movement distances.
+ * @param {number} delta.x Horizontal distance to drag.
+ * @param {number} delta.y Vertical distance to drag.
+ *
+ * @return {Promise} Promise resolving when drag completes.
+ */
+export async function dragAndResize( element, delta ) {
+ const {
+ x: elementX,
+ y: elementY,
+ width: elementWidth,
+ height: elementHeight,
+ } = await element.boundingBox();
+
+ const originX = elementX + ( elementWidth / 2 );
+ const originY = elementY + ( elementHeight / 2 );
+
+ await page.mouse.move( originX, originY );
+ await page.mouse.down();
+ await page.mouse.move( originX + delta.x, originY + delta.y );
+ await page.mouse.up();
+}
diff --git a/packages/e2e-test-utils/src/get-all-blocks.js b/packages/e2e-test-utils/src/get-all-blocks.js
index 6f87f827615cba..7aca29bbdcecc2 100644
--- a/packages/e2e-test-utils/src/get-all-blocks.js
+++ b/packages/e2e-test-utils/src/get-all-blocks.js
@@ -4,10 +4,10 @@
import { wpDataSelect } from './wp-data-select';
/**
- * Returns an array with all blocks; Equivalent to calling wp.data.select( 'core/editor' ).getBlocks();
+ * Returns an array with all blocks; Equivalent to calling wp.data.select( 'core/block-editor' ).getBlocks();
*
* @return {Promise} Promise resolving with an array containing all blocks in the document.
*/
export function getAllBlocks() {
- return wpDataSelect( 'core/editor', 'getBlocks' );
+ return wpDataSelect( 'core/block-editor', 'getBlocks' );
}
diff --git a/packages/e2e-test-utils/src/index.js b/packages/e2e-test-utils/src/index.js
index e984658a99f6df..64e0a22a585b1e 100644
--- a/packages/e2e-test-utils/src/index.js
+++ b/packages/e2e-test-utils/src/index.js
@@ -10,6 +10,7 @@ export { createNewPost } from './create-new-post';
export { createURL } from './create-url';
export { deactivatePlugin } from './deactivate-plugin';
export { disablePrePublishChecks } from './disable-pre-publish-checks';
+export { dragAndResize } from './drag-and-resize';
export { enablePageDialogAccept } from './enable-page-dialog-accept';
export { enablePrePublishChecks } from './enable-pre-publish-checks';
export { ensureSidebarOpened } from './ensure-sidebar-opened';
diff --git a/packages/e2e-test-utils/src/select-block-by-client-id.js b/packages/e2e-test-utils/src/select-block-by-client-id.js
index cf7e4624d621fc..cfcf8b2bc54d5f 100644
--- a/packages/e2e-test-utils/src/select-block-by-client-id.js
+++ b/packages/e2e-test-utils/src/select-block-by-client-id.js
@@ -5,6 +5,6 @@
*/
export async function selectBlockByClientId( clientId ) {
await page.evaluate( ( id ) => {
- wp.data.dispatch( 'core/editor' ).selectBlock( id );
+ wp.data.dispatch( 'core/block-editor' ).selectBlock( id );
}, clientId );
}
diff --git a/packages/e2e-test-utils/src/set-post-content.js b/packages/e2e-test-utils/src/set-post-content.js
index 62398b646fc532..18e47c55820033 100644
--- a/packages/e2e-test-utils/src/set-post-content.js
+++ b/packages/e2e-test-utils/src/set-post-content.js
@@ -8,6 +8,6 @@ export async function setPostContent( content ) {
return await page.evaluate( ( _content ) => {
const { dispatch } = window.wp.data;
const blocks = wp.blocks.parse( _content );
- dispatch( 'core/editor' ).resetBlocks( blocks );
+ dispatch( 'core/block-editor' ).resetBlocks( blocks );
}, content );
}
diff --git a/packages/e2e-test-utils/src/switch-editor-mode-to.js b/packages/e2e-test-utils/src/switch-editor-mode-to.js
index 6978b2ee812472..c0ddfa274c9c2b 100644
--- a/packages/e2e-test-utils/src/switch-editor-mode-to.js
+++ b/packages/e2e-test-utils/src/switch-editor-mode-to.js
@@ -5,7 +5,7 @@
*/
export async function switchEditorModeTo( mode ) {
await page.click(
- '.edit-post-more-menu [aria-label="Show more tools & options"]'
+ '.edit-post-more-menu [aria-label="More tools & options"]'
);
const [ button ] = await page.$x(
`//button[contains(text(), '${ mode } Editor')]`
diff --git a/packages/e2e-tests/fixtures/block-transforms.js b/packages/e2e-tests/fixtures/block-transforms.js
index 79d4d7bc804566..723552c024b545 100644
--- a/packages/e2e-tests/fixtures/block-transforms.js
+++ b/packages/e2e-tests/fixtures/block-transforms.js
@@ -1,131 +1,166 @@
export const EXPECTED_TRANSFORMS = {
core__archives: {
originalBlock: 'Archives',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__archives__showPostCounts: {
originalBlock: 'Archives',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__audio: {
originalBlock: 'Audio',
availableTransforms: [
'File',
+ 'Group',
],
},
core__button__center: {
originalBlock: 'Button',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__calendar: {
originalBlock: 'Calendar',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__media-text': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Image',
],
},
'core__media-text__image-alt-no-align': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Image',
],
},
'core__media-text__image-fill-no-focal-point-selected': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Image',
],
},
'core__media-text__image-fill-with-focal-point-selected': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Image',
],
},
'core__media-text__is-stacked-on-mobile': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Video',
],
},
'core__media-text__media-right-custom-width': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Video',
],
},
'core__media-text__vertical-align-bottom': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Image',
],
},
'core__media-text__video': {
originalBlock: 'Media & Text',
availableTransforms: [
+ 'Group',
'Video',
],
},
core__categories: {
originalBlock: 'Categories',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__code: {
originalBlock: 'Code',
availableTransforms: [
+ 'Group',
'Preformatted',
],
},
core__columns: {
originalBlock: 'Columns',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__cover: {
availableTransforms: [
+ 'Group',
'Image',
],
originalBlock: 'Cover',
},
core__cover__video: {
availableTransforms: [
+ 'Group',
'Video',
],
originalBlock: 'Cover',
},
'core__cover__video-overlay': {
availableTransforms: [
+ 'Group',
'Video',
],
originalBlock: 'Cover',
},
core__embed: {
originalBlock: 'Embed',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__file__new-window': {
originalBlock: 'File',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__file__no-download-button': {
originalBlock: 'File',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__file__no-text-link': {
originalBlock: 'File',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__gallery: {
originalBlock: 'Gallery',
availableTransforms: [
+ 'Group',
'Image',
],
},
core__gallery__columns: {
originalBlock: 'Gallery',
availableTransforms: [
+ 'Group',
'Image',
],
},
@@ -133,10 +168,11 @@ export const EXPECTED_TRANSFORMS = {
originalBlock: 'Group',
availableTransforms: [],
},
- 'core__heading__h2-em': {
+ 'core__heading__h4-em': {
originalBlock: 'Heading',
availableTransforms: [
'Quote',
+ 'Group',
'Paragraph',
],
},
@@ -144,12 +180,15 @@ export const EXPECTED_TRANSFORMS = {
originalBlock: 'Heading',
availableTransforms: [
'Quote',
+ 'Group',
'Paragraph',
],
},
core__html: {
originalBlock: 'Custom HTML',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__image: {
originalBlock: 'Image',
@@ -157,6 +196,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -166,6 +206,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -175,6 +216,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -184,6 +226,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -193,6 +236,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -202,6 +246,7 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
@@ -211,43 +256,59 @@ export const EXPECTED_TRANSFORMS = {
'Gallery',
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
'core__latest-comments': {
originalBlock: 'Latest Comments',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__latest-posts': {
originalBlock: 'Latest Posts',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__latest-posts__displayPostDate': {
originalBlock: 'Latest Posts',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__legacy-widget': {
originalBlock: 'Legacy Widget (Experimental)',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__list__ul: {
originalBlock: 'List',
availableTransforms: [
+ 'Group',
'Paragraph',
'Quote',
],
},
core__more: {
originalBlock: 'More',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__more__custom-text-teaser': {
originalBlock: 'More',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__nextpage: {
originalBlock: 'Page Break',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__paragraph__align-right': {
originalBlock: 'Paragraph',
@@ -255,6 +316,7 @@ export const EXPECTED_TRANSFORMS = {
'Heading',
'List',
'Quote',
+ 'Group',
'Preformatted',
'Verse',
],
@@ -262,6 +324,7 @@ export const EXPECTED_TRANSFORMS = {
core__preformatted: {
originalBlock: 'Preformatted',
availableTransforms: [
+ 'Group',
'Paragraph',
],
},
@@ -269,18 +332,21 @@ export const EXPECTED_TRANSFORMS = {
originalBlock: 'Pullquote',
availableTransforms: [
'Quote',
+ 'Group',
],
},
'core__pullquote__multi-paragraph': {
originalBlock: 'Pullquote',
availableTransforms: [
'Quote',
+ 'Group',
],
},
'core__quote__style-1': {
originalBlock: 'Quote',
availableTransforms: [
'List',
+ 'Group',
'Paragraph',
'Heading',
'Pullquote',
@@ -290,6 +356,7 @@ export const EXPECTED_TRANSFORMS = {
originalBlock: 'Quote',
availableTransforms: [
'List',
+ 'Group',
'Paragraph',
'Heading',
'Pullquote',
@@ -297,44 +364,62 @@ export const EXPECTED_TRANSFORMS = {
},
core__rss: {
originalBlock: 'RSS',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__search: {
originalBlock: 'Search',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__search__custom-text': {
originalBlock: 'Search',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__separator: {
originalBlock: 'Separator',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__shortcode: {
originalBlock: 'Shortcode',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__spacer: {
originalBlock: 'Spacer',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
core__table: {
originalBlock: 'Table',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__tag-cloud': {
originalBlock: 'Tag Cloud',
- availableTransforms: [],
+ availableTransforms: [
+ 'Group',
+ ],
},
'core__tag-cloud__showTagCounts': {
originalBlock: 'Tag Cloud',
availableTransforms: [
+ 'Group',
],
},
core__verse: {
originalBlock: 'Verse',
availableTransforms: [
+ 'Group',
'Paragraph',
],
},
@@ -343,6 +428,7 @@ export const EXPECTED_TRANSFORMS = {
availableTransforms: [
'Cover',
'File',
+ 'Group',
'Media & Text',
],
},
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.html b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.html
new file mode 100644
index 00000000000000..b0fc3807468356
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.html
@@ -0,0 +1,3 @@
+
+A picture is worth a thousand words, or so the saying goes
+
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.json b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.json
new file mode 100644
index 00000000000000..0553075b8e24d6
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.json
@@ -0,0 +1,14 @@
+[
+ {
+ "clientId": "_clientId_0",
+ "name": "core/heading",
+ "isValid": true,
+ "attributes": {
+ "align": "right",
+ "content": "A picture is worth a thousand words, or so the saying goes",
+ "level": 3
+ },
+ "innerBlocks": [],
+ "originalContent": "A picture is worth a thousand words, or so the saying goes "
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.parsed.json b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.parsed.json
new file mode 100644
index 00000000000000..abb13458d09aa7
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.parsed.json
@@ -0,0 +1,23 @@
+[
+ {
+ "blockName": "core/heading",
+ "attrs": {
+ "align": "right",
+ "level": 3
+ },
+ "innerBlocks": [],
+ "innerHTML": "\nA picture is worth a thousand words, or so the saying goes \n",
+ "innerContent": [
+ "\nA picture is worth a thousand words, or so the saying goes \n"
+ ]
+ },
+ {
+ "blockName": null,
+ "attrs": {},
+ "innerBlocks": [],
+ "innerHTML": "\n",
+ "innerContent": [
+ "\n"
+ ]
+ }
+]
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.serialized.html b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.serialized.html
new file mode 100644
index 00000000000000..140a1929ad38c8
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__deprecated-1.serialized.html
@@ -0,0 +1,3 @@
+
+A picture is worth a thousand words, or so the saying goes
+
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.html b/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.html
deleted file mode 100644
index 7755d1dcf4eae2..00000000000000
--- a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-The Inserter Tool
-
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.serialized.html b/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.serialized.html
deleted file mode 100644
index 55ce73502b6185..00000000000000
--- a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.serialized.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
-The Inserter Tool
-
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.html b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.html
new file mode 100644
index 00000000000000..bd13c30bb968d1
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.html
@@ -0,0 +1,3 @@
+
+The Inserter Tool
+
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.json b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.json
similarity index 72%
rename from packages/e2e-tests/fixtures/blocks/core__heading__h2-em.json
rename to packages/e2e-tests/fixtures/blocks/core__heading__h4-em.json
index b72785dd6d8c2a..32985e4bb53c85 100644
--- a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.json
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.json
@@ -5,9 +5,9 @@
"isValid": true,
"attributes": {
"content": "The Inserter Tool",
- "level": 2
+ "level": 4
},
"innerBlocks": [],
- "originalContent": "The Inserter Tool "
+ "originalContent": "The Inserter Tool "
}
]
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.parsed.json b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.parsed.json
similarity index 62%
rename from packages/e2e-tests/fixtures/blocks/core__heading__h2-em.parsed.json
rename to packages/e2e-tests/fixtures/blocks/core__heading__h4-em.parsed.json
index e10209f2270c98..302248c57f6311 100644
--- a/packages/e2e-tests/fixtures/blocks/core__heading__h2-em.parsed.json
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.parsed.json
@@ -1,11 +1,13 @@
[
{
"blockName": "core/heading",
- "attrs": {},
+ "attrs": {
+ "level": 4
+ },
"innerBlocks": [],
- "innerHTML": "\nThe Inserter Tool \n",
+ "innerHTML": "\nThe Inserter Tool \n",
"innerContent": [
- "\nThe Inserter Tool \n"
+ "\nThe Inserter Tool \n"
]
},
{
diff --git a/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.serialized.html b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.serialized.html
new file mode 100644
index 00000000000000..051e1c9f39b273
--- /dev/null
+++ b/packages/e2e-tests/fixtures/blocks/core__heading__h4-em.serialized.html
@@ -0,0 +1,3 @@
+
+The Inserter Tool
+
diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json
index 33943fda4cc2fe..d7b75699be7f83 100644
--- a/packages/e2e-tests/package.json
+++ b/packages/e2e-tests/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/e2e-tests",
- "version": "1.2.1",
+ "version": "1.3.0",
"description": "End-To-End (E2E) tests for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/e2e-tests/plugins/align-hook.php b/packages/e2e-tests/plugins/align-hook.php
index 89435541e3d2ce..dbf5a3cb83a681 100644
--- a/packages/e2e-tests/plugins/align-hook.php
+++ b/packages/e2e-tests/plugins/align-hook.php
@@ -17,7 +17,7 @@ function enqueue_align_plugin_script() {
array(
'wp-blocks',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
'wp-i18n',
),
filemtime( plugin_dir_path( __FILE__ ) . 'align-hook/index.js' ),
diff --git a/packages/e2e-tests/plugins/align-hook/index.js b/packages/e2e-tests/plugins/align-hook/index.js
index 927c491e843717..0380758fbaaf05 100644
--- a/packages/e2e-tests/plugins/align-hook/index.js
+++ b/packages/e2e-tests/plugins/align-hook/index.js
@@ -1,7 +1,7 @@
( function() {
var registerBlockType = wp.blocks.registerBlockType;
var el = wp.element.createElement;
- var InnerBlocks = wp.editor.InnerBlocks;
+ var InnerBlocks = wp.blockEditor.InnerBlocks;
var __ = wp.i18n.__;
var TEMPLATE = [
[ 'core/paragraph', { fontSize: 'large', content: __( 'Content…' ) } ],
diff --git a/packages/e2e-tests/plugins/block-icons.php b/packages/e2e-tests/plugins/block-icons.php
index 6f79bf7d7e1d6a..ce1013f041c889 100644
--- a/packages/e2e-tests/plugins/block-icons.php
+++ b/packages/e2e-tests/plugins/block-icons.php
@@ -18,7 +18,7 @@ function enqueue_block_icons_plugin_script() {
'wp-blocks',
'wp-components',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
'wp-hooks',
'wp-i18n',
),
diff --git a/packages/e2e-tests/plugins/block-icons/index.js b/packages/e2e-tests/plugins/block-icons/index.js
index 705b03ebfbc91b..fe05c1d23dcb33 100644
--- a/packages/e2e-tests/plugins/block-icons/index.js
+++ b/packages/e2e-tests/plugins/block-icons/index.js
@@ -1,7 +1,7 @@
( function() {
var registerBlockType = wp.blocks.registerBlockType;
var el = wp.element.createElement;
- var InnerBlocks = wp.editor.InnerBlocks;
+ var InnerBlocks = wp.blockEditor.InnerBlocks;
var circle = el( 'circle', { cx: 10, cy: 10, r: 10, fill: 'red', stroke: 'blue', strokeWidth: '10' } );
var svg = el( 'svg', { width: 20, height: 20, viewBox: '0 0 20 20' }, circle );
diff --git a/packages/e2e-tests/plugins/container-without-paragraph.php b/packages/e2e-tests/plugins/container-without-paragraph.php
index 315e3eb4258e41..59c06641ce0419 100644
--- a/packages/e2e-tests/plugins/container-without-paragraph.php
+++ b/packages/e2e-tests/plugins/container-without-paragraph.php
@@ -17,7 +17,7 @@ function enqueue_container_without_paragraph_plugin_script() {
array(
'wp-blocks',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
),
filemtime( plugin_dir_path( __FILE__ ) . 'container-without-paragraph/index.js' ),
true
diff --git a/packages/e2e-tests/plugins/container-without-paragraph/index.js b/packages/e2e-tests/plugins/container-without-paragraph/index.js
index 03eec8105a10aa..7ec20eef893ace 100644
--- a/packages/e2e-tests/plugins/container-without-paragraph/index.js
+++ b/packages/e2e-tests/plugins/container-without-paragraph/index.js
@@ -5,13 +5,13 @@
icon: 'yes',
edit() {
- return wp.element.createElement(wp.editor.InnerBlocks, {
+ return wp.element.createElement(wp.blockEditor.InnerBlocks, {
allowedBlocks: ['core/image', 'core/gallery']
});
},
save() {
- return wp.element.createElement(wp.editor.InnerBlocks.Content);
+ return wp.element.createElement(wp.blockEditor.InnerBlocks.Content);
},
})
} )();
diff --git a/packages/e2e-tests/plugins/deprecated-node-matcher.php b/packages/e2e-tests/plugins/deprecated-node-matcher.php
index 964cc7eec9c362..7e0a6e3d2c5578 100644
--- a/packages/e2e-tests/plugins/deprecated-node-matcher.php
+++ b/packages/e2e-tests/plugins/deprecated-node-matcher.php
@@ -18,7 +18,7 @@ function enqueue_deprecated_node_matcher_plugin_script() {
'lodash',
'wp-blocks',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
),
filemtime( plugin_dir_path( __FILE__ ) . 'deprecated-node-matcher/index.js' ),
true
diff --git a/packages/e2e-tests/plugins/deprecated-node-matcher/index.js b/packages/e2e-tests/plugins/deprecated-node-matcher/index.js
index 5f41ed3325afd9..c49c1df95b8360 100644
--- a/packages/e2e-tests/plugins/deprecated-node-matcher/index.js
+++ b/packages/e2e-tests/plugins/deprecated-node-matcher/index.js
@@ -1,6 +1,6 @@
( function() {
var registerBlockType = wp.blocks.registerBlockType;
- var RichText = wp.editor.RichText;
+ var RichText = wp.blockEditor.RichText;
var el = wp.element.createElement;
var el = wp.element.createElement;
diff --git a/packages/e2e-tests/plugins/format-api.php b/packages/e2e-tests/plugins/format-api.php
index dc33462affbc89..cd03a89e76aa48 100644
--- a/packages/e2e-tests/plugins/format-api.php
+++ b/packages/e2e-tests/plugins/format-api.php
@@ -14,7 +14,7 @@ function gutenberg_test_format_api_scripts() {
wp_enqueue_script(
'gutenberg-test-format-api',
plugins_url( 'format-api/index.js', __FILE__ ),
- array( 'wp-editor', 'wp-element', 'wp-rich-text' ),
+ array( 'wp-block-editor', 'wp-element', 'wp-rich-text' ),
filemtime( plugin_dir_path( __FILE__ ) . 'format-api/index.js' ),
true
);
diff --git a/packages/e2e-tests/plugins/format-api/index.js b/packages/e2e-tests/plugins/format-api/index.js
index 671767e2049269..d22f696737ecdc 100644
--- a/packages/e2e-tests/plugins/format-api/index.js
+++ b/packages/e2e-tests/plugins/format-api/index.js
@@ -9,7 +9,7 @@
className: 'my-plugin-link',
edit: function( props ) {
return wp.element.createElement(
- wp.editor.RichTextToolbarButton, {
+ wp.blockEditor.RichTextToolbarButton, {
icon: 'admin-links',
title: 'Custom Link',
onClick: function() {
diff --git a/packages/e2e-tests/plugins/hooks-api.php b/packages/e2e-tests/plugins/hooks-api.php
index f3533d8d86748c..c4b6d70b95f182 100644
--- a/packages/e2e-tests/plugins/hooks-api.php
+++ b/packages/e2e-tests/plugins/hooks-api.php
@@ -18,7 +18,7 @@ function enqueue_hooks_plugin_script() {
'wp-blocks',
'wp-components',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
'wp-hooks',
'wp-i18n',
),
diff --git a/packages/e2e-tests/plugins/hooks-api/index.js b/packages/e2e-tests/plugins/hooks-api/index.js
index eb45316565f386..3cfe4ca1590322 100644
--- a/packages/e2e-tests/plugins/hooks-api/index.js
+++ b/packages/e2e-tests/plugins/hooks-api/index.js
@@ -3,7 +3,7 @@
var Fragment = wp.element.Fragment;
var Button = wp.components.Button;
var PanelBody = wp.components.PanelBody;
- var InspectorControls = wp.editor.InspectorControls;
+ var InspectorControls = wp.blockEditor.InspectorControls;
var addFilter = wp.hooks.addFilter;
var createBlock = wp.blocks.createBlock;
var __ = wp.i18n.__;
diff --git a/packages/e2e-tests/plugins/inner-blocks-allowed-blocks.php b/packages/e2e-tests/plugins/inner-blocks-allowed-blocks.php
index 8dcf752ba7df44..a8a0e6ee9a254c 100644
--- a/packages/e2e-tests/plugins/inner-blocks-allowed-blocks.php
+++ b/packages/e2e-tests/plugins/inner-blocks-allowed-blocks.php
@@ -16,7 +16,7 @@ function enqueue_inner_blocks_allowed_blocks_script() {
plugins_url( 'inner-blocks-allowed-blocks/index.js', __FILE__ ),
array(
'wp-blocks',
- 'wp-editor',
+ 'wp-block-editor',
'wp-element',
'wp-i18n',
),
diff --git a/packages/e2e-tests/plugins/inner-blocks-allowed-blocks/index.js b/packages/e2e-tests/plugins/inner-blocks-allowed-blocks/index.js
index 2b1ba6eadd2c1b..f6f61c27d5c82c 100644
--- a/packages/e2e-tests/plugins/inner-blocks-allowed-blocks/index.js
+++ b/packages/e2e-tests/plugins/inner-blocks-allowed-blocks/index.js
@@ -2,7 +2,7 @@
const { withSelect } = wp.data;
const { registerBlockType } = wp.blocks;
const { createElement: el } = wp.element;
- const { InnerBlocks } = wp.editor;
+ const { InnerBlocks } = wp.blockEditor;
const __ = wp.i18n.__;
const divProps = { className: 'product', style: { outline: '1px solid gray', padding: 5 } };
const template = [
@@ -64,7 +64,7 @@
category: 'common',
edit: withSelect( function( select, ownProps ) {
- var getBlockOrder = select( 'core/editor' ).getBlockOrder;
+ var getBlockOrder = select( 'core/block-editor' ).getBlockOrder;
return {
numberOfChildren: getBlockOrder( ownProps.clientId ).length,
};
diff --git a/packages/e2e-tests/plugins/inner-blocks-templates.php b/packages/e2e-tests/plugins/inner-blocks-templates.php
index 792a2dc0234d15..212992e3619623 100644
--- a/packages/e2e-tests/plugins/inner-blocks-templates.php
+++ b/packages/e2e-tests/plugins/inner-blocks-templates.php
@@ -18,7 +18,7 @@ function enqueue_container_without_paragraph_plugin_script() {
'wp-blocks',
'wp-components',
'wp-element',
- 'wp-editor',
+ 'wp-block-editor',
'wp-hooks',
'wp-i18n',
),
diff --git a/packages/e2e-tests/plugins/inner-blocks-templates/index.js b/packages/e2e-tests/plugins/inner-blocks-templates/index.js
index 6c0b62c51fea4e..7d3662f17dbfc4 100644
--- a/packages/e2e-tests/plugins/inner-blocks-templates/index.js
+++ b/packages/e2e-tests/plugins/inner-blocks-templates/index.js
@@ -2,7 +2,7 @@
var registerBlockType = wp.blocks.registerBlockType;
var createBlock = wp.blocks.createBlock;
var el = wp.element.createElement;
- var InnerBlocks = wp.editor.InnerBlocks;
+ var InnerBlocks = wp.blockEditor.InnerBlocks;
var __ = wp.i18n.__;
var TEMPLATE = [
[ 'core/paragraph', {
diff --git a/packages/e2e-tests/plugins/plugins-api.php b/packages/e2e-tests/plugins/plugins-api.php
index 75c1180e44afc6..a1c91596faea52 100644
--- a/packages/e2e-tests/plugins/plugins-api.php
+++ b/packages/e2e-tests/plugins/plugins-api.php
@@ -45,6 +45,7 @@ function enqueue_plugins_api_plugin_scripts() {
'wp-compose',
'wp-data',
'wp-edit-post',
+ 'wp-block-editor',
'wp-editor',
'wp-element',
'wp-i18n',
@@ -63,7 +64,7 @@ function enqueue_plugins_api_plugin_scripts() {
'wp-compose',
'wp-data',
'wp-edit-post',
- 'wp-editor',
+ 'wp-block-editor',
'wp-element',
'wp-i18n',
'wp-plugins',
diff --git a/packages/e2e-tests/plugins/plugins-api/annotations-sidebar.js b/packages/e2e-tests/plugins/plugins-api/annotations-sidebar.js
index 5dfa0e6a52888b..88ff01965f9762 100644
--- a/packages/e2e-tests/plugins/plugins-api/annotations-sidebar.js
+++ b/packages/e2e-tests/plugins/plugins-api/annotations-sidebar.js
@@ -7,7 +7,7 @@
var withSelect = wp.data.withSelect;
var select = wp.data.select;
var dispatch = wp.data.dispatch;
- var PlainText = wp.editor.PlainText;
+ var PlainText = wp.blockEditor.PlainText;
var Fragment = wp.element.Fragment;
var el = wp.element.createElement;
var Component = wp.element.Component;
@@ -63,7 +63,7 @@
onClick: () => {
dispatch( 'core/annotations' ).__experimentalAddAnnotation( {
source: 'e2e-tests',
- blockClientId: select( 'core/editor' ).getBlockOrder()[ 0 ],
+ blockClientId: select( 'core/block-editor' ).getBlockOrder()[ 0 ],
richTextIdentifier: 'content',
range: {
start: parseInt( this.state.start, 10 ),
diff --git a/packages/e2e-tests/plugins/plugins-api/sidebar.js b/packages/e2e-tests/plugins/plugins-api/sidebar.js
index 22038c81c4c928..5ce92bcd4e09a2 100644
--- a/packages/e2e-tests/plugins/plugins-api/sidebar.js
+++ b/packages/e2e-tests/plugins/plugins-api/sidebar.js
@@ -7,7 +7,7 @@
var withSelect = wp.data.withSelect;
var select = wp.data.select;
var dispatch = wp.data.dispatch;
- var PlainText = wp.editor.PlainText;
+ var PlainText = wp.blockEditor.PlainText;
var Fragment = wp.element.Fragment;
var el = wp.element.createElement;
var __ = wp.i18n.__;
diff --git a/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap b/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap
new file mode 100644
index 00000000000000..af571b2010ed25
--- /dev/null
+++ b/packages/e2e-tests/specs/__snapshots__/block-grouping.test.js.snap
@@ -0,0 +1,99 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Block Grouping Group creation creates a group from multiple blocks of different types via block transforms 1`] = `
+"
+
+
Group Heading
+
+
+
+
+
+
+
+
Some paragraph
+
+"
+`;
+
+exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via block transforms 1`] = `
+"
+
+
First Paragraph
+
+
+
+
Second Paragraph
+
+
+
+
Third Paragraph
+
+"
+`;
+
+exports[`Block Grouping Group creation creates a group from multiple blocks of the same type via options toolbar 1`] = `
+"
+
+
First Paragraph
+
+
+
+
Second Paragraph
+
+
+
+
Third Paragraph
+
+"
+`;
+
+exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 1`] = `
+"
+
+
Group Heading
+
+
+
+
+
+
+
+
Some paragraph
+
+"
+`;
+
+exports[`Block Grouping Group creation groups and ungroups multiple blocks of different types via options toolbar 2`] = `
+"
+Group Heading
+
+
+
+
+
+
+
+Some paragraph
+"
+`;
+
+exports[`Block Grouping Preserving selected blocks attributes preserves width alignment settings of selected blocks 1`] = `
+"
+
+
Group Heading
+
+
+
+
+
+
+
+
+
+
+
+
Some paragraph
+
+"
+`;
diff --git a/packages/e2e-tests/specs/__snapshots__/block-transforms.test.js.snap b/packages/e2e-tests/specs/__snapshots__/block-transforms.test.js.snap
index ce9dfc0be7e555..678dc348b496fd 100644
--- a/packages/e2e-tests/specs/__snapshots__/block-transforms.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/block-transforms.test.js.snap
@@ -64,13 +64,13 @@ exports[`Block transforms correctly transform block Heading in fixture core__hea
"
`;
-exports[`Block transforms correctly transform block Heading in fixture core__heading__h2-em into the Paragraph block 1`] = `
+exports[`Block transforms correctly transform block Heading in fixture core__heading__h4-em into the Paragraph block 1`] = `
"
The Inserter Tool
"
`;
-exports[`Block transforms correctly transform block Heading in fixture core__heading__h2-em into the Quote block 1`] = `
+exports[`Block transforms correctly transform block Heading in fixture core__heading__h4-em into the Quote block 1`] = `
"
The Inserter Tool
"
@@ -96,6 +96,14 @@ exports[`Block transforms correctly transform block Image in fixture core__image
"
`;
+exports[`Block transforms correctly transform block Image in fixture core__image into the Group block 1`] = `
+"
+
+
+
+"
+`;
+
exports[`Block transforms correctly transform block Image in fixture core__image into the Media & Text block 1`] = `
"
@@ -352,6 +360,14 @@ exports[`Block transforms correctly transform block Media & Text in fixture core
"
`;
+exports[`Block transforms correctly transform block Paragraph in fixture core__paragraph__align-right into the Group block 1`] = `
+"
+
+
... like this one, which is separate from the above and right aligned.
+
+"
+`;
+
exports[`Block transforms correctly transform block Paragraph in fixture core__paragraph__align-right into the Heading block 1`] = `
"
... like this one, which is separate from the above and right aligned.
diff --git a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
index 8aab0dcd5b734b..3c7044b5fb8053 100644
--- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
+++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap
@@ -6,12 +6,6 @@ exports[`RichText should apply formatting when selection is collapsed 1`] = `
"
`;
-exports[`RichText should apply formatting with access shortcut 1`] = `
-"
-
test
-"
-`;
-
exports[`RichText should apply formatting with primary shortcut 1`] = `
"
test
diff --git a/packages/e2e-tests/specs/adding-blocks.test.js b/packages/e2e-tests/specs/adding-blocks.test.js
index aeafc65cd8cc39..332dab5083f3ba 100644
--- a/packages/e2e-tests/specs/adding-blocks.test.js
+++ b/packages/e2e-tests/specs/adding-blocks.test.js
@@ -97,7 +97,7 @@ describe( 'adding blocks', () => {
await page.keyboard.type( 'Second paragraph' );
// Switch to Text Mode to check HTML Output
- await page.click( '.edit-post-more-menu [aria-label="Show more tools & options"]' );
+ await page.click( '.edit-post-more-menu [aria-label="More tools & options"]' );
const codeEditorButton = ( await page.$x( "//button[contains(text(), 'Code Editor')]" ) )[ 0 ];
await codeEditorButton.click( 'button' );
diff --git a/packages/e2e-tests/specs/block-deletion.test.js b/packages/e2e-tests/specs/block-deletion.test.js
index 6adcb01681bb2a..02cc0a0f5720ee 100644
--- a/packages/e2e-tests/specs/block-deletion.test.js
+++ b/packages/e2e-tests/specs/block-deletion.test.js
@@ -3,10 +3,12 @@
*/
import {
clickBlockAppender,
+ clickBlockToolbarButton,
getEditedPostContent,
createNewPost,
isInDefaultBlock,
pressKeyWithModifier,
+ pressKeyTimes,
insertBlock,
} from '@wordpress/e2e-test-utils';
@@ -21,10 +23,37 @@ const addThreeParagraphsToNewPost = async () => {
await page.keyboard.press( 'Enter' );
};
-const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => {
- await expect( page ).toClick( '.block-editor-block-settings-menu__toggle' );
- const itemButton = ( await page.$x( `//*[contains(@class, "block-editor-block-settings-menu__popover")]//button[contains(text(), '${ buttonLabel }')]` ) )[ 0 ];
- await itemButton.click();
+/**
+ * Due to an issue with the Popover component not being scrollable
+ * under certain conditions, Pupeteer cannot "see" the "Remove Block"
+ * button. This is a workaround until that issue is resolved.
+ *
+ * see: https://github.com/WordPress/gutenberg/pull/14908#discussion_r284725956
+ */
+const clickOnBlockSettingsMenuRemoveBlockButton = async () => {
+ await clickBlockToolbarButton( 'More options' );
+
+ let isRemoveButton = false;
+
+ let numButtons = await page.$$eval( '.block-editor-block-toolbar button', ( btns ) => btns.length );
+
+ // Limit by the number of buttons available
+ while ( --numButtons ) {
+ await page.keyboard.press( 'Tab' );
+
+ isRemoveButton = await page.evaluate( () => {
+ return document.activeElement.innerText.includes( 'Remove Block' );
+ } );
+
+ // Stop looping once we find the button
+ if ( isRemoveButton ) {
+ await pressKeyTimes( 'Enter', 1 );
+ break;
+ }
+ }
+
+ // Makes failures more explicit
+ await expect( isRemoveButton ).toBe( true );
};
describe( 'block deletion -', () => {
@@ -38,7 +67,8 @@ describe( 'block deletion -', () => {
// Press Escape to show the block toolbar
await page.keyboard.press( 'Escape' );
- await clickOnBlockSettingsMenuItem( 'Remove Block' );
+ await clickOnBlockSettingsMenuRemoveBlockButton();
+
expect( await getEditedPostContent() ).toMatchSnapshot();
// Type additional text and assert that caret position is correct by comparing to snapshot.
@@ -120,7 +150,7 @@ describe( 'deleting all blocks', () => {
await page.keyboard.press( 'Escape' );
- await clickOnBlockSettingsMenuItem( 'Remove Block' );
+ await clickOnBlockSettingsMenuRemoveBlockButton();
// There is a default block:
expect( await page.$$( '.block-editor-block-list__block' ) ).toHaveLength( 1 );
diff --git a/packages/e2e-tests/specs/block-grouping.test.js b/packages/e2e-tests/specs/block-grouping.test.js
new file mode 100644
index 00000000000000..57300ec2b9de66
--- /dev/null
+++ b/packages/e2e-tests/specs/block-grouping.test.js
@@ -0,0 +1,176 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ insertBlock,
+ createNewPost,
+ clickBlockToolbarButton,
+ pressKeyWithModifier,
+ getEditedPostContent,
+ transformBlockTo,
+ getAllBlocks,
+ getAvailableBlockTransforms,
+} from '@wordpress/e2e-test-utils';
+
+async function insertBlocksOfSameType() {
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'First Paragraph' );
+
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'Second Paragraph' );
+
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'Third Paragraph' );
+}
+
+async function insertBlocksOfMultipleTypes() {
+ await insertBlock( 'Heading' );
+ await page.keyboard.type( 'Group Heading' );
+
+ await insertBlock( 'Image' );
+
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'Some paragraph' );
+}
+
+describe( 'Block Grouping', () => {
+ beforeEach( async () => {
+ // Posts are auto-removed at the end of each test run
+ await createNewPost();
+ } );
+
+ describe( 'Group creation', () => {
+ it( 'creates a group from multiple blocks of the same type via block transforms', async () => {
+ // Creating test blocks
+ await insertBlocksOfSameType();
+
+ // Multiselect via keyboard.
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ await transformBlockTo( 'Group' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'creates a group from multiple blocks of different types via block transforms', async () => {
+ // Creating test blocks
+ await insertBlocksOfMultipleTypes();
+
+ // Multiselect via keyboard.
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ await transformBlockTo( 'Group' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'creates a group from multiple blocks of the same type via options toolbar', async () => {
+ // Creating test blocks
+ await insertBlocksOfSameType();
+
+ // Multiselect via keyboard.
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ await clickBlockToolbarButton( 'More options' );
+
+ const groupButton = await page.waitForXPath( '//button[text()="Group"]' );
+ await groupButton.click();
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'groups and ungroups multiple blocks of different types via options toolbar', async () => {
+ // Creating test blocks
+ await insertBlocksOfMultipleTypes();
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ // Group
+ await clickBlockToolbarButton( 'More options' );
+ const groupButton = await page.waitForXPath( '//button[text()="Group"]' );
+ await groupButton.click();
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ // UnGroup
+ await clickBlockToolbarButton( 'More options' );
+ const unGroupButton = await page.waitForXPath( '//button[text()="Ungroup"]' );
+ await unGroupButton.click();
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+ } );
+
+ describe( 'Container Block availability', () => {
+ beforeEach( async () => {
+ // Disable the Group block
+ await page.evaluate( () => {
+ const { dispatch } = wp.data;
+ dispatch( 'core/edit-post' ).hideBlockTypes( [ 'core/group' ] );
+ } );
+
+ // Create a Group
+ await insertBlocksOfMultipleTypes();
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+ } );
+
+ afterAll( async () => {
+ // Re-enable the Group block
+ await page.evaluate( () => {
+ const { dispatch } = wp.data;
+ dispatch( 'core/edit-post' ).showBlockTypes( [ 'core/group' ] );
+ } );
+ } );
+
+ it( 'does not show group transform if container block is disabled', async () => {
+ const availableTransforms = await getAvailableBlockTransforms();
+
+ expect(
+ availableTransforms
+ ).not.toContain( 'Group' );
+ } );
+
+ it( 'does not show group option in the options toolbar if container block is disabled ', async () => {
+ await clickBlockToolbarButton( 'More options' );
+
+ const blockOptionsDropdownHTML = await page.evaluate( () => document.querySelector( '.block-editor-block-settings-menu__content' ).innerHTML );
+
+ expect( blockOptionsDropdownHTML ).not.toContain( 'Group' );
+ } );
+ } );
+
+ describe( 'Preserving selected blocks attributes', () => {
+ it( 'preserves width alignment settings of selected blocks', async () => {
+ await insertBlock( 'Heading' );
+ await page.keyboard.type( 'Group Heading' );
+
+ // Full width image
+ await insertBlock( 'Image' );
+ await clickBlockToolbarButton( 'Full width' );
+
+ // Wide width image)
+ await insertBlock( 'Image' );
+ await clickBlockToolbarButton( 'Wide width' );
+
+ await insertBlock( 'Paragraph' );
+ await page.keyboard.type( 'Some paragraph' );
+
+ await pressKeyWithModifier( 'primary', 'a' );
+ await pressKeyWithModifier( 'primary', 'a' );
+
+ await transformBlockTo( 'Group' );
+
+ const allBlocks = await getAllBlocks();
+
+ // We expect Group block align setting to match that
+ // of the widest of it's "child" innerBlocks
+ expect( allBlocks[ 0 ].attributes.align ).toBe( 'full' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+ } );
+} );
diff --git a/packages/e2e-tests/specs/block-switcher.test.js b/packages/e2e-tests/specs/block-switcher.test.js
index 0d2fcf2adfe9e3..9282e83dc855f1 100644
--- a/packages/e2e-tests/specs/block-switcher.test.js
+++ b/packages/e2e-tests/specs/block-switcher.test.js
@@ -27,6 +27,7 @@ describe( 'adding blocks', () => {
expect(
await getAvailableBlockTransforms()
).toEqual( [
+ 'Group',
'Paragraph',
'Quote',
] );
@@ -50,6 +51,7 @@ describe( 'adding blocks', () => {
expect(
await getAvailableBlockTransforms()
).toEqual( [
+ 'Group',
'Paragraph',
] );
} );
@@ -60,6 +62,7 @@ describe( 'adding blocks', () => {
( [
'core/quote',
'core/paragraph',
+ 'core/group',
] ).map( ( block ) => wp.blocks.unregisterBlockType( block ) );
} );
diff --git a/packages/e2e-tests/specs/block-transforms.test.js b/packages/e2e-tests/specs/block-transforms.test.js
index 8a4a2a38461bb4..89d14bf524491f 100644
--- a/packages/e2e-tests/specs/block-transforms.test.js
+++ b/packages/e2e-tests/specs/block-transforms.test.js
@@ -94,13 +94,7 @@ const getTransformResult = async ( blockContent, transformName ) => {
return getEditedPostContent();
};
-// Skipping all the tests when plugins are enabled
-// makes sure the tests are not executed, and no unused snapshots errors are thrown.
-const maybeDescribe = process.env.POPULAR_PLUGINS ?
- describe.skip :
- describe;
-
-maybeDescribe( 'Block transforms', () => {
+describe( 'Block transforms', () => {
const fileBasenames = getAvailableBlockFixturesBasenames();
const transformStructure = {};
@@ -159,7 +153,12 @@ maybeDescribe( 'Block transforms', () => {
)
);
- it.each( testTable )(
+ // As Group is available as a transform on *all* blocks this would create a lot of
+ // tests which would impact on the performance of the e2e test suite.
+ // To avoid this, we remove `core/group` from test table for all but 2 block types.
+ const testTableWithSomeGroupsFiltered = testTable.filter( ( transform ) => ( transform[ 2 ] !== 'Group' || transform[ 1 ] === 'core__paragraph__align-right' || transform[ 1 ] === 'core__image' ) );
+
+ it.each( testTableWithSomeGroupsFiltered )(
'block %s in fixture %s into the %s block',
async ( originalBlock, fixture, destinationBlock ) => {
const { content } = transformStructure[ fixture ];
diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/button.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/button.test.js.snap
new file mode 100644
index 00000000000000..82fca76a8fb04d
--- /dev/null
+++ b/packages/e2e-tests/specs/blocks/__snapshots__/button.test.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Button can jump focus back & forth 1`] = `
+"
+
+"
+`;
+
+exports[`Button has focus on button content 1`] = `
+"
+
+"
+`;
diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/heading.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/heading.test.js.snap
index 71bb261abc29a8..093ceb0da4e9d5 100644
--- a/packages/e2e-tests/specs/blocks/__snapshots__/heading.test.js.snap
+++ b/packages/e2e-tests/specs/blocks/__snapshots__/heading.test.js.snap
@@ -12,6 +12,18 @@ exports[`Heading can be created by prefixing number sign and a space 1`] = `
"
`;
+exports[`Heading it should correctly apply custom colors 1`] = `
+"
+
Heading
+"
+`;
+
+exports[`Heading it should correctly apply named colors 1`] = `
+"
+
Heading
+"
+`;
+
exports[`Heading should create a paragraph block above when pressing enter at the start 1`] = `
"
diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/spacer.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/spacer.test.js.snap
new file mode 100644
index 00000000000000..cfd7c9149d512e
--- /dev/null
+++ b/packages/e2e-tests/specs/blocks/__snapshots__/spacer.test.js.snap
@@ -0,0 +1,13 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Spacer can be created by typing "/spacer" 1`] = `
+"
+
+"
+`;
+
+exports[`Spacer can be resized using the drag handle and remains selected after being dragged 1`] = `
+"
+
+"
+`;
diff --git a/packages/e2e-tests/specs/blocks/button.test.js b/packages/e2e-tests/specs/blocks/button.test.js
new file mode 100644
index 00000000000000..69343633f4247f
--- /dev/null
+++ b/packages/e2e-tests/specs/blocks/button.test.js
@@ -0,0 +1,30 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ insertBlock,
+ getEditedPostContent,
+ createNewPost,
+} from '@wordpress/e2e-test-utils';
+
+describe( 'Button', () => {
+ beforeEach( async () => {
+ await createNewPost();
+ } );
+
+ it( 'has focus on button content', async () => {
+ await insertBlock( 'Button' );
+ await page.keyboard.type( 'Content' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'can jump focus back & forth', async () => {
+ await insertBlock( 'Button' );
+ await page.keyboard.type( 'WordPress' );
+ await page.keyboard.press( 'Tab' );
+ await page.keyboard.type( 'https://wordpress.org' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+} );
diff --git a/packages/e2e-tests/specs/blocks/heading.test.js b/packages/e2e-tests/specs/blocks/heading.test.js
index 2690ad4690b5b6..cb46f7072b0688 100644
--- a/packages/e2e-tests/specs/blocks/heading.test.js
+++ b/packages/e2e-tests/specs/blocks/heading.test.js
@@ -3,11 +3,19 @@
*/
import {
clickBlockAppender,
- getEditedPostContent,
createNewPost,
+ getEditedPostContent,
+ pressKeyWithModifier,
} from '@wordpress/e2e-test-utils';
describe( 'Heading', () => {
+ const TEXT_COLOR_TEXT = 'Text Color';
+ const CUSTOM_COLOR_TEXT = 'Custom Color';
+ const TEXT_COLOR_UI_X_SELECTOR = `//div[./span[contains(text(),'${ TEXT_COLOR_TEXT }')]]`;
+ const CUSTOM_COLOR_BUTTON_X_SELECTOR = `//button[contains(text(),'${ CUSTOM_COLOR_TEXT }')]`;
+ const COLOR_INPUT_FIELD_SELECTOR = '.components-color-palette__picker .components-text-control__input';
+ const COLOR_PANEL_TOGGLE_X_SELECTOR = '//button[./span[contains(text(),\'Color Settings\')]]';
+
beforeEach( async () => {
await createNewPost();
} );
@@ -44,4 +52,36 @@ describe( 'Heading', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
+
+ it( 'it should correctly apply custom colors', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '### Heading' );
+ const [ colorPanelToggle ] = await page.$x( COLOR_PANEL_TOGGLE_X_SELECTOR );
+ await colorPanelToggle.click();
+
+ const [ customTextColorButton ] = await page.$x(
+ `${ TEXT_COLOR_UI_X_SELECTOR }${ CUSTOM_COLOR_BUTTON_X_SELECTOR }`
+ );
+ await customTextColorButton.click();
+ await page.click( COLOR_INPUT_FIELD_SELECTOR );
+ await pressKeyWithModifier( 'primary', 'A' );
+ await page.keyboard.type( '#181717' );
+ await page.click( '.wp-block-heading' );
+ await page.waitForSelector( '.component-color-indicator[aria-label="(text color: #181717)"]' );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'it should correctly apply named colors', async () => {
+ await clickBlockAppender();
+ await page.keyboard.type( '## Heading' );
+ const [ colorPanelToggle ] = await page.$x( COLOR_PANEL_TOGGLE_X_SELECTOR );
+ await colorPanelToggle.click();
+
+ const whiteColorButtonSelector = `${ TEXT_COLOR_UI_X_SELECTOR }//button[@aria-label='Color: White']`;
+ const [ whiteColorButton ] = await page.$x( whiteColorButtonSelector );
+ await whiteColorButton.click();
+ await page.click( '.wp-block-heading' );
+ await page.waitForXPath( `${ whiteColorButtonSelector }[@aria-pressed='true']` );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
} );
diff --git a/packages/e2e-tests/specs/blocks/preformatted.test.js b/packages/e2e-tests/specs/blocks/preformatted.test.js
index a5f9965af3c519..ea6575785613cf 100644
--- a/packages/e2e-tests/specs/blocks/preformatted.test.js
+++ b/packages/e2e-tests/specs/blocks/preformatted.test.js
@@ -2,6 +2,7 @@
* WordPress dependencies
*/
import {
+ clickBlockToolbarButton,
getEditedPostContent,
createNewPost,
insertBlock,
@@ -21,16 +22,12 @@ describe( 'Preformatted', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
- await page.keyboard.press( 'Escape' );
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
await clickButton( 'Convert to Blocks' );
// Once it's edited, it should be saved as BR tags.
await page.keyboard.type( '0' );
await page.keyboard.press( 'Enter' );
- await page.keyboard.press( 'Escape' );
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
await clickButton( 'Edit as HTML' );
expect( await getEditedPostContent() ).toMatchSnapshot();
diff --git a/packages/e2e-tests/specs/blocks/spacer.test.js b/packages/e2e-tests/specs/blocks/spacer.test.js
new file mode 100644
index 00000000000000..bd02e7a8505599
--- /dev/null
+++ b/packages/e2e-tests/specs/blocks/spacer.test.js
@@ -0,0 +1,38 @@
+/**
+ * WordPress dependencies
+ */
+import {
+ clickBlockAppender,
+ getEditedPostContent,
+ createNewPost,
+ dragAndResize,
+} from '@wordpress/e2e-test-utils';
+
+describe( 'Spacer', () => {
+ beforeEach( async () => {
+ await createNewPost();
+ } );
+
+ it( 'can be created by typing "/spacer"', async () => {
+ // Create a spacer with the slash block shortcut.
+ await clickBlockAppender();
+ await page.keyboard.type( '/spacer' );
+ await page.keyboard.press( 'Enter' );
+
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+ } );
+
+ it( 'can be resized using the drag handle and remains selected after being dragged', async () => {
+ // Create a spacer with the slash block shortcut.
+ await clickBlockAppender();
+ await page.keyboard.type( '/spacer' );
+ await page.keyboard.press( 'Enter' );
+
+ const resizableHandle = await page.$( '.block-library-spacer__resize-container .components-resizable-box__handle' );
+ await dragAndResize( resizableHandle, { x: 0, y: 50 } );
+ expect( await getEditedPostContent() ).toMatchSnapshot();
+
+ const selectedSpacer = await page.$( '[data-type="core/spacer"].is-selected' );
+ expect( selectedSpacer ).not.toBe( null );
+ } );
+} );
diff --git a/packages/e2e-tests/specs/blocks/table.test.js b/packages/e2e-tests/specs/blocks/table.test.js
index 3a61f5e5eda594..0e3dd0bf5cad42 100644
--- a/packages/e2e-tests/specs/blocks/table.test.js
+++ b/packages/e2e-tests/specs/blocks/table.test.js
@@ -3,6 +3,8 @@
*/
import { createNewPost, insertBlock, getEditedPostContent } from '@wordpress/e2e-test-utils';
+const createButtonSelector = "//div[@data-type='core/table']//button[text()='Create Table']";
+
describe( 'Table', () => {
beforeEach( async () => {
await createNewPost();
@@ -34,7 +36,7 @@ describe( 'Table', () => {
await page.keyboard.type( '10' );
// // Create the table.
- const createButton = await page.$x( "//div[@data-type='core/table']//button[text()='Create']" );
+ const createButton = await page.$x( createButtonSelector );
await createButton[ 0 ].click();
// // Expect the post content to have a correctly sized table.
@@ -45,7 +47,7 @@ describe( 'Table', () => {
await insertBlock( 'Table' );
// Create the table.
- const createButton = await page.$x( "//div[@data-type='core/table']//button[text()='Create']" );
+ const createButton = await page.$x( createButtonSelector );
await createButton[ 0 ].click();
// Click the first cell and add some text.
@@ -81,7 +83,7 @@ describe( 'Table', () => {
expect( footerSwitch ).toHaveLength( 0 );
// Create the table.
- const createButton = await page.$x( "//div[@data-type='core/table']//button[text()='Create']" );
+ const createButton = await page.$x( createButtonSelector );
await createButton[ 0 ].click();
// Expect the header and footer switches to be present now that the table has been created.
diff --git a/packages/e2e-tests/specs/editor-modes.test.js b/packages/e2e-tests/specs/editor-modes.test.js
index 1bb525123e197d..eda64312514c98 100644
--- a/packages/e2e-tests/specs/editor-modes.test.js
+++ b/packages/e2e-tests/specs/editor-modes.test.js
@@ -3,6 +3,7 @@
*/
import {
clickBlockAppender,
+ clickBlockToolbarButton,
createNewPost,
switchEditorModeTo,
} from '@wordpress/e2e-test-utils';
@@ -23,8 +24,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await page.keyboard.press( 'Escape' );
// Change editing mode from "Visual" to "HTML".
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
let changeModeButton = await page.waitForXPath( '//button[text()="Edit as HTML"]' );
await changeModeButton.click();
@@ -36,8 +36,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await page.keyboard.press( 'Escape' );
// Change editing mode from "HTML" back to "Visual".
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
changeModeButton = await page.waitForXPath( '//button[text()="Edit visually"]' );
await changeModeButton.click();
@@ -51,8 +50,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await page.keyboard.press( 'Escape' );
// Change editing mode from "Visual" to "HTML".
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const changeModeButton = await page.waitForXPath( '//button[text()="Edit as HTML"]' );
await changeModeButton.click();
@@ -67,8 +65,7 @@ describe( 'Editing modes (visual/HTML)', () => {
await page.keyboard.press( 'Escape' );
// Change editing mode from "Visual" to "HTML".
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const changeModeButton = await page.waitForXPath( '//button[text()="Edit as HTML"]' );
await changeModeButton.click();
@@ -101,7 +98,7 @@ describe( 'Editing modes (visual/HTML)', () => {
// Switch to Code Editor and hide More Menu
await switchEditorModeTo( 'Code' );
await page.click(
- '.edit-post-more-menu [aria-label="Hide more tools & options"]'
+ '.edit-post-more-menu [aria-label="More tools & options"]'
);
// The Block inspector should not be active anymore
diff --git a/packages/e2e-tests/specs/keyboard-navigable-blocks.test.js b/packages/e2e-tests/specs/keyboard-navigable-blocks.test.js
index 5253528a615cd0..64d1afcda1a61e 100644
--- a/packages/e2e-tests/specs/keyboard-navigable-blocks.test.js
+++ b/packages/e2e-tests/specs/keyboard-navigable-blocks.test.js
@@ -100,9 +100,7 @@ const tabThroughBlockToolbar = async () => {
// Tab to focus on the 'More formatting' dropdown toggle
await page.keyboard.press( 'Tab' );
const isFocusedBlockSettingsDropdown = await page.evaluate( () =>
- document.activeElement.classList.contains(
- 'block-editor-block-settings-menu__toggle'
- )
+ document.activeElement.classList.contains( 'components-dropdown-menu__toggle' )
);
await expect( isFocusedBlockSettingsDropdown ).toBe( true );
};
diff --git a/packages/e2e-tests/specs/nux.test.js b/packages/e2e-tests/specs/nux.test.js
index d8a64be089ecaf..40c980fac079cc 100644
--- a/packages/e2e-tests/specs/nux.test.js
+++ b/packages/e2e-tests/specs/nux.test.js
@@ -117,7 +117,7 @@ describe( 'New User Experience (NUX)', () => {
await clickAllTips( page );
// Open the "More" menu to check the "Show Tips" element.
- await page.click( '.edit-post-more-menu [aria-label="Show more tools & options"]' );
+ await page.click( '.edit-post-more-menu [aria-label="More tools & options"]' );
const showTipsButton = await page.$x( '//button[contains(text(), "Show Tips")][@aria-pressed="false"]' );
expect( showTipsButton ).toHaveLength( 1 );
@@ -132,7 +132,7 @@ describe( 'New User Experience (NUX)', () => {
await clickOnMoreMenuItem( 'Show Tips' );
// Open the "More" menu to check the "Show Tips" element.
- await page.click( '.edit-post-more-menu [aria-label="Show more tools & options"]' );
+ await page.click( '.edit-post-more-menu [aria-label="More tools & options"]' );
const showTipsButton = await page.$x( '//button[contains(text(), "Show Tips")][@aria-pressed="true"]' );
expect( showTipsButton ).toHaveLength( 1 );
diff --git a/packages/e2e-tests/specs/plugins/annotations.test.js b/packages/e2e-tests/specs/plugins/annotations.test.js
index 65474777d3db1d..5bab673a41d806 100644
--- a/packages/e2e-tests/specs/plugins/annotations.test.js
+++ b/packages/e2e-tests/specs/plugins/annotations.test.js
@@ -3,13 +3,14 @@
*/
import {
activatePlugin,
+ clickBlockToolbarButton,
clickOnMoreMenuItem,
createNewPost,
deactivatePlugin,
} from '@wordpress/e2e-test-utils';
const clickOnBlockSettingsMenuItem = async ( buttonLabel ) => {
- await expect( page ).toClick( '.block-editor-block-settings-menu__toggle' );
+ await clickBlockToolbarButton( 'More options' );
const itemButton = ( await page.$x( `//*[contains(@class, "block-editor-block-settings-menu__popover")]//button[contains(text(), '${ buttonLabel }')]` ) )[ 0 ];
await itemButton.click();
};
diff --git a/packages/e2e-tests/specs/reusable-blocks.test.js b/packages/e2e-tests/specs/reusable-blocks.test.js
index a8ab98c2dd3181..81f8db73d48629 100644
--- a/packages/e2e-tests/specs/reusable-blocks.test.js
+++ b/packages/e2e-tests/specs/reusable-blocks.test.js
@@ -24,9 +24,9 @@ describe( 'Reusable Blocks', () => {
beforeEach( async () => {
// Remove all blocks from the post so that we're working with a clean slate
await page.evaluate( () => {
- const blocks = wp.data.select( 'core/editor' ).getBlocks();
+ const blocks = wp.data.select( 'core/block-editor' ).getBlocks();
const clientIds = blocks.map( ( block ) => block.clientId );
- wp.data.dispatch( 'core/editor' ).removeBlocks( clientIds );
+ wp.data.dispatch( 'core/block-editor' ).removeBlocks( clientIds );
} );
} );
@@ -45,10 +45,8 @@ describe( 'Reusable Blocks', () => {
'//*[contains(@class, "components-snackbar")]/*[text()="Block created."]'
);
- // Select all of the text in the title field by triple-clicking on it. We
- // triple-click because, on Mac, Mod+A doesn't work. This step can be removed
- // when https://github.com/WordPress/gutenberg/issues/7972 is fixed
- await page.click( '.reusable-block-edit-panel__title', { clickCount: 3 } );
+ // Select all of the text in the title field.
+ await pressKeyWithModifier( 'primary', 'a' );
// Give the reusable block a title
await page.keyboard.type( 'Greeting block' );
@@ -155,7 +153,7 @@ describe( 'Reusable Blocks', () => {
await insertBlock( 'Surprised greeting block' );
// Convert block to a regular block
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const convertButton = await page.waitForXPath(
'//button[text()="Convert to Regular Block"]'
);
@@ -178,7 +176,7 @@ describe( 'Reusable Blocks', () => {
await insertBlock( 'Surprised greeting block' );
// Delete the block and accept the confirmation dialog
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const deleteButton = await page.waitForXPath( '//button[text()="Remove from Reusable Blocks"]' );
await Promise.all( [ waitForAndAcceptDialog(), deleteButton.click() ] );
@@ -214,8 +212,7 @@ describe( 'Reusable Blocks', () => {
await pressKeyWithModifier( 'primary', 'a' );
// Convert block to a reusable block
- await page.waitForSelector( 'button[aria-label="More options"]' );
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const convertButton = await page.waitForXPath( '//button[text()="Add to Reusable Blocks"]' );
await convertButton.click();
@@ -224,10 +221,8 @@ describe( 'Reusable Blocks', () => {
'//*[contains(@class, "components-snackbar")]/*[text()="Block created."]'
);
- // Select all of the text in the title field by triple-clicking on it. We
- // triple-click because, on Mac, Mod+A doesn't work. This step can be removed
- // when https://github.com/WordPress/gutenberg/issues/7972 is fixed
- await page.click( '.reusable-block-edit-panel__title', { clickCount: 3 } );
+ // Select all of the text in the title field.
+ await pressKeyWithModifier( 'primary', 'a' );
// Give the reusable block a title
await page.keyboard.type( 'Multi-selection reusable block' );
@@ -256,7 +251,7 @@ describe( 'Reusable Blocks', () => {
await insertBlock( 'Multi-selection reusable block' );
// Convert block to a regular block
- await page.click( 'button[aria-label="More options"]' );
+ await clickBlockToolbarButton( 'More options' );
const convertButton = await page.waitForXPath(
'//button[text()="Convert to Regular Block"]'
);
diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js
index 1bd6d79c1b10fc..6dce07f78146ca 100644
--- a/packages/e2e-tests/specs/rich-text.test.js
+++ b/packages/e2e-tests/specs/rich-text.test.js
@@ -27,15 +27,6 @@ describe( 'RichText', () => {
expect( await getEditedPostContent() ).toMatchSnapshot();
} );
- it( 'should apply formatting with access shortcut', async () => {
- await clickBlockAppender();
- await page.keyboard.type( 'test' );
- await pressKeyWithModifier( 'primary', 'a' );
- await pressKeyWithModifier( 'access', 'd' );
-
- expect( await getEditedPostContent() ).toMatchSnapshot();
- } );
-
it( 'should apply formatting with primary shortcut', async () => {
await clickBlockAppender();
await page.keyboard.type( 'test' );
diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json
index 74a706fffc4dc1..b6c85e26311ece 100644
--- a/packages/edit-post/package.json
+++ b/packages/edit-post/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/edit-post",
- "version": "3.4.0",
+ "version": "3.5.0",
"description": "Edit Post module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -33,7 +33,6 @@
"@wordpress/data": "file:../data",
"@wordpress/editor": "file:../editor",
"@wordpress/element": "file:../element",
- "@wordpress/format-library": "file:../format-library",
"@wordpress/hooks": "file:../hooks",
"@wordpress/i18n": "file:../i18n",
"@wordpress/keycodes": "file:../keycodes",
diff --git a/packages/edit-post/src/components/fullscreen-mode/style.scss b/packages/edit-post/src/components/fullscreen-mode/style.scss
index 9d51282381eb0e..05ce2a356b351c 100644
--- a/packages/edit-post/src/components/fullscreen-mode/style.scss
+++ b/packages/edit-post/src/components/fullscreen-mode/style.scss
@@ -26,7 +26,7 @@ body.js.is-fullscreen-mode {
.edit-post-header {
transform: translateY(-100%);
animation: edit-post-fullscreen-mode__slide-in-animation 0.1s forwards;
- @include reduce-motion;
+ @include reduce-motion("animation");
}
}
}
diff --git a/packages/edit-post/src/components/header/more-menu/index.js b/packages/edit-post/src/components/header/more-menu/index.js
index 9458a73c66b5e3..16d9b16afb77a1 100644
--- a/packages/edit-post/src/components/header/more-menu/index.js
+++ b/packages/edit-post/src/components/header/more-menu/index.js
@@ -2,7 +2,7 @@
* WordPress dependencies
*/
import { __ } from '@wordpress/i18n';
-import { IconButton, Dropdown, MenuGroup } from '@wordpress/components';
+import { DropdownMenu, MenuGroup } from '@wordpress/components';
/**
* Internal dependencies
@@ -13,24 +13,16 @@ import ToolsMoreMenuGroup from '../tools-more-menu-group';
import OptionsMenuItem from '../options-menu-item';
import WritingMenu from '../writing-menu';
-const ariaClosed = __( 'Show more tools & options' );
-const ariaOpen = __( 'Hide more tools & options' );
-
const MoreMenu = () => (
-
(
-
- ) }
- renderContent={ ( { onClose } ) => (
+ icon="ellipsis"
+ label={ __( 'More tools & options' ) }
+ __unstableLabelPosition="bottom"
+ __unstablePopoverClassName="edit-post-more-menu__content"
+ >
+ { ( { onClose } ) => (
<>
@@ -41,7 +33,7 @@ const MoreMenu = () => (
>
) }
- />
+
);
export default MoreMenu;
diff --git a/packages/edit-post/src/components/header/more-menu/style.scss b/packages/edit-post/src/components/header/more-menu/style.scss
index 011e4eeb91337b..a6e9a1cdc68ae3 100644
--- a/packages/edit-post/src/components/header/more-menu/style.scss
+++ b/packages/edit-post/src/components/header/more-menu/style.scss
@@ -29,16 +29,7 @@
max-width: $break-mobile;
}
- .components-menu-group:not(:last-child),
- > div:not(:last-child) .components-menu-group {
- border-bottom: $border-width solid $light-gray-500;
- }
-
- .components-menu-item__button {
- padding-left: 2rem;
-
- &.has-icon {
- padding-left: 0.5rem;
- }
+ .components-dropdown-menu__menu {
+ padding: 0;
}
}
diff --git a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap
index 78f197fda6035d..cc17255097c02d 100644
--- a/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap
+++ b/packages/edit-post/src/components/header/more-menu/test/__snapshots__/index.js.snap
@@ -2,64 +2,70 @@
exports[`MoreMenu should match snapshot 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ >
+
+
+
+
+
+
+
+
+
+
+
+
`;
diff --git a/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
index 6e395b2e7ebae9..0a2564900eda2e 100644
--- a/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
+++ b/packages/edit-post/src/components/header/plugin-more-menu-item/test/__snapshots__/index.js.snap
@@ -5,6 +5,7 @@ exports[`PluginMoreMenuItem renders menu item as button properly 1`] = `
className="components-menu-group"
>
{ __( 'The last part of the URL. ' ) }
-
+
{ __( 'Read about permalinks' ) }
diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss
index 457f7e0361b21e..73d9431661c267 100644
--- a/packages/edit-post/src/components/sidebar/settings-header/style.scss
+++ b/packages/edit-post/src/components/sidebar/settings-header/style.scss
@@ -26,6 +26,7 @@
color: $dark-gray-900;
@include square-style__neutral;
transition: box-shadow 0.1s linear;
+ @include reduce-motion("transition");
// This pseudo-element "duplicates" the tab label and sets the text to bold.
// This ensures that the tab doesn't change width when selected.
diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss
index 62936b0f0a7ff6..740355cf636f75 100644
--- a/packages/edit-post/src/components/sidebar/style.scss
+++ b/packages/edit-post/src/components/sidebar/style.scss
@@ -164,6 +164,7 @@
font-weight: 400;
@include square-style__neutral;
transition: box-shadow 0.1s linear;
+ @include reduce-motion("transition");
&.is-active {
box-shadow: inset 0 -3px theme(outlines);
diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js
index 28e55b08d6cbf6..ea48a3a1d8e358 100644
--- a/packages/edit-post/src/editor.js
+++ b/packages/edit-post/src/editor.js
@@ -91,6 +91,7 @@ class Editor extends Component {
settings={ editorSettings }
post={ post }
initialEdits={ initialEdits }
+ useSubRegistry={ false }
{ ...props }
>
diff --git a/packages/edit-post/src/hooks/components/index.js b/packages/edit-post/src/hooks/components/index.js
index 20f16a7461def2..709d89d013ed5a 100644
--- a/packages/edit-post/src/hooks/components/index.js
+++ b/packages/edit-post/src/hooks/components/index.js
@@ -2,16 +2,12 @@
* WordPress dependencies
*/
import { addFilter } from '@wordpress/hooks';
-
-/**
- * Internal dependencies
- */
-import MediaUpload from './media-upload';
+import { MediaUpload } from '@wordpress/media-utils';
const replaceMediaUpload = () => MediaUpload;
addFilter(
'editor.MediaUpload',
- 'core/edit-post/components/media-upload/replace-media-upload',
+ 'core/edit-post/replace-media-upload',
replaceMediaUpload
);
diff --git a/packages/edit-post/src/index.native.js b/packages/edit-post/src/index.native.js
index cc5dfaec90ae1d..5854c991101955 100644
--- a/packages/edit-post/src/index.native.js
+++ b/packages/edit-post/src/index.native.js
@@ -1,3 +1,8 @@
+/**
+ * External dependencies
+ */
+import { Platform } from 'react-native';
+
/**
* WordPress dependencies
*/
@@ -18,13 +23,15 @@ export function initializeEditor() {
// register and setup blocks
registerCoreBlocks();
- // disable Code and More blocks for the release
+ // disable Code block for the release
// eslint-disable-next-line no-undef
if ( typeof __DEV__ === 'undefined' || ! __DEV__ ) {
unregisterBlockType( 'core/code' );
- unregisterBlockType( 'core/more' );
- unregisterBlockType( 'core/video' );
- unregisterBlockType( 'core/quote' );
+
+ // Disable Video block except for iOS for now.
+ if ( Platform.OS !== 'ios' ) {
+ unregisterBlockType( 'core/video' );
+ }
}
}
diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json
index d92d4a66c78068..1acce755f85426 100644
--- a/packages/edit-widgets/package.json
+++ b/packages/edit-widgets/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/edit-widgets",
- "version": "0.3.0",
+ "version": "0.4.0",
"private": true,
"description": "Widgets Page module for WordPress..",
"author": "The WordPress Contributors",
diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js
index 3dc3f650e9d8ea..f18519fc3d5fcc 100644
--- a/packages/edit-widgets/src/components/header/index.js
+++ b/packages/edit-widgets/src/components/header/index.js
@@ -1,12 +1,14 @@
/**
* WordPress dependencies
*/
-import { compose } from '@wordpress/compose';
-import { Button } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
-import { withDispatch } from '@wordpress/data';
-function Header( { saveWidgetAreas } ) {
+/**
+ * Internal dependencies
+ */
+import SaveButton from '../save-button';
+
+function Header() {
return (
-
- { __( 'Update' ) }
-
+
);
}
-export default compose( [
- withDispatch( ( dispatch ) => {
- const { saveWidgetAreas } = dispatch( 'core/edit-widgets' );
- return {
- saveWidgetAreas,
- };
- } ),
-] )( Header );
-
+export default Header;
diff --git a/packages/edit-widgets/src/components/save-button/index.js b/packages/edit-widgets/src/components/save-button/index.js
new file mode 100644
index 00000000000000..dd9040356d95ee
--- /dev/null
+++ b/packages/edit-widgets/src/components/save-button/index.js
@@ -0,0 +1,31 @@
+/**
+ * WordPress dependencies
+ */
+import { Button } from '@wordpress/components';
+import { __ } from '@wordpress/i18n';
+import { useDispatch } from '@wordpress/data';
+import { useState, useCallback } from '@wordpress/element';
+
+function SaveButton() {
+ const [ isSaving, setIsSaving ] = useState( false );
+ const { saveWidgetAreas } = useDispatch( 'core/edit-widgets' );
+ const onClick = useCallback( async () => {
+ setIsSaving( true );
+ await saveWidgetAreas();
+ setIsSaving( false );
+ }, [] );
+
+ return (
+
+ { __( 'Update' ) }
+
+ );
+}
+
+export default SaveButton;
diff --git a/packages/edit-widgets/src/components/widget-area/index.js b/packages/edit-widgets/src/components/widget-area/index.js
index 4825a45cefc6e4..0397a77c02957f 100644
--- a/packages/edit-widgets/src/components/widget-area/index.js
+++ b/packages/edit-widgets/src/components/widget-area/index.js
@@ -1,6 +1,13 @@
+/**
+ * External dependencies
+ */
+import { defaultTo } from 'lodash';
+
/**
* WordPress dependencies
*/
+import { useMemo } from '@wordpress/element';
+import { uploadMedia } from '@wordpress/media-utils';
import { compose } from '@wordpress/compose';
import { Panel, PanelBody } from '@wordpress/components';
import {
@@ -9,13 +16,35 @@ import {
} from '@wordpress/block-editor';
import { withDispatch, withSelect } from '@wordpress/data';
+function getBlockEditorSettings( blockEditorSettings, hasUploadPermissions ) {
+ if ( ! hasUploadPermissions ) {
+ return blockEditorSettings;
+ }
+ const mediaUploadBlockEditor = ( { onError, ...argumentsObject } ) => {
+ uploadMedia( {
+ wpAllowedMimeTypes: blockEditorSettings.allowedMimeTypes,
+ onError: ( { message } ) => onError( message ),
+ ...argumentsObject,
+ } );
+ };
+ return {
+ ...blockEditorSettings,
+ __experimentalMediaUpload: mediaUploadBlockEditor,
+ };
+}
+
function WidgetArea( {
blockEditorSettings,
blocks,
initialOpen,
updateBlocks,
widgetAreaName,
+ hasUploadPermissions,
} ) {
+ const settings = useMemo(
+ () => getBlockEditorSettings( blockEditorSettings, hasUploadPermissions ),
+ [ blockEditorSettings, hasUploadPermissions ]
+ );
return (
@@ -41,11 +70,13 @@ export default compose( [
getBlocksFromWidgetArea,
getWidgetArea,
} = select( 'core/edit-widgets' );
+ const { canUser } = select( 'core' );
const blocks = getBlocksFromWidgetArea( id );
const widgetAreaName = ( getWidgetArea( id ) || {} ).name;
return {
blocks,
widgetAreaName,
+ hasUploadPermissions: defaultTo( canUser( 'create', 'media' ), true ),
};
} ),
withDispatch( ( dispatch, { id } ) => {
diff --git a/packages/edit-widgets/src/hooks/components/index.js b/packages/edit-widgets/src/hooks/components/index.js
new file mode 100644
index 00000000000000..4c24e365661fae
--- /dev/null
+++ b/packages/edit-widgets/src/hooks/components/index.js
@@ -0,0 +1,13 @@
+/**
+ * WordPress dependencies
+ */
+import { addFilter } from '@wordpress/hooks';
+import { MediaUpload } from '@wordpress/media-utils';
+
+const replaceMediaUpload = () => MediaUpload;
+
+addFilter(
+ 'editor.MediaUpload',
+ 'core/edit-widgets/replace-media-upload',
+ replaceMediaUpload
+);
diff --git a/packages/edit-widgets/src/hooks/index.js b/packages/edit-widgets/src/hooks/index.js
new file mode 100644
index 00000000000000..c6cbc1d173e861
--- /dev/null
+++ b/packages/edit-widgets/src/hooks/index.js
@@ -0,0 +1,4 @@
+/**
+ * Internal dependencies
+ */
+import './components';
diff --git a/packages/edit-widgets/src/index.js b/packages/edit-widgets/src/index.js
index c61d4c2f9bb781..4480c5b0a06ec0 100644
--- a/packages/edit-widgets/src/index.js
+++ b/packages/edit-widgets/src/index.js
@@ -7,6 +7,7 @@ import { registerCoreBlocks } from '@wordpress/block-library';
/**
* Internal dependencies
*/
+import './hooks';
import './store';
import EditWidgetsInitializer from './components/edit-widgets-initializer';
diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md
index 3840d850ed599a..dd4389226720c5 100644
--- a/packages/editor/CHANGELOG.md
+++ b/packages/editor/CHANGELOG.md
@@ -1,8 +1,154 @@
+## 9.4.0 (2019-06-12)
+
+### Deprecations
+
+- The following components are deprecated as moved to the `@wordpress/block-editor` package:
+ - Autocomplete,
+ - AlignmentToolbar,
+ - BlockAlignmentToolbar,
+ - BlockControls,
+ - BlockEdit,
+ - BlockEditorKeyboardShortcuts,
+ - BlockFormatControls,
+ - BlockIcon,
+ - BlockInspector,
+ - BlockList,
+ - BlockMover,
+ - BlockNavigationDropdown,
+ - BlockSelectionClearer,
+ - BlockSettingsMenu,
+ - BlockTitle,
+ - BlockToolbar,
+ - ColorPalette,
+ - ContrastChecker,
+ - CopyHandler,
+ - createCustomColorsHOC,
+ - DefaultBlockAppender,
+ - FontSizePicker,
+ - getColorClassName,
+ - getColorObjectByAttributeValues,
+ - getColorObjectByColorValue,
+ - getFontSize,
+ - getFontSizeClass,
+ - Inserter,
+ - InnerBlocks,
+ - InspectorAdvancedControls,
+ - InspectorControls,
+ - PanelColorSettings,
+ - PlainText,
+ - RichText,
+ - RichTextShortcut,
+ - RichTextToolbarButton,
+ - RichTextInserterItem,
+ - MediaPlaceholder,
+ - MediaUpload,
+ - MediaUploadCheck,
+ - MultiBlocksSwitcher,
+ - MultiSelectScrollIntoView,
+ - NavigableToolbar,
+ - ObserveTyping,
+ - PreserveScrollInReorder,
+ - SkipToSelectedBlock,
+ - URLInput,
+ - URLInputButton,
+ - URLPopover,
+ - Warning,
+ - WritingFlow,
+ - withColorContext,
+ - withColors,
+ - withFontSizes.
+- The following actions are deprecated as moved to the `core/block-editor` store:
+ - resetBlocks,
+ - receiveBlocks,
+ - updateBlock,
+ - updateBlockAttributes,
+ - selectBlock,
+ - startMultiSelect,
+ - stopMultiSelect,
+ - multiSelect,
+ - clearSelectedBlock,
+ - toggleSelection,
+ - replaceBlocks,
+ - replaceBlock,
+ - moveBlocksDown,
+ - moveBlocksUp,
+ - moveBlockToPosition,
+ - insertBlock,
+ - insertBlocks,
+ - showInsertionPoint,
+ - hideInsertionPoint,
+ - setTemplateValidity,
+ - synchronizeTemplate,
+ - mergeBlocks,
+ - removeBlocks,
+ - removeBlock,
+ - toggleBlockMode,
+ - startTyping,
+ - stopTyping,
+ - enterFormattedText,
+ - exitFormattedText,
+ - insertDefaultBlock,
+ - updateBlockListSettings.
+- The following selectors are deprecated as moved to the `core/block-editor` store:
+ - getBlockDependantsCacheBust,
+ - getBlockName,
+ - isBlockValid,
+ - getBlockAttributes,
+ - getBlock,
+ - getBlocks,
+ - getClientIdsOfDescendants,
+ - getClientIdsWithDescendants,
+ - getGlobalBlockCount,
+ - getBlocksByClientId,
+ - getBlockCount,
+ - getBlockSelectionStart,
+ - getBlockSelectionEnd,
+ - getSelectedBlockCount,
+ - hasSelectedBlock,
+ - getSelectedBlockClientId,
+ - getSelectedBlock,
+ - getBlockRootClientId,
+ - getBlockHierarchyRootClientId,
+ - getAdjacentBlockClientId,
+ - getPreviousBlockClientId,
+ - getNextBlockClientId,
+ - getSelectedBlocksInitialCaretPosition,
+ - getMultiSelectedBlockClientIds,
+ - getMultiSelectedBlocks,
+ - getFirstMultiSelectedBlockClientId,
+ - getLastMultiSelectedBlockClientId,
+ - isFirstMultiSelectedBlock,
+ - isBlockMultiSelected,
+ - isAncestorMultiSelected,
+ - getMultiSelectedBlocksStartClientId,
+ - getMultiSelectedBlocksEndClientId,
+ - getBlockOrder,
+ - getBlockIndex,
+ - isBlockSelected,
+ - hasSelectedInnerBlock,
+ - isBlockWithinSelection,
+ - hasMultiSelection,
+ - isMultiSelecting,
+ - isSelectionEnabled,
+ - getBlockMode =,
+ - isTyping,
+ - isCaretWithinFormattedText,
+ - getBlockInsertionPoint,
+ - isBlockInsertionPointVisible,
+ - isValidTemplate,
+ - getTemplate,
+ - getTemplateLock,
+ - canInsertBlockType,
+ - getInserterItems,
+ - hasInserterItems,
+ - getBlockListSettings.
+
## 9.3.0 (2019-05-21)
### Deprecations
- The `getAutosave`, `getAutosaveAttribute`, and `hasAutosave` selectors are deprecated. Please use the `getAutosave` selector in the `@wordpress/core-data` package.
- The `resetAutosave` action is deprecated. An equivalent action `receiveAutosaves` has been added to the `@wordpress/core-data` package.
+- `ServerSideRender` component was deprecated. The component is now available in `@wordpress/server-side-render`.
### Internal
diff --git a/packages/editor/package.json b/packages/editor/package.json
index 74384ff0ce3900..ed99bfb65a924e 100644
--- a/packages/editor/package.json
+++ b/packages/editor/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/editor",
- "version": "9.3.0",
+ "version": "9.4.0",
"description": "Building blocks for WordPress editors.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -38,6 +38,7 @@
"@wordpress/html-entities": "file:../html-entities",
"@wordpress/i18n": "file:../i18n",
"@wordpress/keycodes": "file:../keycodes",
+ "@wordpress/media-utils": "file:../media-utils",
"@wordpress/notices": "file:../notices",
"@wordpress/nux": "file:../nux",
"@wordpress/url": "file:../url",
diff --git a/packages/editor/src/components/convert-to-group-buttons/convert-button.js b/packages/editor/src/components/convert-to-group-buttons/convert-button.js
new file mode 100644
index 00000000000000..6a7299ed259c98
--- /dev/null
+++ b/packages/editor/src/components/convert-to-group-buttons/convert-button.js
@@ -0,0 +1,127 @@
+/**
+ * External dependencies
+ */
+import { noop } from 'lodash';
+
+/**
+ * WordPress dependencies
+ */
+import { Fragment } from '@wordpress/element';
+import { MenuItem } from '@wordpress/components';
+import { _x } from '@wordpress/i18n';
+import { switchToBlockType } from '@wordpress/blocks';
+import { withSelect, withDispatch } from '@wordpress/data';
+import { compose } from '@wordpress/compose';
+
+/**
+ * Internal dependencies
+ */
+import { Group, Ungroup } from './icons';
+
+export function ConvertToGroupButton( {
+ onConvertToGroup,
+ onConvertFromGroup,
+ isGroupable = false,
+ isUngroupable = false,
+} ) {
+ return (
+
+ { isGroupable && (
+
+ { _x( 'Group', 'verb' ) }
+
+ ) }
+ { isUngroupable && (
+
+ { _x( 'Ungroup', 'Ungrouping blocks from within a Group block back into individual blocks within the Editor ' ) }
+
+ ) }
+
+ );
+}
+
+export default compose( [
+ withSelect( ( select, { clientIds } ) => {
+ const {
+ getBlocksByClientId,
+ canInsertBlockType,
+ } = select( 'core/block-editor' );
+
+ const containerBlockAvailable = canInsertBlockType( 'core/group' );
+
+ const blocksSelection = getBlocksByClientId( clientIds );
+
+ const isSingleContainerBlock = blocksSelection.length === 1 && blocksSelection[ 0 ] && blocksSelection[ 0 ].name === 'core/group';
+
+ // Do we have
+ // 1. Container block available to be inserted?
+ // 2. One or more blocks selected
+ // (we allow single Blocks to become groups unless
+ // they are a soltiary group block themselves)
+ const isGroupable = (
+ containerBlockAvailable &&
+ blocksSelection.length &&
+ ! isSingleContainerBlock
+ );
+
+ // Do we have a single Group Block selected?
+ const isUngroupable = isSingleContainerBlock;
+
+ return {
+ isGroupable,
+ isUngroupable,
+ blocksSelection,
+ };
+ } ),
+ withDispatch( ( dispatch, { clientIds, onToggle = noop, blocksSelection = [] } ) => {
+ const {
+ replaceBlocks,
+ } = dispatch( 'core/block-editor' );
+
+ return {
+ onConvertToGroup() {
+ if ( ! blocksSelection.length ) {
+ return;
+ }
+
+ // Activate the `transform` on `core/group` which does the conversion
+ const newBlocks = switchToBlockType( blocksSelection, 'core/group' );
+
+ if ( newBlocks ) {
+ replaceBlocks(
+ clientIds,
+ newBlocks
+ );
+ }
+
+ onToggle();
+ },
+ onConvertFromGroup() {
+ if ( ! blocksSelection.length ) {
+ return;
+ }
+
+ const innerBlocks = blocksSelection[ 0 ].innerBlocks;
+
+ if ( ! innerBlocks.length ) {
+ return;
+ }
+
+ replaceBlocks(
+ clientIds,
+ innerBlocks
+ );
+
+ onToggle();
+ },
+ };
+ } ),
+] )( ConvertToGroupButton );
diff --git a/packages/editor/src/components/convert-to-group-buttons/icons.js b/packages/editor/src/components/convert-to-group-buttons/icons.js
new file mode 100644
index 00000000000000..990d15dd5ac1cd
--- /dev/null
+++ b/packages/editor/src/components/convert-to-group-buttons/icons.js
@@ -0,0 +1,13 @@
+/**
+ * WordPress dependencies
+ */
+import { Icon, SVG, Path } from '@wordpress/components';
+
+const GroupSVG = ;
+
+export const Group = ;
+
+const UngroupSVG = ;
+
+export const Ungroup = ;
+
diff --git a/packages/editor/src/components/convert-to-group-buttons/index.js b/packages/editor/src/components/convert-to-group-buttons/index.js
new file mode 100644
index 00000000000000..a276ed4434bcab
--- /dev/null
+++ b/packages/editor/src/components/convert-to-group-buttons/index.js
@@ -0,0 +1,33 @@
+/**
+ * WordPress dependencies
+ */
+import { Fragment } from '@wordpress/element';
+import { __experimentalBlockSettingsMenuPluginsExtension } from '@wordpress/block-editor';
+import { withSelect } from '@wordpress/data';
+
+/**
+ * Internal dependencies
+ */
+import ConvertToGroupButton from './convert-button';
+
+function ConvertToGroupButtons( { clientIds } ) {
+ return (
+ <__experimentalBlockSettingsMenuPluginsExtension>
+ { ( { onClose } ) => (
+
+
+
+ ) }
+
+ );
+}
+
+export default withSelect( ( select ) => {
+ const { getSelectedBlockClientIds } = select( 'core/block-editor' );
+ return {
+ clientIds: getSelectedBlockClientIds(),
+ };
+} )( ConvertToGroupButtons );
diff --git a/packages/editor/src/components/deprecated.js b/packages/editor/src/components/deprecated.js
index 8468f254c2dccf..022da5c4023fa2 100644
--- a/packages/editor/src/components/deprecated.js
+++ b/packages/editor/src/components/deprecated.js
@@ -2,118 +2,151 @@
/**
* WordPress dependencies
*/
+import deprecated from '@wordpress/deprecated';
+import { forwardRef } from '@wordpress/element';
import {
- Autocomplete,
- AlignmentToolbar,
- BlockAlignmentToolbar,
- BlockControls,
- BlockEdit,
- BlockEditorKeyboardShortcuts,
- BlockFormatControls,
- BlockIcon,
- BlockInspector,
- BlockList,
- BlockMover,
- BlockNavigationDropdown,
- BlockSelectionClearer,
- BlockSettingsMenu,
- BlockTitle,
- BlockToolbar,
- ColorPalette,
- ContrastChecker,
- CopyHandler,
- createCustomColorsHOC,
- DefaultBlockAppender,
- FontSizePicker,
- getColorClassName,
- getColorObjectByAttributeValues,
- getColorObjectByColorValue,
- getFontSize,
- getFontSizeClass,
- Inserter,
- InnerBlocks,
- InspectorAdvancedControls,
- InspectorControls,
- PanelColorSettings,
- PlainText,
- RichText,
- RichTextShortcut,
- RichTextToolbarButton,
- RichTextInserterItem,
- __unstableRichTextInputEvent,
- MediaPlaceholder,
- MediaUpload,
- MediaUploadCheck,
- MultiBlocksSwitcher,
- MultiSelectScrollIntoView,
- NavigableToolbar,
- ObserveTyping,
- PreserveScrollInReorder,
- SkipToSelectedBlock,
- URLInput,
- URLInputButton,
- URLPopover,
- Warning,
- WritingFlow,
- withColorContext,
- withColors,
- withFontSizes,
+ Autocomplete as RootAutocomplete,
+ AlignmentToolbar as RootAlignmentToolbar,
+ BlockAlignmentToolbar as RootBlockAlignmentToolbar,
+ BlockControls as RootBlockControls,
+ BlockEdit as RootBlockEdit,
+ BlockEditorKeyboardShortcuts as RootBlockEditorKeyboardShortcuts,
+ BlockFormatControls as RootBlockFormatControls,
+ BlockIcon as RootBlockIcon,
+ BlockInspector as RootBlockInspector,
+ BlockList as RootBlockList,
+ BlockMover as RootBlockMover,
+ BlockNavigationDropdown as RootBlockNavigationDropdown,
+ BlockSelectionClearer as RootBlockSelectionClearer,
+ BlockSettingsMenu as RootBlockSettingsMenu,
+ BlockTitle as RootBlockTitle,
+ BlockToolbar as RootBlockToolbar,
+ ColorPalette as RootColorPalette,
+ ContrastChecker as RootContrastChecker,
+ CopyHandler as RootCopyHandler,
+ createCustomColorsHOC as rootCreateCustomColorsHOC,
+ DefaultBlockAppender as RootDefaultBlockAppender,
+ FontSizePicker as RootFontSizePicker,
+ getColorClassName as rootGetColorClassName,
+ getColorObjectByAttributeValues as rootGetColorObjectByAttributeValues,
+ getColorObjectByColorValue as rootGetColorObjectByColorValue,
+ getFontSize as rootGetFontSize,
+ getFontSizeClass as rootGetFontSizeClass,
+ Inserter as RootInserter,
+ InnerBlocks as RootInnerBlocks,
+ InspectorAdvancedControls as RootInspectorAdvancedControls,
+ InspectorControls as RootInspectorControls,
+ PanelColorSettings as RootPanelColorSettings,
+ PlainText as RootPlainText,
+ RichText as RootRichText,
+ RichTextShortcut as RootRichTextShortcut,
+ RichTextToolbarButton as RootRichTextToolbarButton,
+ __unstableRichTextInputEvent as __unstableRootRichTextInputEvent,
+ MediaPlaceholder as RootMediaPlaceholder,
+ MediaUpload as RootMediaUpload,
+ MediaUploadCheck as RootMediaUploadCheck,
+ MultiBlocksSwitcher as RootMultiBlocksSwitcher,
+ MultiSelectScrollIntoView as RootMultiSelectScrollIntoView,
+ NavigableToolbar as RootNavigableToolbar,
+ ObserveTyping as RootObserveTyping,
+ PreserveScrollInReorder as RootPreserveScrollInReorder,
+ SkipToSelectedBlock as RootSkipToSelectedBlock,
+ URLInput as RootURLInput,
+ URLInputButton as RootURLInputButton,
+ URLPopover as RootURLPopover,
+ Warning as RootWarning,
+ WritingFlow as RootWritingFlow,
+ withColorContext as rootWithColorContext,
+ withColors as rootWithColors,
+ withFontSizes as rootWithFontSizes,
} from '@wordpress/block-editor';
-export {
- Autocomplete,
- AlignmentToolbar,
- BlockAlignmentToolbar,
- BlockControls,
- BlockEdit,
- BlockEditorKeyboardShortcuts,
- BlockFormatControls,
- BlockIcon,
- BlockInspector,
- BlockList,
- BlockMover,
- BlockNavigationDropdown,
- BlockSelectionClearer,
- BlockSettingsMenu,
- BlockTitle,
- BlockToolbar,
- ColorPalette,
- ContrastChecker,
- CopyHandler,
- createCustomColorsHOC,
- DefaultBlockAppender,
- FontSizePicker,
- getColorClassName,
- getColorObjectByAttributeValues,
- getColorObjectByColorValue,
- getFontSize,
- getFontSizeClass,
- Inserter,
- InnerBlocks,
- InspectorAdvancedControls,
- InspectorControls,
- PanelColorSettings,
- PlainText,
- RichText,
- RichTextShortcut,
- RichTextToolbarButton,
- RichTextInserterItem,
- __unstableRichTextInputEvent,
- MediaPlaceholder,
- MediaUpload,
- MediaUploadCheck,
- MultiBlocksSwitcher,
- MultiSelectScrollIntoView,
- NavigableToolbar,
- ObserveTyping,
- PreserveScrollInReorder,
- SkipToSelectedBlock,
- URLInput,
- URLInputButton,
- URLPopover,
- Warning,
- WritingFlow,
- withColorContext,
- withColors,
- withFontSizes,
-};
+export { default as ServerSideRender } from '@wordpress/server-side-render';
+
+function deprecateComponent( name, Wrapped, staticsToHoist = [] ) {
+ const Component = forwardRef( ( props, ref ) => {
+ deprecated( 'wp.editor.' + name, {
+ alternative: 'wp.blockEditor.' + name,
+ } );
+
+ return ;
+ } );
+
+ staticsToHoist.forEach( ( staticName ) => {
+ Component[ staticName ] = deprecateComponent(
+ name + '.' + staticName,
+ Wrapped[ staticName ]
+ );
+ } );
+
+ return Component;
+}
+
+function deprecateFunction( name, func ) {
+ return ( ...args ) => {
+ deprecated( 'wp.editor.' + name, {
+ alternative: 'wp.blockEditor.' + name,
+ } );
+
+ return func( ...args );
+ };
+}
+
+const RichText = deprecateComponent( 'RichText', RootRichText, [ 'Content' ] );
+RichText.isEmpty = deprecateFunction( 'RichText.isEmpty', RootRichText.isEmpty );
+
+export { RichText };
+export const Autocomplete = deprecateComponent( 'Autocomplete', RootAutocomplete );
+export const AlignmentToolbar = deprecateComponent( 'AlignmentToolbar', RootAlignmentToolbar );
+export const BlockAlignmentToolbar = deprecateComponent( 'BlockAlignmentToolbar', RootBlockAlignmentToolbar );
+export const BlockControls = deprecateComponent( 'BlockControls', RootBlockControls, [ 'Slot' ] );
+export const BlockEdit = deprecateComponent( 'BlockEdit', RootBlockEdit );
+export const BlockEditorKeyboardShortcuts = deprecateComponent( 'BlockEditorKeyboardShortcuts', RootBlockEditorKeyboardShortcuts );
+export const BlockFormatControls = deprecateComponent( 'BlockFormatControls', RootBlockFormatControls, [ 'Slot' ] );
+export const BlockIcon = deprecateComponent( 'BlockIcon', RootBlockIcon );
+export const BlockInspector = deprecateComponent( 'BlockInspector', RootBlockInspector );
+export const BlockList = deprecateComponent( 'BlockList', RootBlockList );
+export const BlockMover = deprecateComponent( 'BlockMover', RootBlockMover );
+export const BlockNavigationDropdown = deprecateComponent( 'BlockNavigationDropdown', RootBlockNavigationDropdown );
+export const BlockSelectionClearer = deprecateComponent( 'BlockSelectionClearer', RootBlockSelectionClearer );
+export const BlockSettingsMenu = deprecateComponent( 'BlockSettingsMenu', RootBlockSettingsMenu );
+export const BlockTitle = deprecateComponent( 'BlockTitle', RootBlockTitle );
+export const BlockToolbar = deprecateComponent( 'BlockToolbar', RootBlockToolbar );
+export const ColorPalette = deprecateComponent( 'ColorPalette', RootColorPalette );
+export const ContrastChecker = deprecateComponent( 'ContrastChecker', RootContrastChecker );
+export const CopyHandler = deprecateComponent( 'CopyHandler', RootCopyHandler );
+export const DefaultBlockAppender = deprecateComponent( 'DefaultBlockAppender', RootDefaultBlockAppender );
+export const FontSizePicker = deprecateComponent( 'FontSizePicker', RootFontSizePicker );
+export const Inserter = deprecateComponent( 'Inserter', RootInserter );
+export const InnerBlocks = deprecateComponent( 'InnerBlocks', RootInnerBlocks, [ 'ButtonBlockAppender', 'DefaultBlockAppender', 'Content' ] );
+export const InspectorAdvancedControls = deprecateComponent( 'InspectorAdvancedControls', RootInspectorAdvancedControls, [ 'Slot' ] );
+export const InspectorControls = deprecateComponent( 'InspectorControls', RootInspectorControls, [ 'Slot' ] );
+export const PanelColorSettings = deprecateComponent( 'PanelColorSettings', RootPanelColorSettings );
+export const PlainText = deprecateComponent( 'PlainText', RootPlainText );
+export const RichTextShortcut = deprecateComponent( 'RichTextShortcut', RootRichTextShortcut );
+export const RichTextToolbarButton = deprecateComponent( 'RichTextToolbarButton', RootRichTextToolbarButton );
+export const __unstableRichTextInputEvent = deprecateComponent( '__unstableRichTextInputEvent', __unstableRootRichTextInputEvent );
+export const MediaPlaceholder = deprecateComponent( 'MediaPlaceholder', RootMediaPlaceholder );
+export const MediaUpload = deprecateComponent( 'MediaUpload', RootMediaUpload );
+export const MediaUploadCheck = deprecateComponent( 'MediaUploadCheck', RootMediaUploadCheck );
+export const MultiBlocksSwitcher = deprecateComponent( 'MultiBlocksSwitcher', RootMultiBlocksSwitcher );
+export const MultiSelectScrollIntoView = deprecateComponent( 'MultiSelectScrollIntoView', RootMultiSelectScrollIntoView );
+export const NavigableToolbar = deprecateComponent( 'NavigableToolbar', RootNavigableToolbar );
+export const ObserveTyping = deprecateComponent( 'ObserveTyping', RootObserveTyping );
+export const PreserveScrollInReorder = deprecateComponent( 'PreserveScrollInReorder', RootPreserveScrollInReorder );
+export const SkipToSelectedBlock = deprecateComponent( 'SkipToSelectedBlock', RootSkipToSelectedBlock );
+export const URLInput = deprecateComponent( 'URLInput', RootURLInput );
+export const URLInputButton = deprecateComponent( 'URLInputButton', RootURLInputButton );
+export const URLPopover = deprecateComponent( 'URLPopover', RootURLPopover );
+export const Warning = deprecateComponent( 'Warning', RootWarning );
+export const WritingFlow = deprecateComponent( 'WritingFlow', RootWritingFlow );
+
+export const createCustomColorsHOC = deprecateFunction( 'createCustomColorsHOC', rootCreateCustomColorsHOC );
+export const getColorClassName = deprecateFunction( 'getColorClassName', rootGetColorClassName );
+export const getColorObjectByAttributeValues = deprecateFunction( 'getColorObjectByAttributeValues', rootGetColorObjectByAttributeValues );
+export const getColorObjectByColorValue = deprecateFunction( 'getColorObjectByColorValue', rootGetColorObjectByColorValue );
+export const getFontSize = deprecateFunction( 'getFontSize', rootGetFontSize );
+export const getFontSizeClass = deprecateFunction( 'getFontSizeClass', rootGetFontSizeClass );
+export const withColorContext = deprecateFunction( 'withColorContext', rootWithColorContext );
+export const withColors = deprecateFunction( 'withColors', rootWithColors );
+export const withFontSizes = deprecateFunction( 'withFontSizes', rootWithFontSizes );
diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js
index 9d12344c50dc2a..f8999c7868042f 100644
--- a/packages/editor/src/components/index.js
+++ b/packages/editor/src/components/index.js
@@ -1,6 +1,5 @@
// Block Creation Components
export * from './autocompleters';
-export { default as ServerSideRender } from './server-side-render';
// Post Related Components
export { default as AutosaveMonitor } from './autosave-monitor';
diff --git a/packages/editor/src/components/post-featured-image/style.scss b/packages/editor/src/components/post-featured-image/style.scss
index 3258e6191124b2..71af23d863de3a 100644
--- a/packages/editor/src/components/post-featured-image/style.scss
+++ b/packages/editor/src/components/post-featured-image/style.scss
@@ -5,10 +5,10 @@
margin: 0;
}
- // Space consecutive buttons evenly.
+ // Stack consecutive buttons.
.components-button + .components-button {
+ display: block;
margin-top: 1em;
- margin-right: 8px;
}
// This keeps images at their intrinsic size (eg. a 50px
@@ -25,6 +25,7 @@
width: 100%;
padding: 0;
transition: all 0.1s ease-out;
+ @include reduce-motion("transition");
box-shadow: 0 0 0 0 $blue-medium-500;
}
diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js
index 661c66a90a403a..2c9435e1ef6b36 100644
--- a/packages/editor/src/components/post-title/index.native.js
+++ b/packages/editor/src/components/post-title/index.native.js
@@ -89,18 +89,20 @@ class PostTitle extends Component {
{
this.props.onUpdate( value );
} }
placeholder={ decodedPlaceholder }
value={ title }
- onSplit={ this.props.onEnterPress }
+ onSplit={ () => { } }
+ onEnter={ this.props.onEnterPress }
disableEditingMenu={ true }
setRef={ ( ref ) => {
this.titleViewRef = ref;
diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss
index f0dc790b1fdd00..feff1a077f8964 100644
--- a/packages/editor/src/components/post-title/style.scss
+++ b/packages/editor/src/components/post-title/style.scss
@@ -17,6 +17,7 @@
line-height: $default-line-height;
color: $dark-gray-900;
transition: border 0.1s ease-out, box-shadow 0.1s linear;
+ @include reduce-motion("transition");
padding: #{ $block-padding + 5px } $block-padding;
word-break: keep-all;
@@ -94,6 +95,7 @@
&.is-focus-mode .editor-post-title__input {
opacity: 0.5;
transition: opacity 0.1s linear;
+ @include reduce-motion("transition");
&:focus {
opacity: 1;
diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js
index 601a44c0260cfe..0e959f80c7b866 100644
--- a/packages/editor/src/components/provider/index.js
+++ b/packages/editor/src/components/provider/index.js
@@ -19,8 +19,10 @@ import { decodeEntities } from '@wordpress/html-entities';
/**
* Internal dependencies
*/
+import withRegistryProvider from './with-registry-provider';
import { mediaUpload } from '../../utils';
import ReusableBlocksButtons from '../reusable-blocks-buttons';
+import ConvertToGroupButtons from '../convert-to-group-buttons';
const fetchLinkSuggestions = async ( search ) => {
const posts = await apiFetch( {
@@ -159,12 +161,14 @@ class EditorProvider extends Component {
>
{ children }
+
);
}
}
export default compose( [
+ withRegistryProvider,
withSelect( ( select ) => {
const {
__unstableIsEditorReady: isEditorReady,
diff --git a/packages/editor/src/components/provider/with-registry-provider.js b/packages/editor/src/components/provider/with-registry-provider.js
new file mode 100644
index 00000000000000..367782a82b4a42
--- /dev/null
+++ b/packages/editor/src/components/provider/with-registry-provider.js
@@ -0,0 +1,46 @@
+/**
+ * WordPress dependencies
+ */
+import { useState, useEffect } from '@wordpress/element';
+import { withRegistry, createRegistry, RegistryProvider } from '@wordpress/data';
+import { createHigherOrderComponent } from '@wordpress/compose';
+import { storeConfig as blockEditorStoreConfig } from '@wordpress/block-editor';
+
+/**
+ * Internal dependencies
+ */
+import { storeConfig } from '../../store';
+import applyMiddlewares from '../../store/middlewares';
+
+const withRegistryProvider = createHigherOrderComponent(
+ ( WrappedComponent ) => withRegistry( ( props ) => {
+ const { useSubRegistry = true, registry, ...additionalProps } = props;
+ if ( ! useSubRegistry ) {
+ return ;
+ }
+
+ const [ subRegistry, setSubRegistry ] = useState( null );
+ useEffect( () => {
+ const newRegistry = createRegistry( {
+ 'core/block-editor': blockEditorStoreConfig,
+ }, registry );
+ const store = newRegistry.registerStore( 'core/editor', storeConfig );
+ // This should be removed after the refactoring of the effects to controls.
+ applyMiddlewares( store );
+ setSubRegistry( newRegistry );
+ }, [ registry ] );
+
+ if ( ! subRegistry ) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+ } ),
+ 'withRegistryProvider'
+);
+
+export default withRegistryProvider;
diff --git a/packages/editor/src/components/server-side-render/README.md b/packages/editor/src/components/server-side-render/README.md
deleted file mode 100644
index 3ef53b188e644f..00000000000000
--- a/packages/editor/src/components/server-side-render/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# ServerSideRender
-
-This utility component is a wrapper for the generic ServerSideRender in `@wordpress/components`. It adds the `post_id` parameter to the `urlQueryArgs` prop of the wrapped component. Use this component to ensure that the global `$post` object is set up properly in the server-side `render_callback` when rendering within the editor.
diff --git a/packages/editor/src/components/server-side-render/index.js b/packages/editor/src/components/server-side-render/index.js
deleted file mode 100644
index 5830612ebcc36b..00000000000000
--- a/packages/editor/src/components/server-side-render/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/**
- * WordPress dependencies
- */
-import { ServerSideRender } from '@wordpress/components';
-import { select } from '@wordpress/data';
-
-export default function( { urlQueryArgs = {}, ...props } ) {
- const { getCurrentPostId } = select( 'core/editor' );
-
- urlQueryArgs = {
- post_id: getCurrentPostId(),
- ...urlQueryArgs,
- };
-
- return (
-
- );
-}
diff --git a/packages/editor/src/index.js b/packages/editor/src/index.js
index a55a7b1c0bfc1b..eb54a38859b135 100644
--- a/packages/editor/src/index.js
+++ b/packages/editor/src/index.js
@@ -17,6 +17,7 @@ import './hooks';
export * from './components';
export * from './utils';
+export { storeConfig } from './store';
/*
* Backward compatibility
diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js
index 03548de3f93abc..073887663bd1b9 100644
--- a/packages/editor/src/store/actions.js
+++ b/packages/editor/src/store/actions.js
@@ -757,6 +757,9 @@ export function updateEditorSettings( settings ) {
*/
const getBlockEditorAction = ( name ) => function* ( ...args ) {
+ deprecated( '`wp.data.dispatch( \'core/editor\' ).' + name + '`', {
+ alternative: '`wp.data.dispatch( \'core/block-editor\' ).' + name + '`',
+ } );
yield dispatch( 'core/block-editor', name, ...args );
};
diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js
index 1ba136aaab7226..33c5686396097e 100644
--- a/packages/editor/src/store/index.js
+++ b/packages/editor/src/store/index.js
@@ -13,11 +13,22 @@ import * as selectors from './selectors';
import * as actions from './actions';
import { STORE_KEY } from './constants';
-const store = registerStore( STORE_KEY, {
+/**
+ * Post editor data store configuration.
+ *
+ * @see https://github.com/WordPress/gutenberg/blob/master/packages/data/README.md#registerStore
+ *
+ * @type {Object}
+ */
+export const storeConfig = {
reducer,
selectors,
actions,
controls,
+};
+
+const store = registerStore( STORE_KEY, {
+ ...storeConfig,
persist: [ 'preferences' ],
} );
applyMiddlewares( store );
diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js
index 094b47bc716774..7570e8b7848de6 100644
--- a/packages/editor/src/store/selectors.js
+++ b/packages/editor/src/store/selectors.js
@@ -1160,6 +1160,10 @@ export function getEditorSettings( state ) {
function getBlockEditorSelector( name ) {
return createRegistrySelector( ( select ) => ( state, ...args ) => {
+ deprecated( '`wp.data.select( \'core/editor\' ).' + name + '`', {
+ alternative: '`wp.data.select( \'core/block-editor\' ).' + name + '`',
+ } );
+
return select( 'core/block-editor' )[ name ]( ...args );
} );
}
diff --git a/packages/editor/src/utils/media-upload/index.js b/packages/editor/src/utils/media-upload/index.js
index f6572e29c6b0c4..dc79320b703be8 100644
--- a/packages/editor/src/utils/media-upload/index.js
+++ b/packages/editor/src/utils/media-upload/index.js
@@ -7,11 +7,7 @@ import { noop } from 'lodash';
* WordPress dependencies
*/
import { select } from '@wordpress/data';
-
-/**
- * Internal dependencies
- */
-import { mediaUpload } from './media-upload';
+import { uploadMedia } from '@wordpress/media-utils';
/**
* Upload a media file when the file upload button is activated.
@@ -37,7 +33,7 @@ export default function( {
const wpAllowedMimeTypes = getEditorSettings().allowedMimeTypes;
maxUploadFileSize = maxUploadFileSize || getEditorSettings().maxUploadFileSize;
- mediaUpload( {
+ uploadMedia( {
allowedTypes,
filesList,
onFileChange,
diff --git a/packages/element/package.json b/packages/element/package.json
index e67347a38cf9a3..4e4a096b31fe24 100644
--- a/packages/element/package.json
+++ b/packages/element/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/element",
- "version": "2.4.0",
+ "version": "2.5.0",
"description": "Element React module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/element/src/serialize.js b/packages/element/src/serialize.js
index 5c2c2a8011d2a7..b700831f1dccd0 100644
--- a/packages/element/src/serialize.js
+++ b/packages/element/src/serialize.js
@@ -49,10 +49,14 @@ import {
createContext,
Fragment,
StrictMode,
+ forwardRef,
} from './react';
import RawHTML from './raw-html';
const { Provider, Consumer } = createContext();
+const ForwardRef = forwardRef( () => {
+ return null;
+} );
/**
* Valid attribute types.
@@ -406,6 +410,9 @@ export function renderElement( element, context, legacyContext = {} ) {
case Consumer.$$typeof:
return renderElement( props.children( context || type._currentValue ), context, legacyContext );
+
+ case ForwardRef.$$typeof:
+ return renderElement( type.render( props ), context, legacyContext );
}
return '';
diff --git a/packages/element/src/test/serialize.js b/packages/element/src/test/serialize.js
index 7fe4251666decd..b6a8c906751d97 100644
--- a/packages/element/src/test/serialize.js
+++ b/packages/element/src/test/serialize.js
@@ -12,6 +12,7 @@ import {
createElement,
Fragment,
StrictMode,
+ forwardRef,
} from '../react';
import RawHTML from '../raw-html';
import serialize, {
@@ -84,6 +85,20 @@ describe( 'serialize()', () => {
);
} );
+ it( 'should render with forwardRef', () => {
+ const ForwardedComponent = forwardRef( () => {
+ return test
;
+ } );
+
+ const result = serialize(
+
+ );
+
+ expect( result ).toBe(
+ 'test
'
+ );
+ } );
+
describe( 'empty attributes', () => {
it( 'should not render a null attribute value', () => {
const result = serialize( );
diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json
index a6687ebeb34a5a..4d2b81bd73259b 100644
--- a/packages/escape-html/package.json
+++ b/packages/escape-html/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/escape-html",
- "version": "1.3.0",
+ "version": "1.4.0",
"description": "Escape HTML utils.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md
index a88430165222ef..28de7846b5f212 100644
--- a/packages/eslint-plugin/CHANGELOG.md
+++ b/packages/eslint-plugin/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Master
+## 2.3.0 (2019-06-12)
### Bug Fix
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
index 736b49aa1bcccd..9bcd60b15de867 100644
--- a/packages/eslint-plugin/package.json
+++ b/packages/eslint-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/eslint-plugin",
- "version": "2.2.0",
+ "version": "2.3.0",
"description": "ESLint plugin for WordPress development.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -17,6 +17,12 @@
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
+ "files": [
+ "configs",
+ "rules",
+ "index.js"
+ ],
+ "main": "index.js",
"dependencies": {
"babel-eslint": "^10.0.1",
"eslint-plugin-jsx-a11y": "^6.2.1",
diff --git a/packages/format-library/package.json b/packages/format-library/package.json
index 66969a279a6e45..8b84cae37cf00f 100644
--- a/packages/format-library/package.json
+++ b/packages/format-library/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/format-library",
- "version": "1.5.0",
+ "version": "1.6.0",
"description": "Format library for the WordPress editor.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/format-library/src/code/index.js b/packages/format-library/src/code/index.js
index aaccbde3946f49..4eb3cfb1118ef8 100644
--- a/packages/format-library/src/code/index.js
+++ b/packages/format-library/src/code/index.js
@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { toggleFormat } from '@wordpress/rich-text';
-import { RichTextShortcut, RichTextToolbarButton } from '@wordpress/block-editor';
+import { RichTextToolbarButton } from '@wordpress/block-editor';
const name = 'core/code';
const title = __( 'Inline Code' );
@@ -17,21 +17,12 @@ export const code = {
const onToggle = () => onChange( toggleFormat( value, { type: name } ) );
return (
- <>
-
-
- >
+
);
},
};
diff --git a/packages/format-library/src/link/index.js b/packages/format-library/src/link/index.js
index 913285f2e8b0f3..f68d4932f62420 100644
--- a/packages/format-library/src/link/index.js
+++ b/packages/format-library/src/link/index.js
@@ -71,16 +71,6 @@ export const link = {
return (
<>
-
-
{
+ it( 'LinksUI renders', () => {
+ const wrapper = shallow(
+
+ );
+ expect( wrapper ).toBeTruthy();
+ } );
+
+ it( 'Links are removed when no text is in the URL field', () => {
+ // Given
+ const onRemove = jest.fn();
+ const wrapper = shallow(
+
+ ).dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI
+
+ // When
+
+ // Close the BottomSheet
+ const bottomSheet = wrapper.find( 'BottomSheet' ).first();
+ bottomSheet.simulate( 'close' );
+
+ // Then
+
+ expect( onRemove ).toHaveBeenCalledTimes( 1 );
+ } );
+
+ it( 'Links are saved when URL field has content', () => {
+ // Given
+ const onRemove = jest.fn();
+ const wrapper = shallow(
+
+ ).dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI
+
+ // Mock `submitLink` for simplicity (we don't want to test submitLink itself here)
+ wrapper.instance().submitLink = jest.fn();
+
+ // When
+
+ // Simulate user typing on the URL Cell.
+ const bottomSheet = wrapper.find( 'BottomSheet' ).first();
+ const cell = bottomSheet.find( 'BottomSheetCell' ).first();
+ cell.simulate( 'changeValue', 'wordpress.com' );
+
+ // Close the BottomSheet
+ bottomSheet.simulate( 'close' );
+
+ // Then
+ expect( onRemove ).toHaveBeenCalledTimes( 0 );
+ expect( wrapper.instance().submitLink ).toHaveBeenCalledTimes( 1 );
+ } );
+} );
diff --git a/packages/format-library/src/strikethrough/index.js b/packages/format-library/src/strikethrough/index.js
index 244b2787fb4d04..23c17f1fea4a8d 100644
--- a/packages/format-library/src/strikethrough/index.js
+++ b/packages/format-library/src/strikethrough/index.js
@@ -3,7 +3,7 @@
*/
import { __ } from '@wordpress/i18n';
import { toggleFormat } from '@wordpress/rich-text';
-import { RichTextToolbarButton, RichTextShortcut } from '@wordpress/block-editor';
+import { RichTextToolbarButton } from '@wordpress/block-editor';
const name = 'core/strikethrough';
const title = __( 'Strikethrough' );
@@ -17,21 +17,12 @@ export const strikethrough = {
const onToggle = () => onChange( toggleFormat( value, { type: name } ) );
return (
- <>
-
-
- >
+
);
},
};
diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md
index 0b070a6d3c0dbc..cb1625c912898f 100644
--- a/packages/hooks/CHANGELOG.md
+++ b/packages/hooks/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Master
+## 2.4.0 (2019-06-12)
### New Feature
diff --git a/packages/hooks/package.json b/packages/hooks/package.json
index 4f8a280c48dd87..bac2718aceb28a 100644
--- a/packages/hooks/package.json
+++ b/packages/hooks/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/hooks",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "WordPress hooks library.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json
index f395b10b352477..a6f449b3e2aaf0 100644
--- a/packages/html-entities/package.json
+++ b/packages/html-entities/package.json
@@ -1,10 +1,9 @@
{
"name": "@wordpress/html-entities",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "HTML entity utilities for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
- "react-native": "src/index",
"keywords": [
"wordpress",
"html",
@@ -21,6 +20,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
+ "react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.4.4"
},
diff --git a/packages/i18n/package.json b/packages/i18n/package.json
index bdf92ce2757d52..721061946e221b 100644
--- a/packages/i18n/package.json
+++ b/packages/i18n/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/i18n",
- "version": "3.4.0",
+ "version": "3.5.0",
"description": "WordPress internationalization (i18n) library.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json
index af3f44c51a85f3..3850ff5e8f7d4e 100644
--- a/packages/is-shallow-equal/package.json
+++ b/packages/is-shallow-equal/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/is-shallow-equal",
- "version": "1.3.0",
+ "version": "1.4.0",
"description": "Test for shallow equality between two objects or arrays.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -21,6 +21,7 @@
},
"files": [
"arrays.js",
+ "index.js",
"objects.js"
],
"main": "index.js",
diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json
index 9f5eeeaf8117c5..cb5de531080c01 100644
--- a/packages/jest-preset-default/package.json
+++ b/packages/jest-preset-default/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/jest-preset-default",
- "version": "4.1.0",
+ "version": "4.2.0",
"description": "Default Jest preset for WordPress development.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -26,6 +26,7 @@
},
"files": [
"scripts",
+ "index.js",
"jest-preset.json"
],
"main": "index.js",
diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json
index 5e6aa798c0aa85..695528df254808 100644
--- a/packages/keycodes/package.json
+++ b/packages/keycodes/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/keycodes",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/library-export-default-webpack-plugin/CHANGELOG.md b/packages/library-export-default-webpack-plugin/CHANGELOG.md
index 66b9261df4a8bc..dec500a334ca23 100644
--- a/packages/library-export-default-webpack-plugin/CHANGELOG.md
+++ b/packages/library-export-default-webpack-plugin/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Master
+## 1.3.0 (2019-06-12)
### Internal
diff --git a/packages/library-export-default-webpack-plugin/package.json b/packages/library-export-default-webpack-plugin/package.json
index d9fc8d12afc763..60daa21a7c288b 100644
--- a/packages/library-export-default-webpack-plugin/package.json
+++ b/packages/library-export-default-webpack-plugin/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/library-export-default-webpack-plugin",
- "version": "1.2.0",
+ "version": "1.3.0",
"description": "Webpack plugin for exporting default property for selected libraries.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -24,6 +24,7 @@
"files": [
"index.js"
],
+ "main": "index.js",
"dependencies": {
"lodash": "^4.17.11",
"webpack-sources": "^1.1.0"
diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json
index 97efa2fbdf6552..2810b989b7fd01 100644
--- a/packages/list-reusable-blocks/package.json
+++ b/packages/list-reusable-blocks/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/list-reusable-blocks",
- "version": "1.4.0",
+ "version": "1.5.0",
"description": "Adding Export/Import support to the reusable blocks listing.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/media-utils/.npmrc b/packages/media-utils/.npmrc
new file mode 100644
index 00000000000000..43c97e719a5a82
--- /dev/null
+++ b/packages/media-utils/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md
new file mode 100644
index 00000000000000..08666d6e2cf0c1
--- /dev/null
+++ b/packages/media-utils/CHANGELOG.md
@@ -0,0 +1,5 @@
+## 0.1.0 (2019-01-03)
+
+### New Features
+
+- Implemented first version of the package.
diff --git a/packages/media-utils/README.md b/packages/media-utils/README.md
new file mode 100644
index 00000000000000..489214c5446765
--- /dev/null
+++ b/packages/media-utils/README.md
@@ -0,0 +1,45 @@
+# Media Utils
+
+The media utils package provides a set of artifacts to abstract media functionality that may be useful in situations where there is a need to deal with media uploads or with the media library, e.g., artifacts that extend or implement a block-editor.
+This package is meant to be used by the WordPress core. It may not work as expected outside WordPress usages.
+
+## Installation
+
+Install the module
+
+```bash
+npm install @wordpress/media-utils --save
+```
+
+_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._
+
+## Usage
+
+### uploadMedia
+
+Media upload util is a function that allows the invokers to upload files to the WordPress media library.
+As an example, provided that `myFiles` is an array of file objects, `onFileChange` on onFileChange is a function that receives an array of objects containing the description of WordPress media items and `handleFileError` is a function that receives an object describing a possible error, the following code uploads a file to the WordPress media library:
+```js
+wp.mediaUtils.utils.uploadMedia( {
+ filesList: myFiles,
+ onFileChange: handleFileChange,
+ onError: handleFileError
+} );
+```
+
+The following code uploads a file named foo.txt with foo as content to the media library and alerts its URL:
+```js
+wp.mediaUtils.utils.uploadMedia( {
+ filesList: [ new File( ["foo"], "foo.txt", { type: "text/plain"} ) ],
+ onFileChange: ( [ fileObj] ) => alert( fileObj.url ),
+ onError: console.error,
+} );
+```
+
+Beware that first onFileChange is called with temporary blob URLs and then with the final URL's this allows to show the result in an optimistic UI as if the upload was already completed. E.g.: when uploading an image, one can show the image right away in the UI even before the upload is complete.
+
+
+### MediaUpload
+
+Media upload component provides a UI button that allows users to open the WordPress media library. It is normally used in conjunction with the filter `editor.MediaUpload`.
+The component follows the interface specified in https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/media-upload/README.md, and more details regarding its usage can be checked there.
diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json
new file mode 100644
index 00000000000000..8f6ff15ae9420b
--- /dev/null
+++ b/packages/media-utils/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@wordpress/media-utils",
+ "version": "0.2.0",
+ "description": "WordPress Media Upload Utils.",
+ "author": "The WordPress Contributors",
+ "license": "GPL-2.0-or-later",
+ "keywords": [
+ "wordpress",
+ "media",
+ "upload",
+ "media-upload"
+ ],
+ "homepage": "https://github.com/WordPress/gutenberg/master/packages/media-utils/README.md",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/WordPress/gutenberg.git",
+ "directory": "packages/url"
+ },
+ "bugs": {
+ "url": "https://github.com/WordPress/gutenberg/issues"
+ },
+ "main": "build/index.js",
+ "module": "build-module/index.js",
+ "dependencies": {
+ "@babel/runtime": "^7.4.4",
+ "@wordpress/api-fetch": "file:../api-fetch",
+ "@wordpress/blob": "file:../blob",
+ "@wordpress/element": "file:../element",
+ "@wordpress/i18n": "file:../i18n",
+ "lodash": "^4.17.11"
+ },
+ "publishConfig": {
+ "access": "public"
+ }
+}
diff --git a/packages/media-utils/src/components/index.js b/packages/media-utils/src/components/index.js
new file mode 100644
index 00000000000000..82a89c0d58014b
--- /dev/null
+++ b/packages/media-utils/src/components/index.js
@@ -0,0 +1 @@
+export { default as MediaUpload } from './media-upload';
diff --git a/packages/edit-post/src/hooks/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js
similarity index 100%
rename from packages/edit-post/src/hooks/components/media-upload/index.js
rename to packages/media-utils/src/components/media-upload/index.js
diff --git a/packages/media-utils/src/index.js b/packages/media-utils/src/index.js
new file mode 100644
index 00000000000000..590a7f4c9d188d
--- /dev/null
+++ b/packages/media-utils/src/index.js
@@ -0,0 +1,2 @@
+export * from './components';
+export * from './utils';
diff --git a/packages/media-utils/src/utils/index.js b/packages/media-utils/src/utils/index.js
new file mode 100644
index 00000000000000..509b62f1e88648
--- /dev/null
+++ b/packages/media-utils/src/utils/index.js
@@ -0,0 +1 @@
+export { uploadMedia } from './upload-media';
diff --git a/packages/editor/src/utils/media-upload/test/media-upload.js b/packages/media-utils/src/utils/test/upload-media.test.js
similarity index 95%
rename from packages/editor/src/utils/media-upload/test/media-upload.js
rename to packages/media-utils/src/utils/test/upload-media.test.js
index d9e18cf7bf280b..857c139625c027 100644
--- a/packages/editor/src/utils/media-upload/test/media-upload.js
+++ b/packages/media-utils/src/utils/test/upload-media.test.js
@@ -7,7 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
/**
* Internal dependencies
*/
-import { mediaUpload, getMimeTypesArray } from '../media-upload';
+import { uploadMedia, getMimeTypesArray } from '../upload-media';
jest.mock( '@wordpress/blob', () => ( {
createBlobURL: jest.fn(),
@@ -18,11 +18,11 @@ jest.mock( '@wordpress/api-fetch', () => jest.fn() );
const xmlFile = new window.File( [ 'fake_file' ], 'test.xml', { type: 'text/xml' } );
const imageFile = new window.File( [ 'fake_file' ], 'test.jpeg', { type: 'image/jpeg' } );
-describe( 'mediaUpload', () => {
+describe( 'uploadMedia', () => {
it( 'should do nothing on no files', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
filesList: [],
onError,
onFileChange,
@@ -35,7 +35,7 @@ describe( 'mediaUpload', () => {
it( 'should error if allowedTypes contains a partial mime type and the validation fails', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image' ],
filesList: [ xmlFile ],
onError,
@@ -51,7 +51,7 @@ describe( 'mediaUpload', () => {
it( 'should error if allowedTypes contains a complete mime type and the validation fails', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image/gif' ],
filesList: [ imageFile ],
onError,
@@ -70,7 +70,7 @@ describe( 'mediaUpload', () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image/jpeg' ],
filesList: [ imageFile ],
onError,
@@ -84,7 +84,7 @@ describe( 'mediaUpload', () => {
it( 'should error if allowedTypes contains multiple types and the validation fails', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'video', 'image' ],
filesList: [ xmlFile ],
onError,
@@ -103,7 +103,7 @@ describe( 'mediaUpload', () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'video', 'image' ],
filesList: [ imageFile ],
onError,
@@ -120,7 +120,7 @@ describe( 'mediaUpload', () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image' ],
filesList: [ imageFile, xmlFile ],
onError,
@@ -137,7 +137,7 @@ describe( 'mediaUpload', () => {
it( 'should error if the file size is greater than the maximum', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image' ],
filesList: [ imageFile ],
maxUploadFileSize: 1,
@@ -154,7 +154,7 @@ describe( 'mediaUpload', () => {
it( 'should call error handler with the correct error object if file type is not allowed for user', async () => {
const onError = jest.fn();
const onFileChange = jest.fn();
- await mediaUpload( {
+ await uploadMedia( {
allowedTypes: [ 'image' ],
filesList: [ imageFile ],
onError,
diff --git a/packages/editor/src/utils/media-upload/media-upload.js b/packages/media-utils/src/utils/upload-media.js
similarity index 99%
rename from packages/editor/src/utils/media-upload/media-upload.js
rename to packages/media-utils/src/utils/upload-media.js
index 3aed464f5c4c6e..553584d010ace3 100644
--- a/packages/editor/src/utils/media-upload/media-upload.js
+++ b/packages/media-utils/src/utils/upload-media.js
@@ -61,7 +61,7 @@ export function getMimeTypesArray( wpMimeTypesObject ) {
* @param {Function} $0.onFileChange Function called each time a file or a temporary representation of the file is available.
* @param {?Object} $0.wpAllowedMimeTypes List of allowed mime types and file extensions.
*/
-export async function mediaUpload( {
+export async function uploadMedia( {
allowedTypes,
additionalData = {},
filesList,
diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md
index 7a6fde4663c907..16695a45dd0122 100644
--- a/packages/notices/CHANGELOG.md
+++ b/packages/notices/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Master
+## 1.5.0 (2019-06-12)
### New Features
diff --git a/packages/notices/package.json b/packages/notices/package.json
index 237966556163a0..250ffed2398b0c 100644
--- a/packages/notices/package.json
+++ b/packages/notices/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/notices",
- "version": "1.4.0",
+ "version": "1.5.0",
"description": "State management for notices.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/npm-package-json-lint-config/CHANGELOG.md b/packages/npm-package-json-lint-config/CHANGELOG.md
index 11259b591308d6..945e31866bbe8e 100644
--- a/packages/npm-package-json-lint-config/CHANGELOG.md
+++ b/packages/npm-package-json-lint-config/CHANGELOG.md
@@ -1,3 +1,9 @@
+## 2.0.0 (2019-06-12)
+
+### Braking Change
+
+- Added `type` and `react-native` to the order of preferred properties.
+
## 1.2.0 (2019-03-06)
### Internal
diff --git a/packages/npm-package-json-lint-config/index.js b/packages/npm-package-json-lint-config/index.js
index 5af34472e68b1e..d6a93751371b16 100644
--- a/packages/npm-package-json-lint-config/index.js
+++ b/packages/npm-package-json-lint-config/index.js
@@ -53,8 +53,10 @@ const defaultConfig = {
'engines',
'directories',
'files',
+ 'type',
'main',
'module',
+ 'react-native',
'bin',
'dependencies',
'devDependencies',
diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json
index 885a8a5b66bba8..c05441fbf88adc 100644
--- a/packages/npm-package-json-lint-config/package.json
+++ b/packages/npm-package-json-lint-config/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/npm-package-json-lint-config",
- "version": "1.3.0",
+ "version": "2.0.0",
"description": "WordPress npm-package-json-lint shareable configuration.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -21,6 +21,9 @@
"engines": {
"node": ">=8"
},
+ "files": [
+ "index.js"
+ ],
"main": "index.js",
"peerDependencies": {
"npm-package-json-lint": ">=3.6.0"
diff --git a/packages/nux/package.json b/packages/nux/package.json
index 4102efd88c26b3..9768b542bc534c 100644
--- a/packages/nux/package.json
+++ b/packages/nux/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/nux",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "NUX (New User eXperience) module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/plugins/package.json b/packages/plugins/package.json
index 5e93869577524f..86a1f40b8d5658 100644
--- a/packages/plugins/package.json
+++ b/packages/plugins/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/plugins",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "Plugins module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -19,6 +19,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
+ "react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.4.4",
"@wordpress/compose": "file:../compose",
diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json
index a8cdace21091e8..3f6fea2c863b62 100644
--- a/packages/redux-routine/package.json
+++ b/packages/redux-routine/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/redux-routine",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "Redux middleware for generator coroutines.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json
index 304d7fdb78b6de..2422f20a872611 100644
--- a/packages/rich-text/package.json
+++ b/packages/rich-text/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/rich-text",
- "version": "3.3.0",
+ "version": "3.4.0",
"description": "Rich text value and manipulation API.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/rich-text/src/index.js b/packages/rich-text/src/index.js
index 6dbfe46b6d93b4..8f57a21e4ddf34 100644
--- a/packages/rich-text/src/index.js
+++ b/packages/rich-text/src/index.js
@@ -20,6 +20,7 @@ export { remove } from './remove';
export { replace } from './replace';
export { insert } from './insert';
export { insertLineSeparator as __unstableInsertLineSeparator } from './insert-line-separator';
+export { removeLineSeparator as __unstableRemoveLineSeparator } from './remove-line-separator';
export { insertObject } from './insert-object';
export { slice } from './slice';
export { split } from './split';
diff --git a/packages/rich-text/src/remove-line-separator.js b/packages/rich-text/src/remove-line-separator.js
new file mode 100644
index 00000000000000..f9e6dfb157952e
--- /dev/null
+++ b/packages/rich-text/src/remove-line-separator.js
@@ -0,0 +1,56 @@
+/**
+ * Internal dependencies
+ */
+
+import { LINE_SEPARATOR } from './special-characters';
+import { isCollapsed } from './is-collapsed';
+import { remove } from './remove';
+
+/**
+ * Removes a line separator character, if existing, from a Rich Text value at the current
+ * indices. If no line separator exists on the indices it will return undefined.
+ *
+ * @param {Object} value Value to modify.
+ * @param {boolean} backward indicates if are removing from the start index or the end index.
+ *
+ * @return {Object|undefined} A new value with the line separator removed. Or undefined if no line separator is found on the position.
+ */
+export function removeLineSeparator(
+ value,
+ backward = true,
+) {
+ const { replacements, text, start, end } = value;
+ const collapsed = isCollapsed( value );
+ let index = start - 1;
+ let removeStart = collapsed ? start - 1 : start;
+ let removeEnd = end;
+ if ( ! backward ) {
+ index = end;
+ removeStart = start;
+ removeEnd = collapsed ? end + 1 : end;
+ }
+
+ if ( text[ index ] !== LINE_SEPARATOR ) {
+ return;
+ }
+
+ let newValue;
+ // 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,
+ removeStart,
+ removeEnd
+ );
+ }
+ return newValue;
+}
diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md
index 30aed0c902b09b..5fb6c959d8c14c 100644
--- a/packages/scripts/CHANGELOG.md
+++ b/packages/scripts/CHANGELOG.md
@@ -1,3 +1,18 @@
+## Master
+
+### New Features
+
+- The `build` and `start` commands supports simplified syntax for multiple entry points: `wp-scripts build entry-one.js entry-two.js` ([15982](https://github.com/WordPress/gutenberg/pull/15982)).
+
+## 3.3.0 (2019-06-12)
+
+### New Features
+
+- The `lint-js` command lints now JS files in the entire project's directories by default ([15890](https://github.com/WordPress/gutenberg/pull/15890)).
+- The `lint-pkg-json` command lints now `package.json` files in the entire project's directories by default ([15890](https://github.com/WordPress/gutenberg/pull/15890)).
+- The `lint-style` command lints now CSS and SCSS files in the entire project's directories by default ([15890](https://github.com/WordPress/gutenberg/pull/15890)).
+- The `lint-js`, `lint-pkg-json` and `lint-style` commands ignore now files located in `build` and `node_modules` folders by default ([15977](https://github.com/WordPress/gutenberg/pull/15977)).
+
## 3.2.0 (2019-05-21)
### New Feature
diff --git a/packages/scripts/README.md b/packages/scripts/README.md
index b21c7078a61d56..531def5b59d22e 100644
--- a/packages/scripts/README.md
+++ b/packages/scripts/README.md
@@ -25,10 +25,10 @@ _Example:_
"scripts": {
"build": "wp-scripts build",
"check-engines": "wp-scripts check-engines",
- "check-licenses": "wp-scripts check-licenses --production",
- "lint:css": "wp-scripts lint-style '**/*.css'",
- "lint:js": "wp-scripts lint-js .",
- "lint:pkg-json": "wp-scripts lint-pkg-json .",
+ "check-licenses": "wp-scripts check-licenses",
+ "lint:css": "wp-scripts lint-style",
+ "lint:js": "wp-scripts lint-js",
+ "lint:pkg-json": "wp-scripts lint-pkg-json",
"start": "wp-scripts start",
"test:e2e": "wp-scripts test-e2e",
"test:unit": "wp-scripts test-unit-js"
@@ -49,7 +49,8 @@ _Example:_
```json
{
"scripts": {
- "build": "wp-scripts build"
+ "build": "wp-scripts build",
+ "build:custom": "wp-scripts build entry-one.js entry-two.js --output-path=custom"
}
}
```
@@ -57,6 +58,7 @@ _Example:_
This is how you execute the script with presented setup:
* `npm run build` - builds the code for production.
+* `npm run build:custom` - builds the code for production with two entry points and a custom output folder. Paths for custom entry points are relative to the project root.
#### Advanced information
@@ -101,7 +103,7 @@ _Example:_
_Flags_:
- `--prod` (or `--production`): When present, validates only `dependencies` and not `devDependencies`
-- `--dev` (or `--development`): When present, validates both `dependencies` and `devDependencies`
+- `--dev` (or `--development`): When present, validates only `devDependencies` and not `dependencies`
- `--gpl2`: Validates against [GPLv2 license compatibility](https://www.gnu.org/licenses/license-list.en.html)
- `--ignore=a,b,c`: A comma-separated set of package names to ignore for validation. This is intended to be used primarily in cases where a dependency's `license` field is malformed. It's assumed that any `ignored` package argument would be manually vetted for compatibility by the project owner.
@@ -114,7 +116,8 @@ _Example:_
```json
{
"scripts": {
- "lint:js": "wp-scripts lint-js ."
+ "lint:js": "wp-scripts lint-js",
+ "lint:js:src": "wp-scripts lint-js ./src"
}
}
```
@@ -122,6 +125,11 @@ _Example:_
This is how you execute the script with presented setup:
* `npm run lint:js` - lints JavaScript files in the entire project's directories.
+* `npm run lint:js:src` - lints JavaScript files in the project's `src` subfolder's directories.
+
+When you run commands similar to the `npm run lint:js:src` example above, you can provide a file, a directory, or `glob` syntax or any combination of them. See [more examples](https://eslint.org/docs/user-guide/command-line-interface).
+
+By default, files located in `build` and `node_modules` folders are ignored.
#### Advanced information
@@ -136,14 +144,20 @@ _Example:_
```json
{
"scripts": {
- "lint:pkg-json": "wp-scripts lint-pkg-json ."
+ "lint:pkg-json": "wp-scripts lint-pkg-json",
+ "lint:pkg-json:src": "wp-scripts lint-pkg-json ./src"
}
}
```
This is how you execute those scripts using the presented setup:
-* `npm run lint:pkg-json` - lints `package.json` file in the project's root folder.
+* `npm run lint:pkg-json` - lints `package.json` file in the entire project's directories.
+* `npm run lint:pkg-json:src` - lints `package.json` file in the project's `src` subfolder's directories.
+
+When you run commands similar to the `npm run lint:pkg-json:src` example above, you can provide one or multiple directories to scan as well. See [more examples](https://github.com/tclindner/npm-package-json-lint/blob/HEAD/README.md#examples).
+
+By default, files located in `build` and `node_modules` folders are ignored.
#### Advanced information
@@ -158,14 +172,20 @@ _Example:_
```json
{
"scripts": {
- "lint:css": "wp-scripts lint-style '**/*.css'"
+ "lint:style": "wp-scripts lint-style",
+ "lint:css:src": "wp-scripts lint-style 'src/**/*.css'"
}
}
```
This is how you execute the script with presented setup:
-* `npm run lint:css` - lints CSS files in the whole project's directory.
+* `npm run lint:style` - lints CSS and SCSS files in the entire project's directories.
+* `npm run lint:css:src` - lints only CSS files in the project's `src` subfolder's directories.
+
+When you run commands similar to the `npm run lint:css:src` example above, be sure to include the quotation marks around file globs. This ensures that you can use the powers of [globby](https://github.com/sindresorhus/globby) (like the `**` globstar) regardless of your shell. See [more examples](https://github.com/stylelint/stylelint/blob/HEAD/docs/user-guide/cli.md#examples).
+
+By default, files located in `build` and `node_modules` folders are ignored.
#### Advanced information
@@ -180,7 +200,8 @@ _Example:_
```json
{
"scripts": {
- "start": "wp-scripts start"
+ "start": "wp-scripts start",
+ "start:custom": "wp-scripts start entry-one.js entry-two.js --output-path=custom"
}
}
```
@@ -188,6 +209,7 @@ _Example:_
This is how you execute the script with presented setup:
* `npm start` - starts the build for development.
+* `npm run start:custom` - starts the build for development which contains two entry points and a custom output folder. Paths for custom entry points are relative to the project root.
#### Advanced information
diff --git a/packages/scripts/bin/wp-scripts.js b/packages/scripts/bin/wp-scripts.js
index 05e9154da4a9b9..a74076ead7f85f 100755
--- a/packages/scripts/bin/wp-scripts.js
+++ b/packages/scripts/bin/wp-scripts.js
@@ -3,8 +3,8 @@
/**
* Internal dependencies
*/
-const { getCliArgs, spawnScript } = require( '../utils' );
+const { getArgsFromCLI, spawnScript } = require( '../utils' );
-const [ scriptName, ...nodesArgs ] = getCliArgs();
+const [ scriptName, ...nodesArgs ] = getArgsFromCLI();
spawnScript( scriptName, nodesArgs );
diff --git a/packages/scripts/config/.eslintignore b/packages/scripts/config/.eslintignore
new file mode 100644
index 00000000000000..e3fbd98336eafe
--- /dev/null
+++ b/packages/scripts/config/.eslintignore
@@ -0,0 +1,2 @@
+build
+node_modules
diff --git a/packages/scripts/config/.npmpackagejsonlintignore b/packages/scripts/config/.npmpackagejsonlintignore
new file mode 100644
index 00000000000000..ae6cdbfb623bb1
--- /dev/null
+++ b/packages/scripts/config/.npmpackagejsonlintignore
@@ -0,0 +1,3 @@
+# By default, all `node_modules` are ignored.
+
+build
diff --git a/packages/scripts/config/.stylelintignore b/packages/scripts/config/.stylelintignore
new file mode 100644
index 00000000000000..ae6cdbfb623bb1
--- /dev/null
+++ b/packages/scripts/config/.stylelintignore
@@ -0,0 +1,3 @@
+# By default, all `node_modules` are ignored.
+
+build
diff --git a/packages/scripts/package.json b/packages/scripts/package.json
index 712b5edde2b3f5..681b40742999c5 100644
--- a/packages/scripts/package.json
+++ b/packages/scripts/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/scripts",
- "version": "3.2.1",
+ "version": "3.3.0",
"description": "Collection of reusable scripts for WordPress development.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -44,6 +44,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",
diff --git a/packages/scripts/scripts/check-engines.js b/packages/scripts/scripts/check-engines.js
index 5440e27ccaffba..aba446313e5a93 100644
--- a/packages/scripts/scripts/check-engines.js
+++ b/packages/scripts/scripts/check-engines.js
@@ -8,16 +8,16 @@ const { sync: resolveBin } = require( 'resolve-bin' );
* Internal dependencies
*/
const {
- getCliArgs,
- hasCliArg,
+ getArgsFromCLI,
+ hasArgInCLI,
} = require( '../utils' );
-const args = getCliArgs();
+const args = getArgsFromCLI();
-const hasConfig = hasCliArg( '--package' ) ||
- hasCliArg( '--node' ) ||
- hasCliArg( '--npm' ) ||
- hasCliArg( '--yarn' );
+const hasConfig = hasArgInCLI( '--package' ) ||
+ hasArgInCLI( '--node' ) ||
+ hasArgInCLI( '--npm' ) ||
+ hasArgInCLI( '--yarn' );
const config = ! hasConfig ?
[
'--node', '>=10.0.0',
diff --git a/packages/scripts/scripts/check-licenses.js b/packages/scripts/scripts/check-licenses.js
index 7670ae3b0d2140..1b1d46b9befbce 100644
--- a/packages/scripts/scripts/check-licenses.js
+++ b/packages/scripts/scripts/check-licenses.js
@@ -9,7 +9,7 @@ const chalk = require( 'chalk' );
/**
* Internal dependencies
*/
-const { getCliArg, hasCliArg } = require( '../utils' );
+const { getArgFromCLI, hasArgInCLI } = require( '../utils' );
/*
* WARNING: Changes to this file may inadvertently cause us to distribute code that
@@ -22,11 +22,11 @@ const { getCliArg, hasCliArg } = require( '../utils' );
const ERROR = chalk.reset.inverse.bold.red( ' ERROR ' );
-const prod = hasCliArg( '--prod' ) || hasCliArg( '--production' );
-const dev = hasCliArg( '--dev' ) || hasCliArg( '--development' );
-const gpl2 = hasCliArg( '--gpl2' );
-const ignored = hasCliArg( '--ignore' ) ?
- getCliArg( '--ignore' )
+const prod = hasArgInCLI( '--prod' ) || hasArgInCLI( '--production' );
+const dev = hasArgInCLI( '--dev' ) || hasArgInCLI( '--development' );
+const gpl2 = hasArgInCLI( '--gpl2' );
+const ignored = hasArgInCLI( '--ignore' ) ?
+ getArgFromCLI( '--ignore' )
// "--ignore=a, b" -> "[ 'a', ' b' ]"
.split( ',' )
// "[ 'a', ' b' ]" -> "[ 'a', 'b' ]"
diff --git a/packages/scripts/scripts/lint-js.js b/packages/scripts/scripts/lint-js.js
index c7346fb76ad88c..dc55a358c3e158 100644
--- a/packages/scripts/scripts/lint-js.js
+++ b/packages/scripts/scripts/lint-js.js
@@ -9,16 +9,20 @@ const { sync: resolveBin } = require( 'resolve-bin' );
*/
const {
fromConfigRoot,
- getCliArgs,
- hasCliArg,
+ getArgsFromCLI,
+ hasArgInCLI,
+ hasFileArgInCLI,
hasPackageProp,
hasProjectFile,
} = require( '../utils' );
-const args = getCliArgs();
+const args = getArgsFromCLI();
-const hasLintConfig = hasCliArg( '-c' ) ||
- hasCliArg( '--config' ) ||
+const defaultFilesArgs = hasFileArgInCLI() ? [] : [ '.' ];
+
+// See: https://eslint.org/docs/user-guide/configuring#using-configuration-files-1.
+const hasLintConfig = hasArgInCLI( '-c' ) ||
+ hasArgInCLI( '--config' ) ||
hasProjectFile( '.eslintrc.js' ) ||
hasProjectFile( '.eslintrc.yaml' ) ||
hasProjectFile( '.eslintrc.yml' ) ||
@@ -29,13 +33,21 @@ const hasLintConfig = hasCliArg( '-c' ) ||
// When a configuration is not provided by the project, use from the default
// provided with the scripts module. Instruct ESLint to avoid discovering via
// the `--no-eslintrc` flag, as otherwise it will still merge with inherited.
-const config = ! hasLintConfig ?
+const defaultConfigArgs = ! hasLintConfig ?
[ '--no-eslintrc', '--config', fromConfigRoot( '.eslintrc.js' ) ] :
[];
+// See: https://eslint.org/docs/user-guide/configuring#ignoring-files-and-directories.
+const hasIgnoredFiles = hasArgInCLI( '--ignore-path' ) ||
+ hasProjectFile( '.eslintignore' );
+
+const defaultIgnoreArgs = ! hasIgnoredFiles ?
+ [ '--ignore-path', fromConfigRoot( '.eslintignore' ) ] :
+ [];
+
const result = spawn(
resolveBin( 'eslint' ),
- [ ...config, ...args ],
+ [ ...defaultConfigArgs, ...defaultIgnoreArgs, ...args, ...defaultFilesArgs ],
{ stdio: 'inherit' }
);
diff --git a/packages/scripts/scripts/lint-pkg-json.js b/packages/scripts/scripts/lint-pkg-json.js
index b37e0a07bc75bf..487a8191067dbc 100644
--- a/packages/scripts/scripts/lint-pkg-json.js
+++ b/packages/scripts/scripts/lint-pkg-json.js
@@ -9,27 +9,39 @@ const { sync: resolveBin } = require( 'resolve-bin' );
*/
const {
fromConfigRoot,
- getCliArgs,
- hasCliArg,
+ getArgsFromCLI,
+ hasArgInCLI,
+ hasFileArgInCLI,
hasProjectFile,
hasPackageProp,
} = require( '../utils' );
-const args = getCliArgs();
+const args = getArgsFromCLI();
-const hasLintConfig = hasCliArg( '-c' ) ||
- hasCliArg( '--configFile' ) ||
+const defaultFilesArgs = hasFileArgInCLI() ? [] : [ '.' ];
+
+// See: https://github.com/tclindner/npm-package-json-lint/wiki/configuration#configuration.
+const hasLintConfig = hasArgInCLI( '-c' ) ||
+ hasArgInCLI( '--configFile' ) ||
hasProjectFile( '.npmpackagejsonlintrc.json' ) ||
hasProjectFile( 'npmpackagejsonlint.config.js' ) ||
hasPackageProp( 'npmPackageJsonLintConfig' );
-const config = ! hasLintConfig ?
+const defaultConfigArgs = ! hasLintConfig ?
[ '--configFile', fromConfigRoot( 'npmpackagejsonlint.json' ) ] :
[];
+// See: https://github.com/tclindner/npm-package-json-lint/#cli-commands-and-configuration.
+const hasIgnoredFiles = hasArgInCLI( '--ignorePath' ) ||
+ hasProjectFile( '.npmpackagejsonlintignore' );
+
+const defaultIgnoreArgs = ! hasIgnoredFiles ?
+ [ '--ignorePath', fromConfigRoot( '.npmpackagejsonlintignore' ) ] :
+ [];
+
const result = spawn(
resolveBin( 'npm-package-json-lint', { executable: 'npmPkgJsonLint' } ),
- [ ...config, ...args ],
+ [ ...defaultConfigArgs, ...defaultIgnoreArgs, ...args, ...defaultFilesArgs ],
{ stdio: 'inherit' }
);
diff --git a/packages/scripts/scripts/lint-style.js b/packages/scripts/scripts/lint-style.js
index d72a877fa93702..3f7e86fc43277f 100644
--- a/packages/scripts/scripts/lint-style.js
+++ b/packages/scripts/scripts/lint-style.js
@@ -9,15 +9,19 @@ const { sync: resolveBin } = require( 'resolve-bin' );
*/
const {
fromConfigRoot,
- getCliArgs,
- hasCliArg,
+ getArgsFromCLI,
+ hasArgInCLI,
+ hasFileArgInCLI,
hasProjectFile,
hasPackageProp,
} = require( '../utils' );
-const args = getCliArgs();
+const args = getArgsFromCLI();
-const hasStylelintConfig = hasCliArg( '--config' ) ||
+const defaultFilesArgs = hasFileArgInCLI() ? [] : [ '**/*.{css,scss}' ];
+
+// See: https://github.com/stylelint/stylelint/blob/master/docs/user-guide/configuration.md#loading-the-configuration-object.
+const hasLintConfig = hasArgInCLI( '--config' ) ||
hasProjectFile( '.stylelintrc' ) ||
hasProjectFile( '.stylelintrc.js' ) ||
hasProjectFile( '.stylelintrc.json' ) ||
@@ -26,13 +30,21 @@ const hasStylelintConfig = hasCliArg( '--config' ) ||
hasProjectFile( '.stylelint.config.js' ) ||
hasPackageProp( 'stylelint' );
-const config = ! hasStylelintConfig ?
+const defaultConfigArgs = ! hasLintConfig ?
[ '--config', fromConfigRoot( '.stylelintrc.json' ) ] :
[];
+// See: https://github.com/stylelint/stylelint/blob/master/docs/user-guide/configuration.md#stylelintignore.
+const hasIgnoredFiles = hasArgInCLI( '--ignore-path' ) ||
+ hasProjectFile( '.stylelintignore' );
+
+const defaultIgnoreArgs = ! hasIgnoredFiles ?
+ [ '--ignore-path', fromConfigRoot( '.stylelintignore' ) ] :
+ [];
+
const result = spawn(
resolveBin( 'stylelint' ),
- [ ...config, ...args ],
+ [ ...defaultConfigArgs, ...defaultIgnoreArgs, ...args, ...defaultFilesArgs ],
{ stdio: 'inherit' }
);
diff --git a/packages/scripts/scripts/test-e2e.js b/packages/scripts/scripts/test-e2e.js
index 374ebc59f6fbe5..efa0c834df2394 100644
--- a/packages/scripts/scripts/test-e2e.js
+++ b/packages/scripts/scripts/test-e2e.js
@@ -19,9 +19,9 @@ const jest = require( 'jest' );
*/
const {
fromConfigRoot,
- getCliArg,
- getCliArgs,
- hasCliArg,
+ getArgFromCLI,
+ getArgsFromCLI,
+ hasArgInCLI,
hasProjectFile,
hasJestConfig,
} = require( '../utils' );
@@ -37,15 +37,15 @@ const config = ! hasJestConfig() ?
[ '--config', JSON.stringify( require( fromConfigRoot( 'jest-e2e.config.js' ) ) ) ] :
[];
-const hasRunInBand = hasCliArg( '--runInBand' ) ||
- hasCliArg( '-i' );
+const hasRunInBand = hasArgInCLI( '--runInBand' ) ||
+ hasArgInCLI( '-i' );
const runInBand = ! hasRunInBand ?
[ '--runInBand' ] :
[];
-if ( hasCliArg( '--puppeteer-interactive' ) ) {
+if ( hasArgInCLI( '--puppeteer-interactive' ) ) {
process.env.PUPPETEER_HEADLESS = 'false';
- process.env.PUPPETEER_SLOWMO = getCliArg( '--puppeteer-slowmo' ) || 80;
+ process.env.PUPPETEER_SLOWMO = getArgFromCLI( '--puppeteer-slowmo' ) || 80;
}
const configsMapping = {
@@ -55,11 +55,11 @@ const configsMapping = {
};
Object.entries( configsMapping ).forEach( ( [ envKey, argName ] ) => {
- if ( hasCliArg( argName ) ) {
- process.env[ envKey ] = getCliArg( argName );
+ if ( hasArgInCLI( argName ) ) {
+ process.env[ envKey ] = getArgFromCLI( argName );
}
} );
const cleanUpPrefixes = [ '--puppeteer-', '--wordpress-' ];
-jest.run( [ ...config, ...runInBand, ...getCliArgs( cleanUpPrefixes ) ] );
+jest.run( [ ...config, ...runInBand, ...getArgsFromCLI( cleanUpPrefixes ) ] );
diff --git a/packages/scripts/scripts/test-unit-jest.js b/packages/scripts/scripts/test-unit-jest.js
index 1fb5743ac861c0..e7edf3c69ccdb4 100644
--- a/packages/scripts/scripts/test-unit-jest.js
+++ b/packages/scripts/scripts/test-unit-jest.js
@@ -19,7 +19,7 @@ const jest = require( 'jest' );
*/
const {
fromConfigRoot,
- getCliArgs,
+ getArgsFromCLI,
hasJestConfig,
} = require( '../utils' );
@@ -27,4 +27,4 @@ const config = ! hasJestConfig() ?
[ '--config', JSON.stringify( require( fromConfigRoot( 'jest-unit.config.js' ) ) ) ] :
[];
-jest.run( [ ...config, ...getCliArgs() ] );
+jest.run( [ ...config, ...getArgsFromCLI() ] );
diff --git a/packages/scripts/utils/cli.js b/packages/scripts/utils/cli.js
index 3778379ced1920..9b3bbd0dd1ca3d 100644
--- a/packages/scripts/utils/cli.js
+++ b/packages/scripts/utils/cli.js
@@ -1,6 +1,7 @@
/**
* External dependencies
*/
+const minimist = require( 'minimist' );
const spawn = require( 'cross-spawn' );
/**
@@ -12,11 +13,11 @@ const {
} = require( './file' );
const {
exit,
- getCliArgs,
+ getArgsFromCLI,
} = require( './process' );
-const getCliArg = ( arg ) => {
- for ( const cliArg of getCliArgs() ) {
+const getArgFromCLI = ( arg ) => {
+ for ( const cliArg of getArgsFromCLI() ) {
const [ name, value ] = cliArg.split( '=' );
if ( name === arg ) {
return value || null;
@@ -24,7 +25,11 @@ const getCliArg = ( arg ) => {
}
};
-const hasCliArg = ( arg ) => getCliArg( arg ) !== undefined;
+const hasArgInCLI = ( arg ) => getArgFromCLI( arg ) !== undefined;
+
+const getFileArgsFromCLI = () => minimist( getArgsFromCLI() )._;
+
+const hasFileArgInCLI = () => getFileArgsFromCLI().length > 0;
const handleSignal = ( signal ) => {
if ( signal === 'SIGKILL' ) {
@@ -78,8 +83,10 @@ const spawnScript = ( scriptName, args = [] ) => {
};
module.exports = {
- getCliArg,
- getCliArgs,
- hasCliArg,
+ getArgFromCLI,
+ getArgsFromCLI,
+ getFileArgsFromCLI,
+ hasArgInCLI,
+ hasFileArgInCLI,
spawnScript,
};
diff --git a/packages/scripts/utils/config.js b/packages/scripts/utils/config.js
index 3ae98725e5fbc7..a34895230af704 100644
--- a/packages/scripts/utils/config.js
+++ b/packages/scripts/utils/config.js
@@ -1,7 +1,12 @@
+/**
+ * External dependencies
+ */
+const { basename } = require( 'path' );
+
/**
* Internal dependencies
*/
-const { hasCliArg, getCliArgs } = require( './cli' );
+const { getArgsFromCLI, getFileArgsFromCLI, hasArgInCLI, hasFileArgInCLI } = require( './cli' );
const { fromConfigRoot, hasProjectFile } = require( './file' );
const { hasPackageProp } = require( './package' );
@@ -12,22 +17,67 @@ const hasBabelConfig = () =>
hasPackageProp( 'babel' );
const hasJestConfig = () =>
- hasCliArg( '-c' ) ||
- hasCliArg( '--config' ) ||
+ hasArgInCLI( '-c' ) ||
+ hasArgInCLI( '--config' ) ||
hasProjectFile( 'jest.config.js' ) ||
hasProjectFile( 'jest.config.json' ) ||
hasPackageProp( 'jest' );
-const hasWebpackConfig = () => hasCliArg( '--config' ) ||
+const hasWebpackConfig = () => hasArgInCLI( '--config' ) ||
hasProjectFile( 'webpack.config.js' ) ||
hasProjectFile( 'webpack.config.babel.js' );
+/**
+ * Converts CLI arguments to the format which webpack understands.
+ * It allows to optionally pass some additional webpack CLI arguments.
+ *
+ * @see https://webpack.js.org/api/cli/#usage-with-config-file
+ *
+ * @param {?Array} additionalArgs The list of additional CLI arguments.
+ *
+ * @return {Array} The list of CLI arguments to pass to webpack CLI.
+ */
const getWebpackArgs = ( additionalArgs = [] ) => {
- const webpackArgs = getCliArgs();
+ let webpackArgs = getArgsFromCLI();
+
+ const hasWebpackOutputOption = hasArgInCLI( '-o' ) || hasArgInCLI( '--output' );
+ if ( hasFileArgInCLI() && ! hasWebpackOutputOption ) {
+ /**
+ * Converts a path to the entry format supported by webpack, e.g.:
+ * `./entry-one.js` -> `entry-one=./entry-one.js`
+ * `entry-two.js` -> `entry-two=./entry-two.js`
+ *
+ * @param {string} path The path provided.
+ *
+ * @return {string} The entry format supported by webpack.
+ */
+ const pathToEntry = ( path ) => {
+ const entry = basename( path, '.js' );
+
+ if ( ! path.startsWith( './' ) ) {
+ path = './' + path;
+ }
+
+ return [ entry, path ].join( '=' );
+ };
+
+ // The following handles the support for multiple entry points in webpack, e.g.:
+ // `wp-scripts build one.js custom=./two.js` -> `webpack one=./one.js custom=./two.js`
+ webpackArgs = webpackArgs.map( ( cliArg ) => {
+ if ( getFileArgsFromCLI().includes( cliArg ) && ! cliArg.includes( '=' ) ) {
+ return pathToEntry( cliArg );
+ }
+
+ return cliArg;
+ } );
+ }
+
if ( ! hasWebpackConfig() ) {
webpackArgs.push( '--config', fromConfigRoot( 'webpack.config.js' ) );
}
+
webpackArgs.push( ...additionalArgs );
+
return webpackArgs;
};
diff --git a/packages/scripts/utils/index.js b/packages/scripts/utils/index.js
index aebbcdca718007..c45b1b6abbee1e 100644
--- a/packages/scripts/utils/index.js
+++ b/packages/scripts/utils/index.js
@@ -2,11 +2,12 @@
* Internal dependencies
*/
const {
- getCliArg,
- getCliArgs,
- hasCliArg,
+ getArgFromCLI,
+ getArgsFromCLI,
+ getFileArgsFromCLI,
+ hasArgInCLI,
+ hasFileArgInCLI,
spawnScript,
- cleanUpArgs,
} = require( './cli' );
const {
getWebpackArgs,
@@ -27,14 +28,15 @@ const {
module.exports = {
camelCaseDash,
fromConfigRoot,
- getCliArg,
- getCliArgs,
+ getArgFromCLI,
+ getArgsFromCLI,
+ getFileArgsFromCLI,
getWebpackArgs,
hasBabelConfig,
- hasCliArg,
+ hasArgInCLI,
+ hasFileArgInCLI,
hasJestConfig,
hasPackageProp,
hasProjectFile,
spawnScript,
- cleanUpArgs,
};
diff --git a/packages/scripts/utils/process.js b/packages/scripts/utils/process.js
index cfa38afa26aa94..4996190d21e539 100644
--- a/packages/scripts/utils/process.js
+++ b/packages/scripts/utils/process.js
@@ -1,4 +1,4 @@
-const getCliArgs = ( excludePrefixes ) => {
+const getArgsFromCLI = ( excludePrefixes ) => {
const args = process.argv.slice( 2 );
if ( excludePrefixes ) {
return args.filter( ( arg ) => {
@@ -10,6 +10,6 @@ const getCliArgs = ( excludePrefixes ) => {
module.exports = {
exit: process.exit,
- getCliArgs,
+ getArgsFromCLI,
getCurrentWorkingDirectory: process.cwd,
};
diff --git a/packages/scripts/utils/test/index.js b/packages/scripts/utils/test/index.js
index d451a7540411dc..778c51dedb6e9f 100644
--- a/packages/scripts/utils/test/index.js
+++ b/packages/scripts/utils/test/index.js
@@ -7,7 +7,7 @@ import crossSpawn from 'cross-spawn';
* Internal dependencies
*/
import {
- hasCliArg,
+ hasArgInCLI,
hasProjectFile,
spawnScript,
} from '../';
@@ -16,7 +16,7 @@ import {
} from '../package';
import {
exit as exitMock,
- getCliArgs as getCliArgsMock,
+ getArgsFromCLI as getArgsFromCLIMock,
} from '../process';
jest.mock( '../package', () => {
@@ -30,7 +30,7 @@ jest.mock( '../process', () => {
const module = require.requireActual( '../process' );
jest.spyOn( module, 'exit' );
- jest.spyOn( module, 'getCliArgs' );
+ jest.spyOn( module, 'getArgsFromCLI' );
return module;
} );
@@ -38,29 +38,29 @@ jest.mock( '../process', () => {
describe( 'utils', () => {
const crossSpawnMock = jest.spyOn( crossSpawn, 'sync' );
- describe( 'hasCliArg', () => {
+ describe( 'hasArgInCLI', () => {
beforeAll( () => {
- getCliArgsMock.mockReturnValue( [ '-a', '--b', '--config=test' ] );
+ getArgsFromCLIMock.mockReturnValue( [ '-a', '--b', '--config=test' ] );
} );
afterAll( () => {
- getCliArgsMock.mockReset();
+ getArgsFromCLIMock.mockReset();
} );
test( 'should return false when no args passed', () => {
- getCliArgsMock.mockReturnValueOnce( [] );
+ getArgsFromCLIMock.mockReturnValueOnce( [] );
- expect( hasCliArg( '--no-args' ) ).toBe( false );
+ expect( hasArgInCLI( '--no-args' ) ).toBe( false );
} );
test( 'should return false when checking for unrecognized arg', () => {
- expect( hasCliArg( '--non-existent' ) ).toBe( false );
+ expect( hasArgInCLI( '--non-existent' ) ).toBe( false );
} );
test( 'should return true when CLI arg found', () => {
- expect( hasCliArg( '-a' ) ).toBe( true );
- expect( hasCliArg( '--b' ) ).toBe( true );
- expect( hasCliArg( '--config' ) ).toBe( true );
+ expect( hasArgInCLI( '-a' ) ).toBe( true );
+ expect( hasArgInCLI( '--b' ) ).toBe( true );
+ expect( hasArgInCLI( '--config' ) ).toBe( true );
} );
} );
diff --git a/packages/server-side-render/.npmrc b/packages/server-side-render/.npmrc
new file mode 100644
index 00000000000000..43c97e719a5a82
--- /dev/null
+++ b/packages/server-side-render/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/packages/server-side-render/CHANGELOG.md b/packages/server-side-render/CHANGELOG.md
new file mode 100644
index 00000000000000..04ea9fe434a6fb
--- /dev/null
+++ b/packages/server-side-render/CHANGELOG.md
@@ -0,0 +1,5 @@
+## Unreleased
+
+### Enhancements
+
+- Extracted the package from `@wordpress/components` and `@wordpress/editor`;
diff --git a/packages/components/src/server-side-render/README.md b/packages/server-side-render/README.md
similarity index 76%
rename from packages/components/src/server-side-render/README.md
rename to packages/server-side-render/README.md
index df9d75dec8fa4f..a975cd36f19d10 100644
--- a/packages/components/src/server-side-render/README.md
+++ b/packages/server-side-render/README.md
@@ -8,6 +8,23 @@ ServerSideRender should be regarded as a fallback or legacy mechanism, it is not
New blocks should be built in conjunction with any necessary REST API endpoints, so that JavaScript can be used for rendering client-side in the `edit` function. This gives the best user experience, instead of relying on using the PHP `render_callback`. The logic necessary for rendering should be included in the endpoint, so that both the client-side JavaScript and server-side PHP logic should require a minimal amount of differences.
+> This package is meant to be used only with WordPress core. Feel free to use it in your own project but please keep in mind that it might never get fully documented.
+
+## Installation
+
+Install the module
+
+```bash
+npm install @wordpress/server-side-render --save
+```
+
+_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._
+
+## Usage
+
+The props accepted by the component are described below.
+
+
## Props
### attributes
@@ -43,7 +60,7 @@ E.g: `{ post_id: 12 }`.
Render core/archives preview.
```jsx
-import { ServerSideRender } from '@wordpress/components';
+import { ServerSideRender } from '@wordpress/server-side-render';
const MyServerSideRender = () => (
{
+ const coreEditorSelect = select( 'core/editor' );
+ if ( coreEditorSelect ) {
+ const currentPostId = coreEditorSelect.getCurrentPostId();
+ if ( currentPostId ) {
+ return {
+ currentPostId,
+ };
+ }
+ }
+ return EMPTY_OBJECT;
+ }
+)(
+ ( { urlQueryArgs = EMPTY_OBJECT, currentPostId, ...props } ) => {
+ const newUrlQueryArgs = useMemo( () => {
+ if ( ! currentPostId ) {
+ return urlQueryArgs;
+ }
+ return {
+ post_id: currentPostId,
+ ...urlQueryArgs,
+ };
+ }, [ currentPostId, urlQueryArgs ] );
+
+ return (
+
+ );
+ }
+);
diff --git a/packages/components/src/server-side-render/index.js b/packages/server-side-render/src/server-side-render.js
similarity index 96%
rename from packages/components/src/server-side-render/index.js
rename to packages/server-side-render/src/server-side-render.js
index d58564d2cd82eb..a0c6a915efae55 100644
--- a/packages/components/src/server-side-render/index.js
+++ b/packages/server-side-render/src/server-side-render.js
@@ -13,12 +13,10 @@ import {
import { __, sprintf } from '@wordpress/i18n';
import apiFetch from '@wordpress/api-fetch';
import { addQueryArgs } from '@wordpress/url';
-
-/**
- * Internal dependencies
- */
-import Placeholder from '../placeholder';
-import Spinner from '../spinner';
+import {
+ Placeholder,
+ Spinner,
+} from '@wordpress/components';
export function rendererPath( block, attributes = null, urlQueryArgs = {} ) {
return addQueryArgs( `/wp/v2/block-renderer/${ block }`, {
diff --git a/packages/components/src/server-side-render/test/index.js b/packages/server-side-render/src/test/index.js
similarity index 97%
rename from packages/components/src/server-side-render/test/index.js
rename to packages/server-side-render/src/test/index.js
index c09814771f7a75..b97a80aa91b2b8 100644
--- a/packages/components/src/server-side-render/test/index.js
+++ b/packages/server-side-render/src/test/index.js
@@ -1,7 +1,7 @@
/**
* Internal dependencies
*/
-import { rendererPath } from '../index';
+import { rendererPath } from '../server-side-render';
describe( 'rendererPath', function() {
test( 'should return an base path for empty input', function() {
diff --git a/packages/token-list/package.json b/packages/token-list/package.json
index 5d14fab2f65321..d7ca4747b7ad69 100644
--- a/packages/token-list/package.json
+++ b/packages/token-list/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/token-list",
- "version": "1.3.0",
+ "version": "1.4.0",
"description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -18,6 +18,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
+ "react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.4.4",
"lodash": "^4.17.11"
diff --git a/packages/viewport/package.json b/packages/viewport/package.json
index 495f6bb81fafc4..334d7a834a112c 100644
--- a/packages/viewport/package.json
+++ b/packages/viewport/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/viewport",
- "version": "2.4.0",
+ "version": "2.5.0",
"description": "Viewport module for WordPress.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json
index af4448b1d46f53..f3bac4a49be8ef 100644
--- a/packages/wordcount/package.json
+++ b/packages/wordcount/package.json
@@ -1,6 +1,6 @@
{
"name": "@wordpress/wordcount",
- "version": "2.3.0",
+ "version": "2.4.0",
"description": "WordPress word count utility.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
@@ -19,6 +19,7 @@
},
"main": "build/index.js",
"module": "build-module/index.js",
+ "react-native": "src/index",
"dependencies": {
"@babel/runtime": "^7.4.4",
"lodash": "^4.17.11"
diff --git a/playground/src/index.js b/playground/src/index.js
index 0e0bc6a87f8d84..3f125282c49090 100644
--- a/playground/src/index.js
+++ b/playground/src/index.js
@@ -1,8 +1,3 @@
-/**
- * External dependencies
- */
-import '@babel/polyfill';
-
/**
* WordPress dependencies
*/
diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js
index 597dd2735c33f1..4b1f0974325e99 100644
--- a/test/unit/jest.config.js
+++ b/test/unit/jest.config.js
@@ -23,5 +23,6 @@ module.exports = {
'/packages/e2e-tests',
'/.*/build/',
'/.*/build-module/',
+ '/.+\.native\.js$',
],
};
diff --git a/webpack.config.js b/webpack.config.js
index ba130ce0992d4a..8a8348bb494d52 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -4,7 +4,7 @@
const { DefinePlugin } = require( 'webpack' );
const CopyWebpackPlugin = require( 'copy-webpack-plugin' );
const postcss = require( 'postcss' );
-const { get, escapeRegExp } = require( 'lodash' );
+const { get, escapeRegExp, compact } = require( 'lodash' );
const { basename, sep } = require( 'path' );
/**
@@ -12,7 +12,7 @@ const { basename, sep } = require( 'path' );
*/
const CustomTemplatedPathPlugin = require( '@wordpress/custom-templated-path-webpack-plugin' );
const LibraryExportDefaultPlugin = require( '@wordpress/library-export-default-webpack-plugin' );
-const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
+const DependencyExtractionWebpackPlugin = require( '@wordpress/dependency-extraction-webpack-plugin' );
const { camelCaseDash } = require( '@wordpress/scripts/utils' );
/**
@@ -20,6 +20,11 @@ const { camelCaseDash } = require( '@wordpress/scripts/utils' );
*/
const { dependencies } = require( './package' );
+const {
+ NODE_ENV: mode = 'development',
+ WP_DEVTOOL: devtool = ( mode === 'production' ? false : 'source-map' ),
+} = process.env;
+
const WORDPRESS_NAMESPACE = '@wordpress/';
const gutenbergPackages = Object.keys( dependencies )
@@ -27,7 +32,7 @@ const gutenbergPackages = Object.keys( dependencies )
.map( ( packageName ) => packageName.replace( WORDPRESS_NAMESPACE, '' ) );
module.exports = {
- ...defaultConfig,
+ mode,
entry: gutenbergPackages.reduce( ( memo, packageName ) => {
const name = camelCaseDash( packageName );
memo[ name ] = `./packages/${ packageName }`;
@@ -39,12 +44,21 @@ module.exports = {
library: [ 'wp', '[name]' ],
libraryTarget: 'this',
},
+ module: {
+ rules: compact( [
+ mode !== 'production' && {
+ test: /\.js$/,
+ use: require.resolve( 'source-map-loader' ),
+ enforce: 'pre',
+ },
+ ] ),
+ },
plugins: [
- ...defaultConfig.plugins,
new DefinePlugin( {
// Inject the `GUTENBERG_PHASE` global, used for feature flagging.
// eslint-disable-next-line @wordpress/gutenberg-phase
'process.env.GUTENBERG_PHASE': JSON.stringify( parseInt( process.env.npm_package_config_GUTENBERG_PHASE, 10 ) || 1 ),
+ 'process.env.FORCE_REDUCED_MOTION': JSON.stringify( process.env.FORCE_REDUCED_MOTION ),
} ),
new CustomTemplatedPathPlugin( {
basename( path, data ) {
@@ -74,6 +88,7 @@ module.exports = {
'dom-ready',
'redux-routine',
'token-list',
+ 'server-side-render',
'shortcode',
].map( camelCaseDash ) ),
new CopyWebpackPlugin(
@@ -82,7 +97,7 @@ module.exports = {
to: `./build/${ packageName }/`,
flatten: true,
transform: ( content ) => {
- if ( defaultConfig.mode === 'production' ) {
+ if ( mode === 'production' ) {
return postcss( [
require( 'cssnano' )( {
preset: [ 'default', {
@@ -134,5 +149,7 @@ module.exports = {
},
},
] ),
+ new DependencyExtractionWebpackPlugin( { injectPolyfill: true } ),
],
+ devtool,
};