From 4cfbd6446543c18459fb44c0cb4a78d7994a2719 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 13 Aug 2019 18:35:45 +0100 Subject: [PATCH 01/52] Add editor-styles-wrapper class to the widgets screen block list (#17023) --- .../src/components/widget-area/index.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/packages/edit-widgets/src/components/widget-area/index.js b/packages/edit-widgets/src/components/widget-area/index.js index 517ae9d3212ff0..7a1b897cb8d517 100644 --- a/packages/edit-widgets/src/components/widget-area/index.js +++ b/packages/edit-widgets/src/components/widget-area/index.js @@ -86,13 +86,15 @@ function WidgetArea( { - - - - - +
+ + + + + +
From 387d85ab1ac03ccce908a8f836bbaa6e0ad61c92 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Tue, 13 Aug 2019 21:42:48 +0100 Subject: [PATCH 02/52] Add: Button Block appender to the widgets screen. (#16971) --- packages/edit-widgets/src/components/widget-area/index.js | 2 ++ .../edit-widgets/src/components/widget-area/style.scss | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/packages/edit-widgets/src/components/widget-area/index.js b/packages/edit-widgets/src/components/widget-area/index.js index 7a1b897cb8d517..a224ea87f2f833 100644 --- a/packages/edit-widgets/src/components/widget-area/index.js +++ b/packages/edit-widgets/src/components/widget-area/index.js @@ -18,6 +18,7 @@ import { WritingFlow, ObserveTyping, BlockEditorKeyboardShortcuts, + ButtonBlockerAppender, } from '@wordpress/block-editor'; import { withDispatch, withSelect } from '@wordpress/data'; @@ -91,6 +92,7 @@ function WidgetArea( { diff --git a/packages/edit-widgets/src/components/widget-area/style.scss b/packages/edit-widgets/src/components/widget-area/style.scss index 60f2ce9f1b7e7b..392d1ae8ff2128 100644 --- a/packages/edit-widgets/src/components/widget-area/style.scss +++ b/packages/edit-widgets/src/components/widget-area/style.scss @@ -17,3 +17,10 @@ .edit-widgets-main-block-list { padding-top: $block-toolbar-height + 2 * $grid-size; } + +.edit-widgets-main-block-list > .block-list-appender { + padding-top: $panel-padding; + // Add a margin equivalent to block side UI so the appender is aligned with the blocks. + margin-left: $block-side-ui-width; + margin-right: $block-side-ui-width; +} From a94e9075f47f067ff431951d1b1022075f2e4fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Wed, 14 Aug 2019 02:34:15 +0200 Subject: [PATCH 03/52] Scripts: Add section about updating package after new releases (#17026) * Scripts: Add section about updating package after new releases It also contains some editorial changes to make things clearer * Use one version of apostrophe --- packages/scripts/README.md | 46 +++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/packages/scripts/README.md b/packages/scripts/README.md index 671ad6cbd2d780..fdbb75f9876cfd 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -1,14 +1,16 @@ # Scripts -Collection of reusable scripts for WordPress development. For convenience, every tool provided in this package comes with a recommended configuration. +This is a collection of reusable scripts tailored for WordPress development. For convenience, every tool provided in this package comes with an integrated recommended configuration. -Command-line interfaces help to turn working with an app into a pleasant experience, but it is still not enough to keep it easy to maintain in the long run. Developers are left on their own to keep all configurations and dependent tools up to date. This problem multiplies when they own more than one project which shares the same setup. Fortunately, there is a pattern that can simplify maintainers life – reusable scripts. This idea boils down to moving all the necessary configurations and scripts to one single tool dependency. In most cases, it should be possible to accomplish all tasks using the default settings, but some customization is allowed, too. With all that in place updating all projects should become a very straightforward task. +When working seamlessly, sophisticated command-line interfaces help to turn work with a project into a more pleasant experience. However, it’s a misleading assumption that developers can easily pick the proper tools in the first place and then ensure that they play along with each other, including all their extensions. Besides, it’s still not enough because developers are left on their own to keep all configurations and dependent tools up to date. This problem multiplies when they support more than one project which shares the same setup. + +Fortunately, there is a pattern that can simplify maintainers life – reusable scripts. The idea boils down to moving all the necessary configurations and scripts to one single tool dependency. In most cases, it should be possible to accomplish all tasks using the default settings, but some customization is allowed, too. With all that in place, updating all projects should become a very straightforward task. _This package is inspired by [react-scripts](https://www.npmjs.com/package/react-scripts) and [kcd-scripts](https://www.npmjs.com/package/kcd-scripts)._ ## Installation -Install the module +You only need to install one npm module: ```bash npm install @wordpress/scripts --save-dev @@ -16,7 +18,7 @@ npm install @wordpress/scripts --save-dev ## Setup -This is a CLI and exposes a binary called `wp-scripts` so you can call it directly. However this module is designed to be configured using the `scripts` section in the `package.json` file of your project. +This package offers a command-line interface and exposes a binary called `wp-scripts` so you can call it directly with `npx` – an npm package runner. However, this module is designed to be configured using the `scripts` section in the `package.json` file of your project. This comprehensive example demonstrates the most of the capabilities included. _Example:_ @@ -38,11 +40,19 @@ _Example:_ It might also be a good idea to get familiar with the [JavaScript Build Setup tutorial](/docs/designers-developers/developers/tutorials/javascript/js-build-setup.md) for setting up a development environment to use ESNext syntax. It gives a very in-depth explanation of how to use the [build](#build) and [start](#start) scripts. +## Updating to New Release + +To update an existing project to a new version of `@wordpress/scripts`, open the [changelog](/packages/scripts/CHANGELOG.md), find the version you’re currently on (check `package.json` in the top-level directory of your project), and apply the migration instructions for the newer versions. + +In most cases bumping the `@wordpress/scripts` version in `package.json` and running `npm install` in the root folder of your project should be enough, but it’s good to check the [changelog](/packages/scripts/CHANGELOG.md) for potential breaking changes. + +We commit to keeping the breaking changes minimal so you can upgrade `@wordpress/scripts` as seamless as possible. + ## Available Scripts ### `build` -Transforms your code according the configuration provided so it's ready for production and optimized for the best performance. The entry point for your project's code should be located in `src/index.js`. The output generated will be written to `build/index.js`. This script exits after producing a single build. For incremental builds, better suited for development, see the [start](#start) script. +Transforms your code according the configuration provided so it’s ready for production and optimized for the best performance. The entry point for your project’s code should be located in `src/index.js`. The output generated will be written to `build/index.js`. This script exits after producing a single build. For incremental builds, better suited for development, see the [start](#start) script. _Example:_ @@ -62,7 +72,7 @@ This is how you execute the script with presented setup: #### Advanced information -This script uses [webpack](https://webpack.js.org/) behind the scenes. It'll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it'll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. +This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. ### `check-engines` @@ -88,7 +98,7 @@ It uses [check-node-version](https://www.npmjs.com/package/check-node-version) b ### `check-licenses` -Validates that all dependencies of a project are compatible with the project's own license. +Validates that all dependencies of a project are compatible with the project’s own license. _Example:_ @@ -105,7 +115,7 @@ _Flags_: - `--prod` (or `--production`): When present, validates only `dependencies` and not `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. +- `--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. ### `lint-js` @@ -124,8 +134,8 @@ _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. +* `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). @@ -152,8 +162,8 @@ _Example:_ This is how you execute those scripts using the presented setup: -* `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. +* `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). @@ -180,8 +190,8 @@ _Example:_ This is how you execute the script with presented setup: -* `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. +* `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). @@ -193,7 +203,7 @@ It uses [stylelint](https://github.com/stylelint/stylelint) with the [stylelint- ### `start` -Transforms your code according the configuration provided so it's ready for development. The script will automatically rebuild if you make changes to the code, and you will see the build errors in the console. The entry point for your project's code should be located in `src/index.js`. The output generated will be written to `build/index.js`. For single builds, better suited for production, see the [build](#build) script. +Transforms your code according the configuration provided so it’s ready for development. The script will automatically rebuild if you make changes to the code, and you will see the build errors in the console. The entry point for your project’s code should be located in `src/index.js`. The output generated will be written to `build/index.js`. For single builds, better suited for production, see the [build](#build) script. _Example:_ @@ -213,7 +223,7 @@ This is how you execute the script with presented setup: #### Advanced information -This script uses [webpack](https://webpack.js.org/) behind the scenes. It'll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it'll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. +This script uses [webpack](https://webpack.js.org/) behind the scenes. It’ll look for a webpack config in the top-level directory of your package and will use it if it finds one. If none is found, it’ll use the default config provided by `@wordpress/scripts` packages. Learn more in the [Advanced Usage](#advanced-usage) section. ### `test-e2e` @@ -281,7 +291,7 @@ It uses [Jest](https://jestjs.io/) behind the scenes and you are able to use all ## Advanced Usage -In general, this package should be used with the set of recommended config files. While it is possible to override every single config file provided, if you have to do it, it means that your use case is more complex than anticipated. If that happens, it would be better to avoid using the whole abstraction layer and set up your project with full control over tooling used. +In general, this package should be used with the set of recommended config files. While it’s possible to override every single config file provided, if you have to do it, it means that your use case is far more complicated than anticipated. If that happens, it would be better to avoid using the whole abstraction layer and set up your project with full control over tooling used. ### Webpack config @@ -328,5 +338,5 @@ module.exports = { } }; ``` -If you follow this approach, please, be aware that future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package's CHANGELOG](https://github.com/WordPress/gutenberg/blob/master/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading. +If you follow this approach, please, be aware that future versions of this package may change what webpack and Babel plugins we bundle, default configs, etc. Should those changes be necessary, they will be registered in the [package’s CHANGELOG](https://github.com/WordPress/gutenberg/blob/master/packages/scripts/CHANGELOG.md), so make sure to read it before upgrading.

Code is Poetry.

From 940184cb6a88dddfe0d22580255797470209c227 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 14 Aug 2019 09:24:00 +0100 Subject: [PATCH 04/52] Fix: Duplicate button appears even if the block is not allowed (#17007) --- .../src/components/block-actions/index.js | 11 ++++++++--- .../src/components/block-settings-menu/index.js | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-actions/index.js b/packages/block-editor/src/components/block-actions/index.js index 3e12269b074e33..82f12e6e256048 100644 --- a/packages/block-editor/src/components/block-actions/index.js +++ b/packages/block-editor/src/components/block-actions/index.js @@ -46,10 +46,15 @@ export default compose( [ const { getDefaultBlockName } = select( 'core/blocks' ); const blocks = getBlocksByClientId( props.clientIds ); + const rootClientId = getBlockRootClientId( props.clientIds[ 0 ] ); const canDuplicate = every( blocks, ( block ) => { - return !! block && hasBlockSupport( block.name, 'multiple', true ); + return ( + !! block && + hasBlockSupport( block.name, 'multiple', true ) && + canInsertBlockType( block.name, rootClientId ) + ); } ); - const rootClientId = getBlockRootClientId( props.clientIds[ 0 ] ); + const canInsertDefaultBlock = canInsertBlockType( getDefaultBlockName(), rootClientId @@ -83,7 +88,7 @@ export default compose( [ return { onDuplicate() { - if ( isLocked || ! canDuplicate ) { + if ( ! canDuplicate ) { return; } 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 a0133182548025..5a7890c9002b42 100644 --- a/packages/block-editor/src/components/block-settings-menu/index.js +++ b/packages/block-editor/src/components/block-settings-menu/index.js @@ -69,7 +69,7 @@ export function BlockSettingsMenu( { clientIds } ) { clientId={ firstBlockClientId } /> ) } - { ! isLocked && canDuplicate && ( + { canDuplicate && ( Date: Wed, 14 Aug 2019 10:41:41 +0100 Subject: [PATCH 05/52] Fix the double scrollbar appearing in full screen mode (#17031) --- packages/edit-post/src/components/layout/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/edit-post/src/components/layout/style.scss b/packages/edit-post/src/components/layout/style.scss index 66307b74c0101c..f275e9f14d0962 100644 --- a/packages/edit-post/src/components/layout/style.scss +++ b/packages/edit-post/src/components/layout/style.scss @@ -5,6 +5,7 @@ .edit-post-layout { position: relative; + box-sizing: border-box; // Beyond the mobile breakpoint, the editor bar is fixed, so make room for it eabove the layout content. @include break-small { From be7db4f6c98857bae01180b07db0e8a2ec18f6d1 Mon Sep 17 00:00:00 2001 From: Enrique Piqueras Date: Wed, 14 Aug 2019 02:42:23 -0700 Subject: [PATCH 06/52] Core Data: Return updated record in `saveEntityRecord`. (#17030) --- packages/core-data/src/actions.js | 9 ++++++--- packages/core-data/src/test/actions.js | 14 ++++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/core-data/src/actions.js b/packages/core-data/src/actions.js index 43861be226ca5d..80d9fa1915b19a 100644 --- a/packages/core-data/src/actions.js +++ b/packages/core-data/src/actions.js @@ -231,6 +231,7 @@ export function* saveEntityRecord( const recordId = record[ entityIdKey ]; yield { type: 'SAVE_ENTITY_RECORD_START', kind, name, recordId, isAutosave }; + let updatedRecord; let error; try { const path = `${ entity.baseURL }${ recordId ? '/' + recordId : '' }`; @@ -260,14 +261,14 @@ export function* saveEntityRecord( } return acc; }, {} ); - const autosave = yield apiFetch( { + updatedRecord = yield apiFetch( { path: `${ path }/autosaves`, method: 'POST', data, } ); - yield receiveAutosaves( persistedRecord.id, autosave ); + yield receiveAutosaves( persistedRecord.id, updatedRecord ); } else { - const updatedRecord = yield apiFetch( { + updatedRecord = yield apiFetch( { path, method: recordId ? 'PUT' : 'POST', data: record, @@ -285,6 +286,8 @@ export function* saveEntityRecord( error, isAutosave, }; + + return updatedRecord; } /** diff --git a/packages/core-data/src/test/actions.js b/packages/core-data/src/test/actions.js index 16ac5776be1976..7e62338545520d 100644 --- a/packages/core-data/src/test/actions.js +++ b/packages/core-data/src/test/actions.js @@ -21,9 +21,19 @@ describe( 'saveEntityRecord', () => { data: post, } ); // Provide response and trigger action - const { value: received } = fulfillment.next( { ...post, id: 10 } ); - expect( received ).toEqual( receiveEntityRecords( 'postType', 'post', { ...post, id: 10 }, undefined, true ) ); + const updatedRecord = { ...post, id: 10 }; + const { value: received } = fulfillment.next( updatedRecord ); + expect( received ).toEqual( + receiveEntityRecords( + 'postType', + 'post', + updatedRecord, + undefined, + true + ) + ); expect( fulfillment.next().value.type ).toBe( 'SAVE_ENTITY_RECORD_FINISH' ); + expect( fulfillment.next().value ).toBe( updatedRecord ); } ); it( 'triggers a PUT request for an existing record', async () => { From 8d62c26e4c5ec554a7a41c78650ff7480a069749 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 14 Aug 2019 12:57:57 +0100 Subject: [PATCH 07/52] Fix performance tests with the introduction of the navigation mode (#17034) --- packages/e2e-tests/specs/performance.test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/e2e-tests/specs/performance.test.js b/packages/e2e-tests/specs/performance.test.js index c63ffb11560879..174bf2c6ab1301 100644 --- a/packages/e2e-tests/specs/performance.test.js +++ b/packages/e2e-tests/specs/performance.test.js @@ -11,6 +11,7 @@ import { createNewPost, saveDraft, insertBlock, + disableNavigationMode, } from '@wordpress/e2e-test-utils'; function readFile( filePath ) { @@ -53,6 +54,7 @@ describe( 'Performance', () => { while ( i-- ) { startTime = new Date(); await page.reload( { waitUntil: [ 'domcontentloaded', 'load' ] } ); + await disableNavigationMode(); } await insertBlock( 'Paragraph' ); From b99ae5600bd8855270f9b652864d68e97e0c0e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 14 Aug 2019 14:51:44 +0200 Subject: [PATCH 08/52] RichText: ignore selection changes during composition (#16960) --- packages/rich-text/src/component/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 1689efc01056d2..a6af287ec5a5d3 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -391,9 +391,11 @@ class RichText extends Component { } /** - * Handles the `selectionchange` event: sync the selection to local state. + * Syncs the selection to local state. A callback for the `selectionchange` + * native events, `keyup`, `mouseup` and `touchend` synthetic events, and + * animation frames after the `focus` event. * - * @param {Event} event `selectionchange` event. + * @param {Event|SyntheticEvent|DOMHighResTimeStamp} event */ onSelectionChange( event ) { if ( @@ -403,6 +405,15 @@ class RichText extends Component { return; } + // In case of a keyboard event, ignore selection changes during + // composition. + if ( + event.nativeEvent && + event.nativeEvent.isComposing + ) { + return; + } + const { start, end } = this.createRecord(); const value = this.getRecord(); From 989c224ce81008d3448932cfb03aa0a71922e6be Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Wed, 14 Aug 2019 13:59:02 +0100 Subject: [PATCH 09/52] Bump plugin version to 6.3.0 --- gutenberg.php | 2 +- package-lock.json | 2 +- package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 136d5230c59687..e9062de0f73069 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: 6.3.0-rc.1 + * Version: 6.3.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index db4ab1e5b5f252..96b0a0d3d31555 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.3.0-rc.1", + "version": "6.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 46ebf924d604a2..aa4805e6597d89 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "6.3.0-rc.1", + "version": "6.3.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 24caaa3d324bf71fc498abcf559863454433a0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 14 Aug 2019 21:22:41 +0200 Subject: [PATCH 10/52] =?UTF-8?q?RichText=20=F0=9F=A7=B9=20(clean=20up,=20?= =?UTF-8?q?move=20Autocomplete,=20remove=20DOM=20dependency)=20(#16905)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * RichText 🧹 * Clean up * Clean up * Simplify * Move Autocomplete * children => Editable wrapper component * Bind Editable wrapper to class * Minimise diff --- package-lock.json | 1 - .../src/components/rich-text/index.js | 95 ++-- packages/rich-text/package.json | 1 - packages/rich-text/src/component/index.js | 425 +++++++++--------- 4 files changed, 265 insertions(+), 257 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96b0a0d3d31555..2febb418532ae1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5167,7 +5167,6 @@ "@babel/runtime": "^7.4.4", "@wordpress/compose": "file:packages/compose", "@wordpress/data": "file:packages/data", - "@wordpress/dom": "file:packages/dom", "@wordpress/element": "file:packages/element", "@wordpress/escape-html": "file:packages/escape-html", "@wordpress/hooks": "file:packages/hooks", diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1cf2c6225afaa0..344aa0d96edc1b 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -9,7 +9,7 @@ import { omit } from 'lodash'; */ import { RawHTML, Component } from '@wordpress/element'; import { withDispatch, withSelect } from '@wordpress/data'; -import { pasteHandler, children, getBlockTransforms, findTransform } from '@wordpress/blocks'; +import { pasteHandler, children as childrenSource, getBlockTransforms, findTransform } from '@wordpress/blocks'; import { withInstanceId, compose } from '@wordpress/compose'; import { RichText, @@ -42,6 +42,21 @@ import { RemoveBrowserShortcuts } from './remove-browser-shortcuts'; const wrapperClasses = 'editor-rich-text block-editor-rich-text'; const classes = 'editor-rich-text__editable block-editor-rich-text__editable'; +/** + * Get the multiline tag based on the multiline prop. + * + * @param {?(string|boolean)} multiline The multiline prop. + * + * @return {?string} The multiline tag. + */ +function getMultilineTag( multiline ) { + if ( multiline !== true && multiline !== 'p' && multiline !== 'li' ) { + return; + } + + return multiline === true ? 'p' : multiline; +} + class RichTextWrapper extends Component { constructor() { super( ...arguments ); @@ -102,7 +117,13 @@ class RichTextWrapper extends Component { } onPaste( { value, onChange, html, plainText, image } ) { - const { onReplace, onSplit, tagName, canUserUseUnfilteredHTML } = this.props; + const { + onReplace, + onSplit, + tagName, + canUserUseUnfilteredHTML, + multiline, + } = this.props; if ( image && ! html ) { const file = image.getAsFile ? image.getAsFile() : image; @@ -149,7 +170,7 @@ class RichTextWrapper extends Component { // If the content should be multiline, we should process text // separated by a line break as separate lines. - if ( this.multilineTag ) { + if ( multiline ) { valueToInsert = replace( valueToInsert, /\n+/g, LINE_SEPARATOR ); } @@ -187,6 +208,7 @@ class RichTextWrapper extends Component { const blocks = []; const [ before, after ] = split( record ); const hasPastedBlocks = pastedBlocks.length > 0; + const multilineTag = getMultilineTag( multiline ); // Create a block with the content before the caret if there's no pasted // blocks, or if there are pasted blocks and the value is not empty. @@ -195,7 +217,7 @@ class RichTextWrapper extends Component { if ( ! hasPastedBlocks || ! isEmpty( before ) ) { blocks.push( onSplit( toHTMLString( { value: before, - multilineTag: multiline, + multilineTag, } ) ) ); } @@ -212,7 +234,7 @@ class RichTextWrapper extends Component { if ( hasPastedBlocks || ! onSplitMiddle || ! isEmpty( after ) ) { blocks.push( onSplit( toHTMLString( { value: after, - multilineTag: multiline, + multilineTag, } ) ) ); } @@ -313,6 +335,7 @@ class RichTextWrapper extends Component { // From experimental filter. To do: pick props instead. ...experimentalProps } = this.props; + const multilineTag = getMultilineTag( multiline ); const adjustedAllowedFormats = this.getAllowedFormats(); const hasFormats = ! adjustedAllowedFormats || adjustedAllowedFormats.length > 0; @@ -321,13 +344,13 @@ class RichTextWrapper extends Component { // Handle deprecated format. if ( Array.isArray( originalValue ) ) { - adjustedValue = children.toHTML( originalValue ); - adjustedOnChange = ( newValue ) => originalOnChange( children.fromDOM( + adjustedValue = childrenSource.toHTML( originalValue ); + adjustedOnChange = ( newValue ) => originalOnChange( childrenSource.fromDOM( __unstableCreateElement( document, newValue ).childNodes ) ); } - return ( + const content = ( - { ( { isSelected, value, onChange } ) => + { ( { isSelected, value, onChange, Editable } ) => <> - { isSelected && multiline === 'li' && ( + { isSelected && multilineTag === 'li' && ( ) } { isSelected && } + + { ( { listBoxId, activeId } ) => + + } + } ); + + return ( +
+ { content } +
+ ); } } @@ -444,23 +483,18 @@ const RichTextContainer = compose( [ ] )( RichTextWrapper ); RichTextContainer.Content = ( { value, tagName: Tag, multiline, ...props } ) => { - let html = value; - let MultilineTag; - - if ( multiline === true || multiline === 'p' || multiline === 'li' ) { - MultilineTag = multiline === true ? 'p' : multiline; - } - // Handle deprecated `children` and `node` sources. if ( Array.isArray( value ) ) { - html = children.toHTML( value ); + value = childrenSource.toHTML( value ); } - if ( ! html && MultilineTag ) { - html = `<${ MultilineTag }>`; + const MultilineTag = getMultilineTag( multiline ); + + if ( ! value && MultilineTag ) { + value = `<${ MultilineTag }>`; } - const content = { html }; + const content = { value }; if ( Tag ) { return { content }; @@ -469,13 +503,8 @@ RichTextContainer.Content = ( { value, tagName: Tag, multiline, ...props } ) => return content; }; -RichTextContainer.isEmpty = ( value = '' ) => { - // Handle deprecated `children` and `node` sources. - if ( Array.isArray( value ) ) { - return ! value || value.length === 0; - } - - return value.length === 0; +RichTextContainer.isEmpty = ( value ) => { + return ! value || value.length === 0; }; RichTextContainer.Content.defaultProps = { diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index e0d763ba266d8c..c0fea5f7ccb901 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -24,7 +24,6 @@ "@babel/runtime": "^7.4.4", "@wordpress/compose": "file:../compose", "@wordpress/data": "file:../data", - "@wordpress/dom": "file:../dom", "@wordpress/element": "file:../element", "@wordpress/escape-html": "file:../escape-html", "@wordpress/hooks": "file:../hooks", diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index a6af287ec5a5d3..f75aceb79cc598 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -12,7 +12,6 @@ import { * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { isHorizontalEdge } from '@wordpress/dom'; import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; import { withSelect } from '@wordpress/data'; import { withSafeTimeout, compose } from '@wordpress/compose'; @@ -24,7 +23,6 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import FormatEdit from './format-edit'; import Editable from './editable'; import { pickAriaProps } from './aria'; -import { isEmpty } from '../is-empty'; import { create } from '../create'; import { apply, toDom } from '../to-dom'; import { toHTMLString } from '../to-html-string'; @@ -85,42 +83,40 @@ function createPrepareEditableTree( props, prefix ) { class RichText extends Component { constructor( { value, - __unstableMultiline: multiline, selectionStart, selectionEnd, } ) { super( ...arguments ); - if ( multiline === true || multiline === 'p' || multiline === 'li' ) { - this.multilineTag = multiline === true ? 'p' : multiline; - } - - if ( this.multilineTag === 'li' ) { - this.multilineWrapperTags = [ 'ul', 'ol' ]; - } - this.onFocus = this.onFocus.bind( this ); this.onBlur = this.onBlur.bind( this ); this.onChange = this.onChange.bind( this ); - this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); - this.onKeyDown = this.onKeyDown.bind( this ); + this.handleDelete = this.handleDelete.bind( this ); + this.handleEnter = this.handleEnter.bind( this ); + this.handleSpace = this.handleSpace.bind( this ); + this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); this.onPaste = this.onPaste.bind( this ); this.onCreateUndoLevel = this.onCreateUndoLevel.bind( this ); this.onInput = this.onInput.bind( this ); this.onCompositionEnd = this.onCompositionEnd.bind( this ); this.onSelectionChange = this.onSelectionChange.bind( this ); - this.getRecord = this.getRecord.bind( this ); this.createRecord = this.createRecord.bind( this ); this.applyRecord = this.applyRecord.bind( this ); this.valueToFormat = this.valueToFormat.bind( this ); this.setRef = this.setRef.bind( this ); this.valueToEditableHTML = this.valueToEditableHTML.bind( this ); - this.handleHorizontalNavigation = this.handleHorizontalNavigation.bind( this ); this.onPointerDown = this.onPointerDown.bind( this ); this.formatToValue = this.formatToValue.bind( this ); + this.Editable = this.Editable.bind( this ); - this.state = {}; + this.onKeyDown = ( event ) => { + this.handleDelete( event ); + this.handleEnter( event ); + this.handleSpace( event ); + this.handleHorizontalNavigation( event ); + }; + this.state = {}; this.lastHistoryValue = value; // Internal values are updated synchronously, unlike props and state. @@ -152,34 +148,28 @@ class RichText extends Component { } } - /** - * Get the current record (value and selection) from props and state. - * - * @return {Object} The current record (value and selection). - */ - getRecord() { - return this.record; - } - createRecord() { + const { __unstableMultilineTag: multilineTag } = this.props; const selection = getSelection(); const range = selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; return create( { element: this.editableRef, range, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, + multilineTag, + multilineWrapperTags: multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, __unstableIsEditableTree: true, } ); } applyRecord( record, { domOnly } = {} ) { + const { __unstableMultilineTag: multilineTag } = this.props; + apply( { value: record, current: this.editableRef, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, + multilineTag, + multilineWrapperTags: multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, prepareEditableTree: createPrepareEditableTree( this.props, 'format_prepare_functions' ), __unstableDomOnly: domOnly, placeholder: this.props.placeholder, @@ -229,7 +219,7 @@ class RichText extends Component { window.console.log( 'Received HTML:\n\n', html ); window.console.log( 'Received plain text:\n\n', plainText ); - const record = this.getRecord(); + const record = this.record; const transformed = formatTypes.reduce( ( accumlator, { __unstablePasteRule } ) => { // Only allow one transform. if ( __unstablePasteRule && accumlator === record ) { @@ -340,7 +330,7 @@ class RichText extends Component { inputType.indexOf( 'format' ) === 0 || INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) ) { - this.applyRecord( this.getRecord() ); + this.applyRecord( this.record ); return; } } @@ -415,43 +405,45 @@ class RichText extends Component { } const { start, end } = this.createRecord(); - const value = this.getRecord(); - - if ( start !== value.start || end !== value.end ) { - const { - __unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, - __unstableOnEnterFormattedText: onEnterFormattedText, - __unstableOnExitFormattedText: onExitFormattedText, - } = this.props; - const newValue = { - ...value, - start, - end, - // Allow `getActiveFormats` to get new `activeFormats`. - activeFormats: undefined, - }; + const value = this.record; - const activeFormats = getActiveFormats( newValue ); + if ( start === value.start && end === value.end ) { + return; + } - // Update the value with the new active formats. - newValue.activeFormats = activeFormats; + const { + __unstableIsCaretWithinFormattedText: isCaretWithinFormattedText, + __unstableOnEnterFormattedText: onEnterFormattedText, + __unstableOnExitFormattedText: onExitFormattedText, + } = this.props; + const newValue = { + ...value, + start, + end, + // Allow `getActiveFormats` to get new `activeFormats`. + activeFormats: undefined, + }; - if ( ! isCaretWithinFormattedText && activeFormats.length ) { - onEnterFormattedText(); - } else if ( isCaretWithinFormattedText && ! activeFormats.length ) { - onExitFormattedText(); - } + const activeFormats = getActiveFormats( newValue ); - // It is important that the internal value is updated first, - // otherwise the value will be wrong on render! - this.record = newValue; - this.applyRecord( newValue, { domOnly: true } ); - this.props.onSelectionChange( start, end ); - this.setState( { activeFormats } ); + // Update the value with the new active formats. + newValue.activeFormats = activeFormats; - if ( activeFormats.length > 0 ) { - this.recalculateBoundaryStyle(); - } + if ( ! isCaretWithinFormattedText && activeFormats.length ) { + onEnterFormattedText(); + } else if ( isCaretWithinFormattedText && ! activeFormats.length ) { + onExitFormattedText(); + } + + // It is important that the internal value is updated first, + // otherwise the value will be wrong on render! + this.record = newValue; + this.applyRecord( newValue, { domOnly: true } ); + this.props.onSelectionChange( start, end ); + this.setState( { activeFormats } ); + + if ( activeFormats.length > 0 ) { + this.recalculateBoundaryStyle(); } } @@ -516,112 +508,109 @@ class RichText extends Component { } /** - * Handles a delete keyDown event to handle merge or removal for collapsed - * selection where caret is at directional edge: forward for a delete key, - * reverse for a backspace key. - * - * @see https://en.wikipedia.org/wiki/Caret_navigation + * Handles delete on keydown: + * - outdent list items, + * - delete content if everything is selected, + * - trigger the onDelete prop when selection is uncollapsed and at an edge. * - * @param {KeyboardEvent} event Keydown event. + * @param {SyntheticEvent} event A synthetic keyboard event. */ - onDeleteKeyDown( event ) { - const { onDelete } = this.props; + handleDelete( event ) { + const { keyCode } = event; - if ( ! onDelete ) { + if ( keyCode !== DELETE && keyCode !== BACKSPACE ) { return; } - const { keyCode } = event; + const { onDelete, __unstableMultilineTag: multilineTag } = this.props; + const { activeFormats = [] } = this.state; + const value = this.createRecord(); + const { start, end, text } = value; const isReverse = keyCode === BACKSPACE; - const record = this.createRecord(); - // Only process delete if the key press occurs at uncollapsed edge. - if ( ! isCollapsed( record ) ) { - return; + if ( multilineTag ) { + const newValue = removeLineSeparator( value, isReverse ); + if ( newValue ) { + this.onChange( newValue ); + event.preventDefault(); + } } - // It is important to consider emptiness because an empty container - // will include a padding BR node _after_ the caret, so in a forward - // deletion the isHorizontalEdge function will incorrectly interpret the - // presence of the BR node as not being at the edge. - const isEdge = ( isEmpty( record ) || isHorizontalEdge( this.editableRef, isReverse ) ); + // Always handle full content deletion ourselves. + if ( start === 0 && end !== 0 && end === text.length ) { + this.onChange( remove( value ) ); + event.preventDefault(); + return; + } - if ( ! isEdge ) { + // Only process delete if the key press occurs at an uncollapsed edge. + if ( + ! onDelete || + ! isCollapsed( value ) || + activeFormats.length || + ( isReverse && start !== 0 ) || + ( ! isReverse && end !== text.length ) + ) { return; } + onDelete( { isReverse, value } ); event.preventDefault(); - - if ( onDelete ) { - onDelete( { isReverse, value: record } ); - } } /** - * Handles a keydown event. + * Triggers the `onEnter` prop on keydown. * * @param {SyntheticEvent} event A synthetic keyboard event. */ - onKeyDown( event ) { - const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; + handleEnter( event ) { + if ( event.keyCode !== ENTER ) { + return; + } + + event.preventDefault(); + const { onEnter } = this.props; - if ( - // Only override left and right keys without modifiers pressed. - ! shiftKey && ! altKey && ! metaKey && ! ctrlKey && - ( keyCode === LEFT || keyCode === RIGHT ) - ) { - this.handleHorizontalNavigation( event ); + if ( ! onEnter ) { + return; } - // Use the space key in list items (at the start of an item) to indent - // the list item. - if ( keyCode === SPACE && this.multilineTag === 'li' ) { - const value = this.createRecord(); + onEnter( { + value: this.removeEditorOnlyFormats( this.createRecord() ), + onChange: this.onChange, + shiftKey: event.shiftKey, + } ); + } - if ( isCollapsed( value ) ) { - const { text, start } = value; - const characterBefore = text[ start - 1 ]; + /** + * Indents list items on space keydown. + * + * @param {SyntheticEvent} event A synthetic keyboard event. + */ + handleSpace( event ) { + const { tagName, __unstableMultilineTag: multilineTag } = this.props; - // The caret must be at the start of a line. - if ( ! characterBefore || characterBefore === LINE_SEPARATOR ) { - this.onChange( indentListItems( value, { type: this.props.tagName } ) ); - event.preventDefault(); - } - } + if ( event.keyCode !== SPACE || multilineTag !== 'li' ) { + return; } - if ( keyCode === DELETE || keyCode === BACKSPACE ) { - const value = this.createRecord(); - const { start, end } = value; - - // Always handle full content deletion ourselves. - if ( start === 0 && end !== 0 && end === value.text.length ) { - this.onChange( remove( value ) ); - event.preventDefault(); - return; - } + const value = this.createRecord(); - if ( this.multilineTag ) { - const newValue = removeLineSeparator( value, keyCode === BACKSPACE ); - if ( newValue ) { - this.onChange( newValue ); - event.preventDefault(); - } - } + if ( ! isCollapsed( value ) ) { + return; + } - this.onDeleteKeyDown( event ); - } else if ( keyCode === ENTER ) { - event.preventDefault(); + const { text, start } = value; + const characterBefore = text[ start - 1 ]; - if ( onEnter ) { - onEnter( { - value: this.removeEditorOnlyFormats( this.createRecord() ), - onChange: this.onChange, - shiftKey: event.shiftKey, - } ); - } + // The caret must be at the start of a line. + if ( characterBefore && characterBefore !== LINE_SEPARATOR ) { + return; } + + this.onChange( indentListItems( value, { type: tagName } ) ); + event.preventDefault(); } /** @@ -632,7 +621,17 @@ class RichText extends Component { * @param {SyntheticEvent} event A synthetic keyboard event. */ handleHorizontalNavigation( event ) { - const value = this.getRecord(); + const { keyCode, shiftKey, altKey, metaKey, ctrlKey } = event; + + if ( + // Only override left and right keys without modifiers pressed. + shiftKey || altKey || metaKey || ctrlKey || + ( keyCode !== LEFT && keyCode !== RIGHT ) + ) { + return; + } + + const value = this.record; const { text, formats, start, end, activeFormats = [] } = value; const collapsed = isCollapsed( value ); // To do: ideally, we should look at visual position instead. @@ -706,7 +705,7 @@ class RichText extends Component { return; } - const newPos = value.start + ( isReverse ? -1 : 1 ); + const newPos = start + ( isReverse ? -1 : 1 ); const newActiveFormats = isReverse ? formatsBefore : formatsAfter; const newValue = { ...value, @@ -820,26 +819,30 @@ class RichText extends Component { * @return {Object} An internal rich-text value. */ formatToValue( value ) { - if ( this.props.format === 'string' ) { - const prepare = createPrepareEditableTree( this.props, 'format_value_functions' ); - - value = create( { - html: value, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, - } ); - value.formats = prepare( value ); + const { format, __unstableMultilineTag: multilineTag } = this.props; + if ( format !== 'string' ) { return value; } + const prepare = createPrepareEditableTree( this.props, 'format_value_functions' ); + + value = create( { + html: value, + multilineTag, + multilineWrapperTags: multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, + } ); + value.formats = prepare( value ); + return value; } valueToEditableHTML( value ) { + const { __unstableMultilineTag: multilineTag } = this.props; + return toDom( { value, - multilineTag: this.multilineTag, + multilineTag, prepareEditableTree: createPrepareEditableTree( this.props, 'format_prepare_functions' ), placeholder: this.props.placeholder, } ).body.innerHTML; @@ -872,106 +875,84 @@ class RichText extends Component { * @return {*} The external data format, data type depends on props. */ valueToFormat( value ) { + const { format, __unstableMultilineTag: multilineTag } = this.props; + value = this.removeEditorOnlyFormats( value ); - if ( this.props.format === 'string' ) { - return toHTMLString( { - value, - multilineTag: this.multilineTag, - } ); + if ( format !== 'string' ) { + return; } - return value; + return toHTMLString( { value, multilineTag } ); } - render() { + Editable( props ) { const { tagName: Tagname = 'div', style, - wrapperClassName, className, placeholder, - __unstableIsSelected: isSelected, - children, - // To do: move autocompletion logic to rich-text. - __unstableAutocompleters: autocompleters, - __unstableAutocomplete: Autocomplete = ( { children: ch } ) => ch( {} ), - __unstableOnReplace: onReplace, - allowedFormats, - withoutInteractiveFormatting, } = this.props; - // Generating a key that includes `tagName` ensures that if the tag // changes, we replace the relevant element. This is needed because we // prevent Editable component updates. const key = Tagname; - const ariaProps = pickAriaProps( this.props ); - const record = this.getRecord(); - const autoCompleteContent = ( { listBoxId, activeId } ) => ( + return ( + + ); + } + + render() { + const { + __unstableIsSelected: isSelected, + children, + allowedFormats, + withoutInteractiveFormatting, + } = this.props; + + return ( <> - { isSelected && } - - ); - - const content = ( - - { autoCompleteContent } - - ); - - if ( ! children ) { - return content; - } - - return ( -
- { children( { + { children && children( { isSelected, - value: record, + value: this.record, onChange: this.onChange, + Editable: this.Editable, } ) } - { content } -
+ { ! children && } + ); } } From 28919d1b73206aa9d67998815c2b166ecadf7d8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 14 Aug 2019 22:16:20 +0200 Subject: [PATCH 11/52] RichText (block context): move list controls to list block (#16962) * Move list controls * Don't restrict to isSelected --- .../src/components/rich-text/index.js | 12 +- .../src/components/rich-text/list-edit.js | 104 ------------------ packages/block-library/src/list/edit.js | 103 ++++++++++++++++- 3 files changed, 100 insertions(+), 119 deletions(-) delete mode 100644 packages/block-editor/src/components/rich-text/list-edit.js diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 344aa0d96edc1b..e765ba77291dca 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -36,7 +36,6 @@ import Autocomplete from '../autocomplete'; import BlockFormatControls from '../block-format-controls'; import FormatToolbar from './format-toolbar'; import { withBlockEditContext } from '../block-edit/context'; -import { ListEdit } from './list-edit'; import { RemoveBrowserShortcuts } from './remove-browser-shortcuts'; const wrapperClasses = 'editor-rich-text block-editor-rich-text'; @@ -296,6 +295,7 @@ class RichTextWrapper extends Component { render() { const { + children, tagName, value: originalValue, onChange: originalOnChange, @@ -303,7 +303,6 @@ class RichTextWrapper extends Component { selectionEnd, onSelectionChange, multiline, - onTagNameChange, inlineToolbar, wrapperClassName, className, @@ -376,14 +375,7 @@ class RichTextWrapper extends Component { > { ( { isSelected, value, onChange, Editable } ) => <> - { isSelected && multilineTag === 'li' && ( - - ) } + { children && children( { value, onChange } ) } { isSelected && ! inlineToolbar && hasFormats && ( diff --git a/packages/block-editor/src/components/rich-text/list-edit.js b/packages/block-editor/src/components/rich-text/list-edit.js deleted file mode 100644 index 762f24b8a10025..00000000000000 --- a/packages/block-editor/src/components/rich-text/list-edit.js +++ /dev/null @@ -1,104 +0,0 @@ -/** - * WordPress dependencies - */ - -import { Toolbar } from '@wordpress/components'; -import { __, _x } from '@wordpress/i18n'; -import { - __unstableIndentListItems as indentListItems, - __unstableOutdentListItems as outdentListItems, - __unstableChangeListType as changeListType, - __unstableIsListRootSelected as isListRootSelected, - __unstableIsActiveListType as isActiveListType, -} from '@wordpress/rich-text'; - -/** - * Internal dependencies - */ - -import { RichTextShortcut } from './shortcut'; -import BlockFormatControls from '../block-format-controls'; - -export const ListEdit = ( { - onTagNameChange, - tagName, - value, - onChange, -} ) => ( - <> - { - onChange( outdentListItems( value ) ); - } } - /> - { - onChange( indentListItems( value, { type: tagName } ) ); - } } - /> - { - onChange( indentListItems( value, { type: tagName } ) ); - } } - /> - { - onChange( outdentListItems( value ) ); - } } - /> - - { - onChange( outdentListItems( value ) ); - }, - }, - { - icon: 'editor-indent', - title: __( 'Indent list item' ), - shortcut: _x( 'Space', 'keyboard key' ), - onClick: () => { - onChange( indentListItems( value, { type: tagName } ) ); - }, - }, - ].filter( Boolean ) } - /> - - -); diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index 585aecd6aa7ab8..5c7e04a667af43 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -1,9 +1,17 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, _x } from '@wordpress/i18n'; import { createBlock } from '@wordpress/blocks'; -import { RichText } from '@wordpress/block-editor'; +import { RichText, BlockControls, RichTextShortcut } from '@wordpress/block-editor'; +import { Toolbar } from '@wordpress/components'; +import { + __unstableIndentListItems as indentListItems, + __unstableOutdentListItems as outdentListItems, + __unstableChangeListType as changeListType, + __unstableIsListRootSelected as isListRootSelected, + __unstableIsActiveListType as isActiveListType, +} from '@wordpress/rich-text'; /** * Internal dependencies @@ -18,12 +26,96 @@ export default function ListEdit( { className, } ) { const { ordered, values } = attributes; + const tagName = ordered ? 'ol' : 'ul'; + + const controls = ( { value, onChange } ) => { + if ( value.start === undefined ) { + return; + } + + return <> + { + onChange( outdentListItems( value ) ); + } } + /> + { + onChange( indentListItems( value, { type: tagName } ) ); + } } + /> + { + onChange( indentListItems( value, { type: tagName } ) ); + } } + /> + { + onChange( outdentListItems( value ) ); + } } + /> + + + + ; + }; return ( setAttributes( { values: nextValues } ) } value={ values } wrapperClassName="block-library-list" @@ -34,7 +126,8 @@ export default function ListEdit( { __unstableOnSplitMiddle={ () => createBlock( 'core/paragraph' ) } onReplace={ onReplace } onRemove={ () => onReplace( [] ) } - onTagNameChange={ ( tag ) => setAttributes( { ordered: tag === 'ol' } ) } - /> + > + { controls } + ); } From e07fb3cdbe6d803ebd5ccc2f624f99f1a928e56e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s?= Date: Thu, 15 Aug 2019 10:50:21 +0200 Subject: [PATCH 12/52] Update notifications (#17046) --- .github/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e2d185b76a068d..97359c3011bd3a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -46,7 +46,7 @@ /packages/library-export-default-webpack-plugin @gziolo @ntwb @nerrad @ajitbohra /packages/npm-package-json-lint-config @gziolo @ntwb @nerrad @ajitbohra /packages/postcss-themes @youknowriad @ntwb @nerrad @ajitbohra -/packages/scripts @youknowriad @gziolo @ntwb @nerrad @ajitbohra @nosolosw +/packages/scripts @youknowriad @gziolo @ntwb @nerrad @ajitbohra # UI Components /packages/components @youknowriad @gziolo @ajitbohra @jaymanpandya @jorgefilipecosta @talldan @chrisvanpatten From 43250cebf0fee36138ca03c7b0dec63b0470118b Mon Sep 17 00:00:00 2001 From: Jackie6 <541172791@qq.com> Date: Thu, 15 Aug 2019 04:42:16 -0700 Subject: [PATCH 13/52] List block: add start and reversed attributes (#15113) * Squash for easier merge: f6ad4f07e6a1c8d99e55aa7acb01cf8ef1dfddd7 (HEAD -> update/list-block, Jackie6/update/list-block) Add id to BaseControl 212cab106044c4d7300d9441e48241b911db0bcb Fix unit test and e2e test 421a198bf58431c91b4c09d6ee45ba932762cb15 Refine the type definition of start and reversed 4172707059558ba9fc47d9557457e0b8e441f050 Fix typo 21ed1dbeb7881a9a5a6eea85fb3293e9d6ba0d5a Update CSS styles 925f1ab1ed073b31358a129c62abd56021e14c38 Update list type style and remove description ef0aca95cb88203ceff78b81473e37ad8fc22714 Allow the start to be NaN 495166d3fc14591ceb88597915a19e976b2ebbeb Change to check strict equality 216ca51cfacfc174ca00d9669b88b53eaa6d0356 Fix the accidental change of travis yml 8e31259624ece930c385bf9cb7a28046505c7aff Change local state to attributes 09199ff9516aab099e47ed6e61fd4e2fadfdb95b Add list type, start, reversed settings * Revert adding attribute for now, let's extract to a separate PR * Use proper TextControl * Adjust toggle control to unset attribute * Clearer label --- .../src/components/rich-text/index.js | 5 ++ packages/block-library/src/list/block.json | 6 +++ packages/block-library/src/list/edit.js | 53 +++++++++++++++++-- packages/block-library/src/list/save.js | 10 +++- packages/rich-text/src/component/editable.js | 8 +++ 5 files changed, 75 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index e765ba77291dca..51af2159ec92f5 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -331,6 +331,9 @@ class RichTextWrapper extends Component { identifier, // eslint-disable-next-line no-unused-vars instanceId, + // To do: find a better way to implicitly inherit props. + start, + reversed, // From experimental filter. To do: pick props instead. ...experimentalProps } = this.props; @@ -400,6 +403,8 @@ class RichTextWrapper extends Component { aria-autocomplete={ listBoxId ? 'list' : undefined } aria-owns={ listBoxId } aria-activedescendant={ activeId } + start={ start } + reversed={ reversed } /> } diff --git a/packages/block-library/src/list/block.json b/packages/block-library/src/list/block.json index 26cdf8c19d6780..654bd3e281ccf5 100644 --- a/packages/block-library/src/list/block.json +++ b/packages/block-library/src/list/block.json @@ -12,6 +12,12 @@ "selector": "ol,ul", "multiline": "li", "default": "" + }, + "start": { + "type": "number" + }, + "reversed": { + "type": "boolean" } } } diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index 5c7e04a667af43..efc38c50a63e22 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -3,8 +3,18 @@ */ import { __, _x } from '@wordpress/i18n'; import { createBlock } from '@wordpress/blocks'; -import { RichText, BlockControls, RichTextShortcut } from '@wordpress/block-editor'; -import { Toolbar } from '@wordpress/components'; +import { + RichText, + BlockControls, + RichTextShortcut, + InspectorControls, +} from '@wordpress/block-editor'; +import { + Toolbar, + TextControl, + PanelBody, + ToggleControl, +} from '@wordpress/components'; import { __unstableIndentListItems as indentListItems, __unstableOutdentListItems as outdentListItems, @@ -25,7 +35,7 @@ export default function ListEdit( { onReplace, className, } ) { - const { ordered, values } = attributes; + const { ordered, values, reversed, start } = attributes; const tagName = ordered ? 'ol' : 'ul'; const controls = ( { value, onChange } ) => { @@ -111,7 +121,7 @@ export default function ListEdit( { ; }; - return ( + return <> createBlock( 'core/paragraph' ) } onReplace={ onReplace } onRemove={ () => onReplace( [] ) } + start={ start } + reversed={ reversed } > { controls } - ); + { ordered && + + + { + const int = parseInt( value, 10 ); + + setAttributes( { + // It should be possible to unset the value, + // e.g. with an empty string. + start: isNaN( int ) ? undefined : int, + } ); + } } + value={ Number.isInteger( start ) ? start.toString( 10 ) : '' } + step="1" + /> + { + setAttributes( { + // Unset the attribute if not reversed. + reversed: value || undefined, + } ); + } } + /> + + + } + ; } diff --git a/packages/block-library/src/list/save.js b/packages/block-library/src/list/save.js index 9b0ff55b44cd5d..18458c2c1ce5a5 100644 --- a/packages/block-library/src/list/save.js +++ b/packages/block-library/src/list/save.js @@ -4,10 +4,16 @@ import { RichText } from '@wordpress/block-editor'; export default function save( { attributes } ) { - const { ordered, values } = attributes; + const { ordered, values, reversed, start } = attributes; const tagName = ordered ? 'ol' : 'ul'; return ( - + ); } diff --git a/packages/rich-text/src/component/editable.js b/packages/rich-text/src/component/editable.js index c5407fc308582c..c4c7cb453acd84 100644 --- a/packages/rich-text/src/component/editable.js +++ b/packages/rich-text/src/component/editable.js @@ -116,6 +116,14 @@ export default class Editable extends Component { this.editorNode.className = nextProps.className; } + if ( this.props.start !== nextProps.start ) { + this.editorNode.setAttribute( 'start', nextProps.start ); + } + + if ( this.props.reversed !== nextProps.reversed ) { + this.editorNode.reversed = nextProps.reversed; + } + const { removedKeys, updatedKeys } = diffAriaProps( this.props, nextProps ); removedKeys.forEach( ( key ) => this.editorNode.removeAttribute( key ) ); From d83c23932f4ca9530ccb2c86057b3d0f52bc351c Mon Sep 17 00:00:00 2001 From: Dave Smith Date: Thu, 15 Aug 2019 16:06:26 +0100 Subject: [PATCH 14/52] Adds default funcs as props (#17036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously the component assumed that the onChange and onInput props would always be present. However, there are times where this may not be the case such as when the BlockPreview component uses the provider to render the previews. In such cases `onChange` is called but is undefined which causes an error to be thrown. To avoid this we provide lodash noop as defaults for both “change” props. --- packages/block-editor/src/components/provider/index.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 33eff22a08892b..488dd6244d8c65 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import { noop } from 'lodash'; + /** * WordPress dependencies */ @@ -87,8 +92,8 @@ class BlockEditorProvider extends Component { this.unsubscribe = registry.subscribe( () => { const { - onChange, - onInput, + onChange = noop, + onInput = noop, } = this.props; const newBlocks = getBlocks(); From 0ca1b7e1e16ba5b61d82a38756d6c88dfe9056f7 Mon Sep 17 00:00:00 2001 From: Benjamin Intal Date: Fri, 16 Aug 2019 06:55:30 +0800 Subject: [PATCH 15/52] Added ESNext examples to format API tutorial (page 1) (#16804) * Added ESNext examples * Added missing end tag --- .../tutorials/format-api/1-register-format.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/designers-developers/developers/tutorials/format-api/1-register-format.md b/docs/designers-developers/developers/tutorials/format-api/1-register-format.md index 6a18806f6a48a4..7a0f01e5fe0e9d 100644 --- a/docs/designers-developers/developers/tutorials/format-api/1-register-format.md +++ b/docs/designers-developers/developers/tutorials/format-api/1-register-format.md @@ -27,6 +27,8 @@ add_action( 'enqueue_block_editor_assets', 'my_custom_format_enqueue_assets_edit Then add a new file named `my-custom-format.js` with the following contents: +{% codetabs %} +{% ES5 %} ```js ( function( wp ) { wp.richText.registerFormatType( @@ -38,6 +40,19 @@ Then add a new file named `my-custom-format.js` with the following contents: ); } )( window.wp ); ``` +{% ESNext %} +```js +const { registerFormatType } = wp.richText; + +registerFormatType( + 'my-custom-format/sample-output', { + title: 'Sample output', + tagName: 'samp', + className: null, + } +); +``` +{% end %} Make that plugin available in your WordPress setup and activate it. Then, load a new page/post. From 0f3c147e1198947933e2905095ab7f2a94baabe7 Mon Sep 17 00:00:00 2001 From: Benjamin Intal Date: Fri, 16 Aug 2019 06:55:51 +0800 Subject: [PATCH 16/52] Added ESNext examples to format API tutorial (page 2) (#16803) * Added ESNext examples * Added missing end tags --- .../tutorials/format-api/2-toolbar-button.md | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md b/docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md index 52b07b6002581c..d6517b93ccc10d 100644 --- a/docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md +++ b/docs/designers-developers/developers/tutorials/format-api/2-toolbar-button.md @@ -4,6 +4,8 @@ Now that the format is available, the next step is to surface it to the UI. You Paste this code in `my-custom-format.js`: +{% codetabs %} +{% ES5 %} ```js ( function( wp ) { var MyCustomButton = function( props ) { @@ -27,6 +29,31 @@ Paste this code in `my-custom-format.js`: ); } )( window.wp ); ``` +{% ESNext %} +```js +const { registerFormatType } = wp.richText; +const { RichTextToolbarButton } = wp.blockEditor; + +const MyCustomButton = props => { + return { + console.log( 'toggle format' ); + } } + /> +}; + +registerFormatType( + 'my-custom-format/sample-output', { + title: 'Sample output', + tagName: 'samp', + className: null, + edit: MyCustomButton, + } +); +``` +{% end %} **Important**: note that this code is using two new utilities (`wp.element.createElement`, and `wp.editor.RichTextToolbarButton`) so don't forget adding the corresponding `wp-element` and `wp-editor` packages to the dependencies array in the PHP file along with the existing `wp-rich-text`. @@ -42,6 +69,8 @@ By default, the button is rendered on every rich text toolbar (image captions, b It is possible to render the button only on blocks of a certain type by using `wp.data.withSelect` together with `wp.compose.ifCondition`. The following sample code renders the previously shown button only on Paragraph blocks: +{% codetabs %} +{% ES5 %} ```js ( function( wp ) { var withSelect = wp.data.withSelect; @@ -82,6 +111,47 @@ The following sample code renders the previously shown button only on Paragraph ); } )( window.wp ); ``` +{% ESNext %} +```js +const { compose, ifCondition } = wp.compose; +const { registerFormatType } = wp.richText; +const { RichTextToolbarButton } = wp.blockEditor; +const { withSelect } = wp.data; + +const MyCustomButton = props => { + return { + console.log( 'toggle format' ); + } } + /> +}; + +const ConditionalButton = compose( + withSelect( function( select ) { + return { + selectedBlock: select( 'core/editor' ).getSelectedBlock() + } + } ), + ifCondition( function( props ) { + return ( + props.selectedBlock && + props.selectedBlock.name === 'core/paragraph' + ); + } ) +)( MyCustomButton ); + +registerFormatType( + 'my-custom-format/sample-output', { + title: 'Sample output', + tagName: 'samp', + className: null, + edit: ConditionalButton, + } +); +``` +{% end %} Don't forget adding `wp-compose` and `wp-data` to the dependencies array in the PHP script. From 2a7c8a676c6e70009d8c2c4c988fa94e467bb939 Mon Sep 17 00:00:00 2001 From: Benjamin Intal Date: Fri, 16 Aug 2019 06:56:04 +0800 Subject: [PATCH 17/52] Added ESNext examples to format API tutorial (page 3) (#16802) * Added ESNext examples * Added missing end tag * Use wp.blockEditor instead of wp.editor --- .../tutorials/format-api/3-apply-format.md | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/docs/designers-developers/developers/tutorials/format-api/3-apply-format.md b/docs/designers-developers/developers/tutorials/format-api/3-apply-format.md index 278f74143882ce..8b822b3a75845a 100644 --- a/docs/designers-developers/developers/tutorials/format-api/3-apply-format.md +++ b/docs/designers-developers/developers/tutorials/format-api/3-apply-format.md @@ -6,11 +6,13 @@ The [rich-text package](/packages/rich-text/README.md) offers a few utilities to Update `my-custom-format.js` with this new code: +{% codetabs %} +{% ES5 %} ```js ( function( wp ) { var MyCustomButton = function( props ) { return wp.element.createElement( - wp.editor.RichTextToolbarButton, { + wp.blockEditor.RichTextToolbarButton, { icon: 'editor-code', title: 'Sample output', onClick: function() { @@ -33,6 +35,35 @@ Update `my-custom-format.js` with this new code: ); } )( window.wp ); ``` +{% ESNext %} +```js +const { registerFormatType, toggleFormat } = wp.richText +const { RichTextToolbarButton } = wp.blockEditor; + +const MyCustomButton = props => { + return { + props.onChange( toggleFormat( + props.value, + { type: 'my-custom-format/sample-output' } + ) ); + } } + isActive={ props.isActive } + /> +}; + +registerFormatType( + 'my-custom-format/sample-output', { + title: 'Sample output', + tagName: 'samp', + className: null, + edit: MyCustomButton, + } +); +``` +{% end %} Now, let's check that is working as intended: reload the post/page, make a text selection, click the button, and then change to HTML view to confirm that the tag was effectively applied. From 0f3058e295cdfc8a874cfb5e78e1f8fae53a1278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Grzegorz=20=28Greg=29=20Zi=C3=B3=C5=82kowski?= Date: Fri, 16 Aug 2019 10:54:59 +0200 Subject: [PATCH 18/52] Scripts: Improve the way test files are discovered (#17033) * Scripts: Improve the way test files are discovered * Fix file matching patterns and file extensions for integration tests --- packages/jest-preset-default/CHANGELOG.md | 10 ++++++++++ packages/jest-preset-default/README.md | 2 +- packages/jest-preset-default/jest-preset.json | 6 +++--- packages/scripts/CHANGELOG.md | 4 ++++ packages/scripts/README.md | 11 +++++++++++ packages/scripts/config/jest-e2e.config.js | 5 ++--- ....spec.js.snap => blocks-raw-handling.test.js.snap} | 0 ...w-handling.spec.js => blocks-raw-handling.test.js} | 0 .../{full-content.spec.js => full-content.test.js} | 0 ...{is-valid-block.spec.js => is-valid-block.test.js} | 0 ...ling.spec.js => non-matched-tags-handling.test.js} | 0 ...-converter.spec.js => shortcode-converter.test.js} | 0 12 files changed, 31 insertions(+), 7 deletions(-) rename test/integration/__snapshots__/{blocks-raw-handling.spec.js.snap => blocks-raw-handling.test.js.snap} (100%) rename test/integration/{blocks-raw-handling.spec.js => blocks-raw-handling.test.js} (100%) rename test/integration/full-content/{full-content.spec.js => full-content.test.js} (100%) rename test/integration/{is-valid-block.spec.js => is-valid-block.test.js} (100%) rename test/integration/{non-matched-tags-handling.spec.js => non-matched-tags-handling.test.js} (100%) rename test/integration/{shortcode-converter.spec.js => shortcode-converter.test.js} (100%) diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index 0fd0e34112251b..3b0b30a79c8265 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -1,3 +1,13 @@ +## Master + +### Breaking Changes + +- Files with `.spec.js` suffix are no longer matched as test files by default. + +### New Features + +- Align `testMatch` config option with Jest and allow test files with `.ts` suffix. + ## 4.0.0 (2019-03-06) ### Breaking Changes diff --git a/packages/jest-preset-default/README.md b/packages/jest-preset-default/README.md index 52e7df8c715fde..ab5251a1098681 100644 --- a/packages/jest-preset-default/README.md +++ b/packages/jest-preset-default/README.md @@ -29,7 +29,7 @@ npm install @wordpress/jest-preset-default --save-dev * `setupFiles` - runs code before each test which sets up global variables required in the testing environment. * `setupFilesAfterEnv` - runs code which adds improved support for `Console` object and `React` components to the testing framework before each test. * `snapshotSerializers` - makes it possible to use snapshot tests on `Enzyme` wrappers. -* `testMatch`- includes `/test/` subfolder in the glob patterns Jest uses to detect test files. It detects only test files containing `.js` extension. +* `testMatch`- includes `/test/` subfolder in addition to the glob patterns Jest uses to detect test files. It detects only test files containing `.js` (or `.ts`) suffix. It doesn't match files with `.spec.js` suffix. * `timers` - use of [fake timers](https://jestjs.io/docs/en/timer-mocks.html) for functions such as `setTimeout` is enabled. * `transform` - keeps the default [babel-jest](https://github.com/facebook/jest/tree/master/packages/babel-jest) transformer. * `verbose` - each individual test won't be reported during the run. diff --git a/packages/jest-preset-default/jest-preset.json b/packages/jest-preset-default/jest-preset.json index be09fe7d6bb573..eb670e561b8497 100644 --- a/packages/jest-preset-default/jest-preset.json +++ b/packages/jest-preset-default/jest-preset.json @@ -15,9 +15,9 @@ "/node_modules/enzyme-to-json/serializer.js" ], "testMatch": [ - "**/__tests__/**/*.js", - "**/?(*.)(spec|test).js", - "**/test/*.js" + "**/__tests__/**/*.[jt]s", + "**/test/*.[jt]s", + "**/?(*.)test.[jt]s" ], "timers": "fake", "transform": { diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 7ccc3d4d75efa7..69166d2f0b3f33 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -1,5 +1,9 @@ ## Master +### Breaking Changes + +- Test files matching has changed to fix the overlap between two types of tests implemented with `test-e2e` and `test-unit`. Refer to the documentation of the corresponding scripts to learn about new file discovery rules. + ### New Features - The bundled `puppeteer` dependency has been updated from requiring `1.6.1` to requiring `^1.19.0` ([#16875](https://github.com/WordPress/gutenberg/pull/16875)). It uses Chromium v77 instead of Chromium v69. diff --git a/packages/scripts/README.md b/packages/scripts/README.md index fdbb75f9876cfd..50b95ff07c5e3e 100644 --- a/packages/scripts/README.md +++ b/packages/scripts/README.md @@ -252,6 +252,11 @@ This is how you execute those scripts using the presented setup: * `npm run test-e2e FILE_NAME -- --puppeteer-interactive ` - runs one test file interactively. * `npm run test-e2e:watch -- --puppeteer-interactive` - runs all tests interactively and watch for changes. +Jest will look for test files with any of the following popular naming conventions: + +- Files with `.js` (or `.ts`) suffix at any level of depth in `spec` folders. +- Files with `.spec.js` (or `.spec.ts`) suffix. + This script automatically detects the best config to start Puppeteer but sometimes you may need to specify custom options: - You can add a `jest-puppeteer.config.js` at the root of the project or define a custom path using `JEST_PUPPETEER_CONFIG` environment variable. Check [jest-puppeteer](https://github.com/smooth-code/jest-puppeteer#jest-puppeteerconfigjs) for more details. @@ -285,6 +290,12 @@ This is how you execute those scripts using the presented setup: * `npm run test:unit:help` - prints all available options to configure unit tests runner. * `npm run test:unit:watch` - runs all unit tests in the watch mode. +Jest will look for test files with any of the following popular naming conventions: + +- Files with `.js` (or `.ts`) suffix located at any level of depth in `__tests__` folders. +- Files with `.js` (or `.ts`) suffix directly located in `test` folders. +- Files with `.test.js` (or `.test.ts`) suffix. + #### Advanced information It uses [Jest](https://jestjs.io/) behind the scenes and you are able to use all of its [CLI options](https://jestjs.io/docs/en/cli.html). You can also run `./node_modules/.bin/wp-scripts test:unit --help` or `npm run test:unit:help` (as mentioned above) to view all of the available options. By default, it uses the set of recommended options defined in [@wordpress/jest-preset-default](https://www.npmjs.com/package/@wordpress/jest-preset-default) npm package. You can override them with your own options as described in [Jest documentation](https://jestjs.io/docs/en/configuration). Learn more in the [Advanced Usage](#advanced-usage) section. diff --git a/packages/scripts/config/jest-e2e.config.js b/packages/scripts/config/jest-e2e.config.js index d27b8d1b8cd2b0..45140772dc4c1c 100644 --- a/packages/scripts/config/jest-e2e.config.js +++ b/packages/scripts/config/jest-e2e.config.js @@ -11,9 +11,8 @@ const { hasBabelConfig } = require( '../utils' ); const jestE2EConfig = { preset: 'jest-puppeteer', testMatch: [ - '**/__tests__/**/*.js', - '**/?(*.)(spec|test).js', - '**/test/*.js', + '**/specs/**/*.[jt]s', + '**/?(*.)spec.[jt]s', ], }; diff --git a/test/integration/__snapshots__/blocks-raw-handling.spec.js.snap b/test/integration/__snapshots__/blocks-raw-handling.test.js.snap similarity index 100% rename from test/integration/__snapshots__/blocks-raw-handling.spec.js.snap rename to test/integration/__snapshots__/blocks-raw-handling.test.js.snap diff --git a/test/integration/blocks-raw-handling.spec.js b/test/integration/blocks-raw-handling.test.js similarity index 100% rename from test/integration/blocks-raw-handling.spec.js rename to test/integration/blocks-raw-handling.test.js diff --git a/test/integration/full-content/full-content.spec.js b/test/integration/full-content/full-content.test.js similarity index 100% rename from test/integration/full-content/full-content.spec.js rename to test/integration/full-content/full-content.test.js diff --git a/test/integration/is-valid-block.spec.js b/test/integration/is-valid-block.test.js similarity index 100% rename from test/integration/is-valid-block.spec.js rename to test/integration/is-valid-block.test.js diff --git a/test/integration/non-matched-tags-handling.spec.js b/test/integration/non-matched-tags-handling.test.js similarity index 100% rename from test/integration/non-matched-tags-handling.spec.js rename to test/integration/non-matched-tags-handling.test.js diff --git a/test/integration/shortcode-converter.spec.js b/test/integration/shortcode-converter.test.js similarity index 100% rename from test/integration/shortcode-converter.spec.js rename to test/integration/shortcode-converter.test.js From fbf19384325ede65c10b83560ad109a31178c421 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Fri, 16 Aug 2019 15:12:58 +0100 Subject: [PATCH 19/52] Fix: Button block does not centers on the editor (#17063) --- packages/block-library/src/button/edit.js | 4 ---- packages/block-library/src/button/editor.scss | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/button/edit.js b/packages/block-library/src/button/edit.js index e70cc00b278540..b258486b989203 100644 --- a/packages/block-library/src/button/edit.js +++ b/packages/block-library/src/button/edit.js @@ -33,10 +33,6 @@ import { const { getComputedStyle } = window; const applyFallbackStyles = withFallbackStyles( ( node, ownProps ) => { - if ( node ) { - node.classList.add( 'wp-block-button-wrapper' ); - } - const { textColor, backgroundColor } = ownProps; const backgroundColorValue = backgroundColor && backgroundColor.color; const textColorValue = textColor && textColor.color; diff --git a/packages/block-library/src/button/editor.scss b/packages/block-library/src/button/editor.scss index 3250c9d7dcfe00..5ccd04dd5b6254 100644 --- a/packages/block-library/src/button/editor.scss +++ b/packages/block-library/src/button/editor.scss @@ -1,6 +1,10 @@ .block-editor-block-list__block[data-type="core/button"] { &[data-align="center"] { text-align: center; + div[data-block] { + margin-left: auto; + margin-right: auto; + } } &[data-align="right"] { @@ -76,6 +80,7 @@ } } -.wp-block-button-wrapper { +// Display "table" is used because the button container should only wrap the content and not takes the full width. +div[data-type="core/button"] div[data-block] { display: table; } From 15f5ff44609df48a47dee1d3e750470cdda50a5e Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Fri, 16 Aug 2019 13:09:57 -0400 Subject: [PATCH 20/52] [RNMobile] Extract caption component (#16825) * Extract caption component * Use caption component for video * Move Caption to block-editor for mobile tests This move avoids a dependency issue that was causing the mobile tests to fail due to the Caption component's import of RichText from the block-editor package. * Pass onBlur to Caption component * Connect Caption component to store --- .../src/components/caption/index.native.js | 65 +++++++++++++++++++ .../src/components/index.native.js | 1 + .../block-library/src/image/edit.native.js | 49 ++++---------- .../block-library/src/video/edit.native.js | 57 +++++++++------- .../block-library/src/video/style.native.scss | 8 --- 5 files changed, 114 insertions(+), 66 deletions(-) create mode 100644 packages/block-editor/src/components/caption/index.native.js diff --git a/packages/block-editor/src/components/caption/index.native.js b/packages/block-editor/src/components/caption/index.native.js new file mode 100644 index 00000000000000..68e965524c8b87 --- /dev/null +++ b/packages/block-editor/src/components/caption/index.native.js @@ -0,0 +1,65 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { RichText } from '@wordpress/block-editor'; +import { compose } from '@wordpress/compose'; +import { withDispatch, withSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; + +const Caption = ( { accessible, accessibilityLabel, onBlur, onChange, onFocus, isSelected, shouldDisplay, text } ) => ( + + + +); + +export default compose( [ + withSelect( ( select, { accessibilityLabelCreator, clientId } ) => { + const { + getBlockAttributes, + getSelectedBlockClientId, + } = select( 'core/block-editor' ); + const { caption } = getBlockAttributes( clientId ); + const accessibilityLabel = accessibilityLabelCreator ? accessibilityLabelCreator( caption ) : undefined; + const isBlockSelected = getSelectedBlockClientId() === clientId; + + // We'll render the caption so that the soft keyboard is not forced to close on Android + // but still hide it by setting its display style to none. See wordpress-mobile/gutenberg-mobile#1221 + const shouldDisplay = ! RichText.isEmpty( caption ) > 0 || isBlockSelected; + + return { + accessibilityLabel, + shouldDisplay, + text: caption, + }; + } ), + withDispatch( ( dispatch, { clientId } ) => { + const { updateBlockAttributes } = dispatch( 'core/block-editor' ); + return { + onChange: ( caption ) => { + updateBlockAttributes( clientId, { caption } ); + }, + }; + } ), +] )( Caption ); diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index dee7b5fbdeddae..5f13de7d91b4ce 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -17,6 +17,7 @@ export { default as MediaPlaceholder } from './media-placeholder'; export { default as MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from './media-upload'; export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; +export { default as Caption } from './caption'; // Content Related Components export { default as BlockList } from './block-list'; diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 3928691b777aa0..3eb19a6bfa991b 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -21,10 +21,10 @@ import { ToolbarButton, } from '@wordpress/components'; import { + Caption, MediaPlaceholder, MediaUpload, MEDIA_TYPE_IMAGE, - RichText, BlockControls, InspectorControls, } from '@wordpress/block-editor'; @@ -115,7 +115,6 @@ class ImageEdit extends React.Component { requestImageFailedRetryDialog( attributes.id ); } - this._caption.blur(); this.setState( { isCaptionSelected: false, } ); @@ -201,8 +200,8 @@ class ImageEdit extends React.Component { } render() { - const { attributes, isSelected, setAttributes } = this.props; - const { url, caption, height, width, alt, href, id } = attributes; + const { attributes, isSelected } = this.props; + const { url, height, width, alt, href, id } = attributes; const onImageSettingsButtonPressed = () => { this.setState( { showSettings: true } ); @@ -270,12 +269,6 @@ class ImageEdit extends React.Component { ); } - // We still want to render the caption so that the soft keyboard is not forced to close on Android - const shouldCaptionDisplay = () => { - const isCaptionEmpty = RichText.isEmpty( caption ) > 0; - return ! isCaptionEmpty || isSelected; - }; - const imageContainerHeight = Dimensions.get( 'window' ).width / IMAGE_ASPECT_RATIO; const getImageComponent = ( openMediaOptions, getMediaOptions ) => ( - isEmpty( caption ) ? /* translators: accessibility text. Empty image caption. */ - __( 'Image caption. Empty' ) : + ( 'Image caption. Empty' ) : sprintf( - /* translators: accessibility text. %s: image caption. */ + /* translators: accessibility text. %s: image caption. */ __( 'Image caption. %s' ), - caption - ) + caption ) } - accessibilityRole={ 'button' } - > - { - this._caption = ref; - } } - rootTagsToEliminate={ [ 'p' ] } - placeholder={ __( 'Write caption…' ) } - value={ caption } - onChange={ ( newCaption ) => setAttributes( { caption: newCaption } ) } - unstableOnFocus={ this.onFocusCaption } - onBlur={ this.props.onBlur } // always assign onBlur as props - isSelected={ this.state.isCaptionSelected } - __unstableMobileNoFocusOnMount - fontSize={ 14 } - underlineColorAndroid="transparent" - textAlign={ 'center' } - tagName={ '' } - /> - + onFocus={ this.onFocusCaption } + onBlur={ this.props.onBlur } // always assign onBlur as props + /> ); diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 4d7ce426d55c05..3b0a9d186d3084 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -2,7 +2,7 @@ * External dependencies */ import React from 'react'; -import { View, TextInput, TouchableWithoutFeedback, Text } from 'react-native'; +import { View, TouchableWithoutFeedback, Text } from 'react-native'; /** * Internal dependencies */ @@ -22,10 +22,10 @@ import { ToolbarButton, } from '@wordpress/components'; import { + Caption, MediaPlaceholder, MediaUpload, MEDIA_TYPE_VIDEO, - RichText, BlockControls, InspectorControls, } from '@wordpress/block-editor'; @@ -48,6 +48,7 @@ class VideoEdit extends React.Component { super( props ); this.state = { + isCaptionSelected: false, showSettings: false, videoContainerHeight: 0, }; @@ -59,6 +60,7 @@ class VideoEdit extends React.Component { this.updateMediaProgress = this.updateMediaProgress.bind( this ); this.onVideoPressed = this.onVideoPressed.bind( this ); this.onVideoContanerLayout = this.onVideoContanerLayout.bind( this ); + this.onFocusCaption = this.onFocusCaption.bind( this ); } componentDidMount() { @@ -75,6 +77,14 @@ class VideoEdit extends React.Component { } } + static getDerivedStateFromProps( props, state ) { + // Avoid a UI flicker in the toolbar by insuring that isCaptionSelected + // is updated immediately any time the isSelected prop becomes false + return { + isCaptionSelected: props.isSelected && state.isCaptionSelected, + }; + } + onVideoPressed() { const { attributes } = this.props; @@ -83,6 +93,16 @@ class VideoEdit extends React.Component { } else if ( attributes.id && ! isURL( attributes.src ) ) { requestImageFailedRetryDialog( attributes.id ); } + + this.setState( { + isCaptionSelected: false, + } ); + } + + onFocusCaption() { + if ( ! this.state.isCaptionSelected ) { + this.setState( { isCaptionSelected: true } ); + } } updateMediaProgress( payload ) { @@ -135,8 +155,8 @@ class VideoEdit extends React.Component { } render() { - const { attributes, isSelected, setAttributes } = this.props; - const { caption, id, src } = attributes; + const { attributes, isSelected } = this.props; + const { id, src } = attributes; const { videoContainerHeight } = this.state; const toolbarEditButton = ( @@ -173,9 +193,10 @@ class VideoEdit extends React.Component { return ( - - { toolbarEditButton } - + { ! this.state.isCaptionSelected && + + { toolbarEditButton } + } { false && ); diff --git a/packages/block-library/src/video/style.native.scss b/packages/block-library/src/video/style.native.scss index a7b77efce2c4bc..dd4d70ae0a4752 100644 --- a/packages/block-library/src/video/style.native.scss +++ b/packages/block-library/src/video/style.native.scss @@ -53,14 +53,6 @@ border-style: solid; } -.caption-text { - font-family: $default-regular-font; -} - -.captionPlaceholder { - color: $gray; -} - .icon { fill: $gray-dark; width: 100%; From d80b024c7b8a759905cde40f7e7841f7732747c2 Mon Sep 17 00:00:00 2001 From: Michael Joseph Panaga Date: Sat, 17 Aug 2019 18:01:52 +0800 Subject: [PATCH 21/52] Update: Latest Posts Block: Use (no title) instead of (Untitled) for untitled posts (#17074) --- packages/block-library/src/latest-posts/edit.js | 2 +- packages/block-library/src/latest-posts/index.php | 2 +- packages/block-library/src/rss/index.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/block-library/src/latest-posts/edit.js b/packages/block-library/src/latest-posts/edit.js index 0b6714c6fff087..aa2111a98a101d 100644 --- a/packages/block-library/src/latest-posts/edit.js +++ b/packages/block-library/src/latest-posts/edit.js @@ -206,7 +206,7 @@ class LatestPostsEdit extends Component { { titleTrimmed } ) : - __( '(Untitled)' ) + __( '(no title)' ) } { displayPostDate && post.date_gmt && diff --git a/packages/block-library/src/latest-posts/index.php b/packages/block-library/src/latest-posts/index.php index 81278794f31749..21f4f335842ddf 100644 --- a/packages/block-library/src/latest-posts/index.php +++ b/packages/block-library/src/latest-posts/index.php @@ -34,7 +34,7 @@ function render_block_core_latest_posts( $attributes ) { foreach ( $recent_posts as $post ) { $title = get_the_title( $post ); if ( ! $title ) { - $title = __( '(Untitled)' ); + $title = __( '(no title)' ); } $list_items_markup .= sprintf( '
  • %2$s', diff --git a/packages/block-library/src/rss/index.php b/packages/block-library/src/rss/index.php index 53879d5bb2716b..0300585de24f02 100644 --- a/packages/block-library/src/rss/index.php +++ b/packages/block-library/src/rss/index.php @@ -32,7 +32,7 @@ function render_block_core_rss( $attributes ) { foreach ( $rss_items as $item ) { $title = esc_html( trim( strip_tags( $item->get_title() ) ) ); if ( empty( $title ) ) { - $title = __( '(Untitled)' ); + $title = __( '(no title)' ); } $link = $item->get_link(); $link = esc_url( $link ); From c819d0e0cb8e6a99e3d435150d5572a9edf17431 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 19 Aug 2019 12:28:07 +0100 Subject: [PATCH 22/52] Remove borders around inserter items for blocks with children blocks (#17083) --- .../src/components/block-switcher/index.js | 3 +-- .../src/components/block-types-list/index.js | 3 --- .../components/inserter-list-item/index.js | 16 +---------- .../components/inserter-list-item/style.scss | 27 ------------------- packages/block-editor/src/store/selectors.js | 2 -- .../block-editor/src/store/test/selectors.js | 1 - 6 files changed, 2 insertions(+), 50 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index d6a9f1fe135503..9fe7c5c9db5cbc 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -8,7 +8,7 @@ import { castArray, filter, first, mapKeys, orderBy, uniq, map } from 'lodash'; */ import { __, _n, sprintf } from '@wordpress/i18n'; import { Dropdown, IconButton, Toolbar, PanelBody, Path, SVG } from '@wordpress/components'; -import { getBlockType, getPossibleBlockTransformations, switchToBlockType, hasChildBlocksWithInserterSupport, cloneBlock } from '@wordpress/blocks'; +import { getBlockType, getPossibleBlockTransformations, switchToBlockType, cloneBlock } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; import { DOWN } from '@wordpress/keycodes'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -151,7 +151,6 @@ export class BlockSwitcher extends Component { id: destinationBlockType.name, icon: destinationBlockType.icon, title: destinationBlockType.title, - hasChildBlocksWithInserterSupport: hasChildBlocksWithInserterSupport( destinationBlockType.name ), } ) ) } onSelect={ ( item ) => { onTransform( blocks, item.id ); diff --git a/packages/block-editor/src/components/block-types-list/index.js b/packages/block-editor/src/components/block-types-list/index.js index d2639e80f2f3c9..8c2de6772dd8ef 100644 --- a/packages/block-editor/src/components/block-types-list/index.js +++ b/packages/block-editor/src/components/block-types-list/index.js @@ -21,9 +21,6 @@ function BlockTypesList( { items, onSelect, onHover = () => {}, children } ) { key={ item.id } className={ getBlockMenuDefaultClassName( item.id ) } icon={ item.icon } - hasChildBlocksWithInserterSupport={ - item.hasChildBlocksWithInserterSupport - } onClick={ () => { onSelect( item ); onHover( null ); diff --git a/packages/block-editor/src/components/inserter-list-item/index.js b/packages/block-editor/src/components/inserter-list-item/index.js index 5e995c4d7a7a2a..a4af815ac36446 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -10,7 +10,6 @@ import BlockIcon from '../block-icon'; function InserterListItem( { icon, - hasChildBlocksWithInserterSupport, onClick, isDisabled, title, @@ -21,9 +20,6 @@ function InserterListItem( { backgroundColor: icon.background, color: icon.foreground, } : {}; - const itemIconStackStyle = icon && icon.shadowColor ? { - backgroundColor: icon.shadowColor, - } : {}; return (
  • @@ -31,11 +27,7 @@ function InserterListItem( { className={ classnames( 'editor-block-types-list__item block-editor-block-types-list__item', - className, - { - 'editor-block-types-list__item-has-children block-editor-block-types-list__item-has-children': - hasChildBlocksWithInserterSupport, - } + className ) } onClick={ ( event ) => { @@ -50,12 +42,6 @@ function InserterListItem( { style={ itemIconStyle } > - { hasChildBlocksWithInserterSupport && - - } { title } 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 cf6bc1ba697a17..d99f818f6f0a40 100644 --- a/packages/block-editor/src/components/inserter-list-item/style.scss +++ b/packages/block-editor/src/components/inserter-list-item/style.scss @@ -92,30 +92,3 @@ .block-editor-block-types-list__item-title { padding: 4px 2px 8px; } - -.block-editor-block-types-list__item-has-children { - .block-editor-block-types-list__item-icon { - background: $white; - margin-right: 3px; - margin-bottom: 6px; - padding: 9px 20px 9px; - position: relative; - top: -2px; - left: -2px; - box-shadow: 0 0 0 1px $light-gray-500; - } - - // Show a "stacked card" below an item that has children. - .block-editor-block-types-list__item-icon-stack { - display: block; - background: $white; - box-shadow: 0 0 0 1px $light-gray-500; - width: 100%; - height: 100%; - position: absolute; - z-index: -1; // Show below the card as a shadow - bottom: -6px; - right: -6px; - border-radius: 4px; - } -} diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 462cf8cda1b8c9..792b27542f7a5b 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -24,7 +24,6 @@ import { getBlockType, getBlockTypes, hasBlockSupport, - hasChildBlocksWithInserterSupport, } from '@wordpress/blocks'; // Module constants @@ -1241,7 +1240,6 @@ export const getInserterItems = createSelector( isDisabled, utility: calculateUtility( blockType.category, count, isContextual ), frecency: calculateFrecency( time, count ), - hasChildBlocksWithInserterSupport: hasChildBlocksWithInserterSupport( blockType.name ), }; }; diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 4e1289e44aefc3..1e7dfe7f2b6f44 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -1971,7 +1971,6 @@ describe( 'selectors', () => { isDisabled: false, utility: 0, frecency: 0, - hasChildBlocksWithInserterSupport: false, } ); const reusableBlockItem = items.find( ( item ) => item.id === 'core/block/1' ); expect( reusableBlockItem ).toEqual( { From df7afa8b550515fbf7f85e7816bebabf7b2438c8 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Mon, 19 Aug 2019 17:35:41 +0100 Subject: [PATCH 23/52] Add a help panel to the inserter available in all blocks (#16813) --- packages/block-editor/README.md | 1 + .../src/components/block-card/index.js | 18 ++ .../src/components/block-card/style.scss | 34 +++ .../src/components/block-inspector/index.js | 10 +- .../src/components/block-inspector/style.scss | 38 ---- .../src/components/block-preview/index.js | 18 +- .../src/components/block-preview/style.scss | 41 +--- .../components/inserter-list-item/style.scss | 4 +- .../src/components/inserter/menu.js | 215 +++++++++++------- .../src/components/inserter/style.scss | 79 ++++++- packages/block-editor/src/store/defaults.js | 2 + packages/block-editor/src/style.scss | 1 + packages/components/src/index.js | 1 + packages/components/src/style.scss | 1 + packages/components/src/tip/index.js | 17 ++ packages/components/src/tip/style.scss | 15 ++ packages/e2e-tests/specs/editor-modes.test.js | 2 +- .../src/components/options-modal/index.js | 2 + .../options-modal/options/enable-feature.js | 24 ++ .../components/options-modal/options/index.js | 1 + .../test/__snapshots__/index.js.snap | 4 + packages/edit-post/src/editor.js | 5 + packages/edit-post/src/store/defaults.js | 1 + .../editor/src/components/provider/index.js | 1 + 24 files changed, 345 insertions(+), 190 deletions(-) create mode 100644 packages/block-editor/src/components/block-card/index.js create mode 100644 packages/block-editor/src/components/block-card/style.scss create mode 100644 packages/components/src/tip/index.js create mode 100644 packages/components/src/tip/style.scss create mode 100644 packages/edit-post/src/components/options-modal/options/enable-feature.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 1d230d45fc3a23..a5a5ed24f0014f 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -387,6 +387,7 @@ The default editor settings bodyPlaceholder string Empty post placeholder titlePlaceholder string Empty title placeholder codeEditingEnabled string Whether or not the user can switch to the code editor + showInserterHelpPanel boolean Whether or not the inserter help panel is shown **experimentalCanUserUseUnfilteredHTML string Whether the user should be able to use unfiltered HTML or the HTML should be filtered e.g., to remove elements considered insecure like iframes. **experimentalEnableLegacyWidgetBlock boolean Whether the user has enabled the Legacy Widget Block \_\_experimentalEnableMenuBlock boolean Whether the user has enabled the Menu Block diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js new file mode 100644 index 00000000000000..f060612622804e --- /dev/null +++ b/packages/block-editor/src/components/block-card/index.js @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import BlockIcon from '../block-icon'; + +function BlockCard( { blockType } ) { + return ( +
    + +
    +
    { blockType.title }
    +
    { blockType.description }
    +
    +
    + ); +} + +export default BlockCard; diff --git a/packages/block-editor/src/components/block-card/style.scss b/packages/block-editor/src/components/block-card/style.scss new file mode 100644 index 00000000000000..6ac54020dd80a3 --- /dev/null +++ b/packages/block-editor/src/components/block-card/style.scss @@ -0,0 +1,34 @@ + +.block-editor-block-card { + display: flex; + align-items: flex-start; +} + +.block-editor-block-card__icon { + border: $border-width solid $light-gray-700; + padding: 7px; + margin-right: 10px; + height: 36px; + width: 36px; +} + +.block-editor-block-card__content { + flex-grow: 1; +} + +.block-editor-block-card__title { + font-weight: 500; + margin-bottom: 5px; +} + +.block-editor-block-card__description { + font-size: $default-font-size; +} + +.block-editor-block-card .block-editor-block-icon { + margin-left: -2px; + margin-right: 10px; + padding: 0 3px; + width: $icon-button-size; + height: $icon-button-size-small; +} diff --git a/packages/block-editor/src/components/block-inspector/index.js b/packages/block-editor/src/components/block-inspector/index.js index e04dbcbd932be1..ea18d7838cca4f 100644 --- a/packages/block-editor/src/components/block-inspector/index.js +++ b/packages/block-editor/src/components/block-inspector/index.js @@ -15,7 +15,7 @@ import { withSelect } from '@wordpress/data'; * Internal dependencies */ import SkipToSelectedBlock from '../skip-to-selected-block'; -import BlockIcon from '../block-icon'; +import BlockCard from '../block-card'; import InspectorControls from '../inspector-controls'; import InspectorAdvancedControls from '../inspector-advanced-controls'; import BlockStyles from '../block-styles'; @@ -51,13 +51,7 @@ const BlockInspector = ( { return ( <> -
    - -
    -
    { blockType.title }
    -
    { blockType.description }
    -
    -
    + { hasBlockStyles && (
    scaledElementRect.height * scale ) ? ( containerElementRect.height - ( scaledElementRect.height * scale ) ) / 2 : 0; - setIsTallPreview( scaledElementRect.height * scale > containerElementRect.height ); setPreviewScale( scale ); setPosition( { x: offsetX * scale, y: offsetY } ); @@ -60,7 +58,6 @@ function ScaledBlockPreview( { blocks, viewportWidth } ) { } else { const containerElementRect = containerElement.getBoundingClientRect(); setPreviewScale( containerElementRect.width / viewportWidth ); - setIsTallPreview( true ); } setIsReady( true ); @@ -86,14 +83,15 @@ function ScaledBlockPreview( { blocks, viewportWidth } ) { width: viewportWidth, }; - const contentClassNames = classnames( 'block-editor-block-preview__content editor-styles-wrapper', { - 'is-tall-preview': isTallPreview, - 'is-ready': isReady, - } ); - return ( -
    - +
    +
    diff --git a/packages/block-editor/src/components/block-preview/style.scss b/packages/block-editor/src/components/block-preview/style.scss index 76c2f4ccaf10a5..2316de61a8355f 100644 --- a/packages/block-editor/src/components/block-preview/style.scss +++ b/packages/block-editor/src/components/block-preview/style.scss @@ -1,35 +1,3 @@ -// This is the preview that shows up to the right of the thumbnail when hovering. -.block-editor-block-switcher__preview { - padding: $block-padding; - font-family: $editor-font; - overflow: hidden; - width: 100%; - pointer-events: none; - display: none; - - @include break-medium { - display: block; - } - - .block-editor-block-preview__content { - font-family: $editor-font; - - > div { - font-family: $editor-font; - } - - &:not(.is-tall-preview) { - // Vertical alignment. - margin-top: 50%; - } - } - - .block-editor-block-preview__title { - margin-bottom: 10px; - color: $dark-gray-300; - } -} - // These rules ensure the preview scales smoothly regardless of the container size. .block-editor-block-preview__container { // In the component, a top padding is provided as an inline style to provid an aspect-ratio. @@ -39,6 +7,11 @@ // The preview component measures the pixel width of this item, so as to calculate the scale factor. // But without this baseline width, it collapses to 0. width: 100%; + + overflow: hidden; + &.is-ready { + overflow: visible; + } } .block-editor-block-preview__content { @@ -74,10 +47,6 @@ height: auto; } - &.is-tall-preview { - top: 4px; - } - .block-editor-block-list__insertion-point, .block-editor-block-drop-zone, .reusable-block-indicator, 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 d99f818f6f0a40..8dc8b8084ad4f9 100644 --- a/packages/block-editor/src/components/inserter-list-item/style.scss +++ b/packages/block-editor/src/components/inserter-list-item/style.scss @@ -1,7 +1,7 @@ .block-editor-block-types-list__list-item { display: block; width: 33.33%; - padding: 0 4px; + padding: 0; margin: 0 0 12px; } @@ -11,7 +11,7 @@ width: 100%; font-size: $default-font-size; color: $dark-gray-700; - padding: 0; + padding: 0 4px; align-items: stretch; justify-content: center; cursor: pointer; diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 0b5f47c68af256..6714a27f6c69d2 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -16,18 +16,24 @@ import { deburr, } from 'lodash'; import scrollIntoView from 'dom-scroll-into-view'; +import classnames from 'classnames'; /** * WordPress dependencies */ import { __, _n, _x, sprintf } from '@wordpress/i18n'; import { Component, createRef } from '@wordpress/element'; -import { withSpokenMessages, PanelBody } from '@wordpress/components'; +import { + PanelBody, + withSpokenMessages, + Tip, +} from '@wordpress/components'; import { getCategories, isReusableBlock, createBlock, isUnmodifiedDefaultBlock, + getBlockType, } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { withInstanceId, compose, withSafeTimeout } from '@wordpress/compose'; @@ -39,6 +45,7 @@ import { addQueryArgs } from '@wordpress/url'; */ import BlockPreview from '../block-preview'; import BlockTypesList from '../block-types-list'; +import BlockCard from '../block-card'; import ChildBlocks from './child-blocks'; const MAX_SUGGESTED_ITEMS = 9; @@ -246,7 +253,7 @@ export class InserterMenu extends Component { } render() { - const { instanceId, onSelect, rootClientId } = this.props; + const { instanceId, onSelect, rootClientId, showInserterHelpPanel } = this.props; const { childItems, hoveredItem, @@ -265,99 +272,139 @@ export class InserterMenu extends Component { /* eslint-disable jsx-a11y/no-autofocus, jsx-a11y/no-static-element-interactions */ return (
    - - - -
    - - + + - { !! suggestedItems.length && - - - - } - - { map( getCategories(), ( category ) => { - const categoryItems = itemsPerCategory[ category.slug ]; - if ( ! categoryItems || ! categoryItems.length ) { - return null; - } - return ( +
    + + + + { !! suggestedItems.length && - + - ); - } ) } - - { !! reusableItems.length && ( - - - { + const categoryItems = itemsPerCategory[ category.slug ]; + if ( ! categoryItems || ! categoryItems.length ) { + return null; + } + return ( + + + + ); + } ) } + + { !! reusableItems.length && ( + - { __( 'Manage All Reusable Blocks' ) } - - - ) } - { isEmpty( suggestedItems ) && isEmpty( reusableItems ) && isEmpty( itemsPerCategory ) && ( -

    { __( 'No blocks found.' ) }

    - ) } + + + { __( 'Manage All Reusable Blocks' ) } + + + ) } + { isEmpty( suggestedItems ) && isEmpty( reusableItems ) && isEmpty( itemsPerCategory ) && ( +

    { __( 'No blocks found.' ) }

    + ) } +
    - { hoveredItem && isReusableBlock( hoveredItem ) && -
    -
    { __( 'Preview' ) }
    - + { showInserterHelpPanel && ( +
    + { hoveredItem && ( + <> + + { isReusableBlock( hoveredItem ) && ( +
    +
    { __( 'Preview' ) }
    + +
    + ) } + + ) } + { ! hoveredItem && ( +
    +
    +
    Content Blocks
    +

    + { __( + 'Welcome to the wonderful world of blocks! Blocks are the basis of all content within the editor. ' + ) } +

    +

    + { __( + 'There are blocks available for all kinds of content: insert text, headings, images, lists, videos, tables, and lots more.' + ) } +

    +

    + { __( + 'Browse through the library to learn more about what each block does.' + ) } +

    +
    + + { __( + 'While writing, you can press "/" to quickly insert new blocks.' + ) } + +
    + ) }
    - } + ) }
    ); /* eslint-enable jsx-a11y/no-autofocus, jsx-a11y/no-static-element-interactions */ @@ -371,6 +418,7 @@ export default compose( getBlockName, getBlockRootClientId, getBlockSelectionEnd, + getSettings, } = select( 'core/block-editor' ); const { getChildBlockNames, @@ -388,6 +436,7 @@ export default compose( return { rootChildBlocks: getChildBlockNames( destinationRootBlockName ), items: getInserterItems( destinationRootClientId ), + showInserterHelpPanel: getSettings().showInserterHelpPanel, destinationRootClientId, }; } ), diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 4cc764ed0861a6..a415e13ccb547c 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -36,6 +36,21 @@ $block-inserter-search-height: 38px; } .block-editor-inserter__menu { + height: 100%; + display: flex; + width: auto; + + @include break-medium { + width: 400px; + position: relative; + + &.has-help-panel { + width: 700px; + } + } +} + +.block-editor-inserter__main-area { width: auto; display: flex; flex-direction: column; @@ -130,25 +145,65 @@ $block-inserter-search-height: 38px; } } -.block-editor-inserter__preview { - border: $border-width solid $light-gray-500; - box-shadow: $shadow-popover; - background: $white; - position: absolute; - left: 100%; - top: -1px; - bottom: -1px; - width: 300px; - height: auto; - padding: 10px; +.block-editor-inserter__menu-help-panel { display: none; + border-left: $border-width solid $light-gray-500; + width: 300px; + height: 100%; + padding: 20px; + overflow-y: auto; @include break-medium { display: block; } + + .block-editor-block-card { + padding-bottom: 20px; + margin-bottom: 10px; + border-bottom: $border-width solid $light-gray-500; + @include edit-post__fade-in-animation(); + } } .block-editor-inserter__preview-title { margin-bottom: 10px; - color: $dark-gray-300; +} + +.block-editor-inserter__menu-help-panel-no-block { + display: flex; + height: 100%; + flex-direction: column; + @include edit-post__fade-in-animation(); + + .block-editor-inserter__menu-help-panel-no-block-text { + flex-grow: 1; + + h4 { + font-size: $big-font-size; + } + } + + .components-notice { + margin: 0; + } + + h4 { + margin-top: 0; + } +} + +.block-editor-inserter__menu-help-panel-hover-area { + flex-grow: 1; + margin-top: 20px; + padding: 20px; + border: 1px dotted $light-gray-500; + display: flex; + align-items: center; + text-align: center; +} + +.block-editor-inserter__menu-help-panel-title { + font-size: $big-font-size; + font-weight: 600; + margin-bottom: 20px; } diff --git a/packages/block-editor/src/store/defaults.js b/packages/block-editor/src/store/defaults.js index 885a8b79f7fb06..2e7cb1f59cb78e 100644 --- a/packages/block-editor/src/store/defaults.js +++ b/packages/block-editor/src/store/defaults.js @@ -27,6 +27,7 @@ export const PREFERENCES_DEFAULTS = { * bodyPlaceholder string Empty post placeholder * titlePlaceholder string Empty title placeholder * codeEditingEnabled string Whether or not the user can switch to the code editor + * showInserterHelpPanel boolean Whether or not the inserter help panel is shown * __experimentalCanUserUseUnfilteredHTML string Whether the user should be able to use unfiltered HTML or the HTML should be filtered e.g., to remove elements considered insecure like iframes. * __experimentalEnableLegacyWidgetBlock boolean Whether the user has enabled the Legacy Widget Block * __experimentalEnableMenuBlock boolean Whether the user has enabled the Menu Block @@ -145,6 +146,7 @@ export const SETTINGS_DEFAULTS = { availableLegacyWidgets: {}, hasPermissionsToManageWidgets: false, + showInserterHelpPanel: true, __experimentalCanUserUseUnfilteredHTML: false, __experimentalEnableLegacyWidgetBlock: false, __experimentalEnableMenuBlock: false, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 8083cf0cbc6bf5..644083ea80a810 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -3,6 +3,7 @@ @import "./components/block-inspector/style.scss"; @import "./components/block-list/style.scss"; @import "./components/block-list-appender/style.scss"; +@import "./components/block-card/style.scss"; @import "./components/block-compare/style.scss"; @import "./components/block-mover/style.scss"; @import "./components/block-navigation/style.scss"; diff --git a/packages/components/src/index.js b/packages/components/src/index.js index 3ab485312f848b..5ce67b968084ea 100644 --- a/packages/components/src/index.js +++ b/packages/components/src/index.js @@ -55,6 +55,7 @@ export { default as Spinner } from './spinner'; export { default as TabPanel } from './tab-panel'; export { default as TextControl } from './text-control'; export { default as TextareaControl } from './textarea-control'; +export { default as Tip } from './tip'; export { default as ToggleControl } from './toggle-control'; export { default as Toolbar } from './toolbar'; export { default as ToolbarButton } from './toolbar-button'; diff --git a/packages/components/src/style.scss b/packages/components/src/style.scss index 39c566be5ad526..39b026111065df 100644 --- a/packages/components/src/style.scss +++ b/packages/components/src/style.scss @@ -39,6 +39,7 @@ @import "./spinner/style.scss"; @import "./text-control/style.scss"; @import "./textarea-control/style.scss"; +@import "./tip/style.scss"; @import "./toggle-control/style.scss"; @import "./toolbar/style.scss"; @import "./toolbar-button/style.scss"; diff --git a/packages/components/src/tip/index.js b/packages/components/src/tip/index.js new file mode 100644 index 00000000000000..1672283d426f61 --- /dev/null +++ b/packages/components/src/tip/index.js @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { SVG, Path } from '../'; + +function Tip( props ) { + return ( +
    + + + +

    { props.children }

    +
    + ); +} + +export default Tip; diff --git a/packages/components/src/tip/style.scss b/packages/components/src/tip/style.scss new file mode 100644 index 00000000000000..d02afb1bdf8d09 --- /dev/null +++ b/packages/components/src/tip/style.scss @@ -0,0 +1,15 @@ +.components-tip { + display: flex; + color: $dark-gray-500; + + svg { + align-self: center; + fill: $alert-yellow; + flex-shrink: 0; + margin-right: $grid-size-large; + } + + p { + margin: 0; + } +} diff --git a/packages/e2e-tests/specs/editor-modes.test.js b/packages/e2e-tests/specs/editor-modes.test.js index 9fa866e893e9fd..6cb49238db01d3 100644 --- a/packages/e2e-tests/specs/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor-modes.test.js @@ -88,7 +88,7 @@ describe( 'Editing modes (visual/HTML)', () => { it( 'the code editor should unselect blocks and disable the inserter', async () => { // The paragraph block should be selected const title = await page.$eval( - '.block-editor-block-inspector__card-title', + '.block-editor-block-card__title', ( element ) => element.innerText ); expect( title ).toBe( 'Paragraph' ); diff --git a/packages/edit-post/src/components/options-modal/index.js b/packages/edit-post/src/components/options-modal/index.js index bb3f613539062f..1cfe8326327554 100644 --- a/packages/edit-post/src/components/options-modal/index.js +++ b/packages/edit-post/src/components/options-modal/index.js @@ -27,6 +27,7 @@ import { EnablePublishSidebarOption, EnableTipsOption, EnablePanelOption, + EnableFeature, } from './options'; import MetaBoxesSection from './meta-boxes-section'; @@ -47,6 +48,7 @@ export function OptionsModal( { isModalActive, isViewable, closeModal } ) {
    +
    diff --git a/packages/edit-post/src/components/options-modal/options/enable-feature.js b/packages/edit-post/src/components/options-modal/options/enable-feature.js new file mode 100644 index 00000000000000..a0b79eddef3a55 --- /dev/null +++ b/packages/edit-post/src/components/options-modal/options/enable-feature.js @@ -0,0 +1,24 @@ +/** + * WordPress dependencies + */ +import { compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import BaseOption from './base'; + +export default compose( + withSelect( ( select, { feature } ) => ( { + isChecked: select( 'core/edit-post' ).isFeatureActive( feature ), + } ) ), + withDispatch( ( dispatch, { feature } ) => { + const { toggleFeature } = dispatch( 'core/edit-post' ); + return { + onChange() { + toggleFeature( feature ); + }, + }; + } ) +)( BaseOption ); diff --git a/packages/edit-post/src/components/options-modal/options/index.js b/packages/edit-post/src/components/options-modal/options/index.js index e1263f9f684741..8684b680377054 100644 --- a/packages/edit-post/src/components/options-modal/options/index.js +++ b/packages/edit-post/src/components/options-modal/options/index.js @@ -3,3 +3,4 @@ export { default as EnablePanelOption } from './enable-panel'; export { default as EnablePluginDocumentSettingPanelOption } from './enable-plugin-document-setting-panel'; export { default as EnablePublishSidebarOption } from './enable-publish-sidebar'; export { default as EnableTipsOption } from './enable-tips'; +export { default as EnableFeature } from './enable-feature'; diff --git a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap index c2907a26e3001e..7a89493c76eea9 100644 --- a/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/options-modal/test/__snapshots__/index.js.snap @@ -15,6 +15,10 @@ exports[`OptionsModal should match snapshot when the modal is active 1`] = ` +
    ); diff --git a/packages/block-editor/src/components/writing-flow/style.scss b/packages/block-editor/src/components/writing-flow/style.scss index e1ff5e860ad149..422b378f2e8e2e 100644 --- a/packages/block-editor/src/components/writing-flow/style.scss +++ b/packages/block-editor/src/components/writing-flow/style.scss @@ -1,10 +1,8 @@ .block-editor-writing-flow { - height: 100%; display: flex; flex-direction: column; } .block-editor-writing-flow__click-redirect { - flex-basis: 100%; cursor: text; } diff --git a/packages/e2e-tests/specs/typewriter.test.js b/packages/e2e-tests/specs/typewriter.test.js new file mode 100644 index 00000000000000..a41d9b0ba4b71b --- /dev/null +++ b/packages/e2e-tests/specs/typewriter.test.js @@ -0,0 +1,168 @@ +/** + * WordPress dependencies + */ +import { createNewPost } from '@wordpress/e2e-test-utils'; + +describe( 'TypeWriter', () => { + beforeEach( async () => { + await createNewPost(); + } ); + + const getCaretPosition = async () => + await page.evaluate( () => wp.dom.computeCaretRect().y ); + + // Allow the scroll position to be 1px off. + const BUFFER = 1; + + const getDiff = async ( caretPosition ) => + Math.abs( await getCaretPosition() - caretPosition ); + + it( 'should maintain caret position', async () => { + // Create first block. + await page.keyboard.press( 'Enter' ); + + const initialPosition = await getCaretPosition(); + + // The page shouldn't be scrolled when it's being filled. + await page.keyboard.press( 'Enter' ); + + expect( await getCaretPosition() ).toBeGreaterThan( initialPosition ); + + // Create blocks until the the typewriter effect kicks in. + while ( await page.evaluate( () => + wp.dom.getScrollContainer( document.activeElement ).scrollTop === 0 + ) ) { + await page.keyboard.press( 'Enter' ); + } + + const newPosition = await getCaretPosition(); + + // Now the scroll position should be maintained. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + + // Type until the text wraps. + while ( await page.evaluate( () => + document.activeElement.clientHeight <= + parseInt( getComputedStyle( document.activeElement ).lineHeight, 10 ) + ) ) { + await page.keyboard.type( 'a' ); + } + + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + + // Pressing backspace will reposition the caret to the previous line. + // Scroll position should be adjusted again. + await page.keyboard.press( 'Backspace' ); + + expect( await getDiff( newPosition ) ).toBeLessThanOrEqual( BUFFER ); + + // Should reset scroll position to maintain. + await page.keyboard.press( 'ArrowUp' ); + + const positionAfterArrowUp = await getCaretPosition(); + + expect( positionAfterArrowUp ).toBeLessThan( newPosition ); + + // Should be scrolled to new position. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( positionAfterArrowUp ) ).toBeLessThanOrEqual( BUFFER ); + } ); + + it( 'should maintain caret position after scroll', async () => { + // Create first block. + await page.keyboard.press( 'Enter' ); + + await page.evaluate( () => + wp.dom.getScrollContainer( document.activeElement ).scrollTop = 1 + ); + + const initialPosition = await getCaretPosition(); + + // Should maintain scroll position. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); + } ); + + it( 'should maintain caret position after leaving last editable', async () => { + // Create first block. + await page.keyboard.press( 'Enter' ); + // Create second block. + await page.keyboard.press( 'Enter' ); + // Move to first block. + await page.keyboard.press( 'ArrowUp' ); + + const initialPosition = await getCaretPosition(); + + // Should maintain scroll position. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( initialPosition ) ).toBeLessThanOrEqual( BUFFER ); + } ); + + it( 'should scroll caret into view from the top', async () => { + // Create first block. + await page.keyboard.press( 'Enter' ); + + let count = 0; + + // Create blocks until the the typewriter effect kicks in. + while ( await page.evaluate( () => + wp.dom.getScrollContainer( document.activeElement ).scrollTop === 0 + ) ) { + await page.keyboard.press( 'Enter' ); + count++; + } + + // Scroll the active element to the very bottom of the scroll container, + // then scroll 20px down, so the caret is partially hidden. + await page.evaluate( () => { + document.activeElement.scrollIntoView( false ); + wp.dom.getScrollContainer( document.activeElement ).scrollTop -= 20; + } ); + + const bottomPostition = await getCaretPosition(); + + // Should scroll the caret back into view (preserve browser behaviour). + await page.keyboard.type( 'a' ); + + const newBottomPosition = await getCaretPosition(); + + expect( newBottomPosition ).toBeLessThan( bottomPostition ); + + // Should maintain new caret position. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( newBottomPosition ) ).toBeLessThanOrEqual( BUFFER ); + + await page.keyboard.press( 'Backspace' ); + + while ( count-- ) { + await page.keyboard.press( 'ArrowUp' ); + } + + // Scroll the active element to the very top of the scroll container, + // then scroll 10px down, so the caret is partially hidden. + await page.evaluate( () => { + document.activeElement.scrollIntoView(); + wp.dom.getScrollContainer( document.activeElement ).scrollTop += 20; + } ); + + const topPostition = await getCaretPosition(); + + // Should scroll the caret back into view (preserve browser behaviour). + await page.keyboard.type( 'a' ); + + const newTopPosition = await getCaretPosition(); + + expect( newTopPosition ).toBeGreaterThan( topPostition ); + + // Should maintain new caret position. + await page.keyboard.press( 'Enter' ); + + expect( await getDiff( newTopPosition ) ).toBeLessThanOrEqual( BUFFER ); + } ); +} ); diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index ee2b55bbed5d24..4ae2c2a9a71e9b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -62,6 +62,7 @@ function Layout( { const className = classnames( 'edit-post-layout', { 'is-sidebar-opened': sidebarIsOpened, 'has-fixed-toolbar': hasFixedToolbar, + 'has-metaboxes': hasActiveMetaboxes, } ); const publishLandmarkProps = { diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 8287f5c27396d5..37d3749f0f4983 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -7,6 +7,7 @@ import { } from '@wordpress/editor'; import { WritingFlow, + Typewriter, ObserveTyping, BlockList, CopyHandler, @@ -27,14 +28,16 @@ function VisualEditor() { - - - - - - - - + + + + + + + + + + <__experimentalBlockSettingsMenuFirstItem> { ( { onClose } ) => } diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 8ca4cf509cf967..e76eacdf32f96b 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -1,6 +1,6 @@ .edit-post-visual-editor { position: relative; - padding: 50px 0; + padding-top: 50px; & .components-button { font-family: $default-font; @@ -8,11 +8,14 @@ } .edit-post-visual-editor .block-editor-writing-flow__click-redirect { - // Collapse to minimum height of 50px, to fully occupy editor bottom pad. - height: 50px; + // Allow the page to be scrolled with the last block in the middle. + height: 50vh; width: 100%; - // Offset for: Visual editor padding, block (default appender) margin. - margin: #{ -1 * $block-spacing } auto -50px; +} + +// Hide the extra space when there are metaboxes. +.has-metaboxes .edit-post-visual-editor .block-editor-writing-flow__click-redirect { + height: 0; } // The base width of blocks From 2f9d0e20775083b3f1f99da2bc0f9e55ae78e6e0 Mon Sep 17 00:00:00 2001 From: Matt Chowning Date: Tue, 20 Aug 2019 10:48:39 -0400 Subject: [PATCH 33/52] [RNMobile] Hide replaceable block when adding block (#16931) --- .../src/components/block-list/index.native.js | 48 ++++++++++++------- .../components/block-list/style.native.scss | 1 - 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 3a193a9d02a9b1..facef0cc5e01ca 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -52,12 +52,12 @@ export class BlockList extends Component { if ( this.props.isPostTitleSelected ) { // if post title selected, insert at top of post return 0; - } else if ( this.props.selectedBlockOrder === -1 ) { + } else if ( this.props.selectedBlockIndex === -1 ) { // if no block selected, insert at end of post return this.props.blockCount; } // insert after selected block - return this.props.selectedBlockOrder + 1; + return this.props.selectedBlockIndex + 1; } blockHolderBorderStyle() { @@ -125,19 +125,21 @@ export class BlockList extends Component { return isUnmodifiedDefaultBlock( block ); } - renderItem( { item: clientId } ) { + renderItem( { item: clientId, index } ) { + const { shouldShowBlockAtIndex, shouldShowInsertionPoint } = this.props; return ( - { this.props.shouldShowInsertionPoint( clientId ) && this.renderAddBlockSeparator() } - + { shouldShowInsertionPoint( clientId ) && this.renderAddBlockSeparator() } + { shouldShowBlockAtIndex( index ) && ( + ) } ); } @@ -173,7 +175,6 @@ export default compose( [ getBlockOrder, getSelectedBlock, getSelectedBlockClientId, - isBlockSelected, getBlockInsertionPoint, isBlockInsertionPointVisible, } = select( 'core/block-editor' ); @@ -181,24 +182,37 @@ export default compose( [ const selectedBlockClientId = getSelectedBlockClientId(); const blockClientIds = getBlockOrder( rootClientId ); const insertionPoint = getBlockInsertionPoint(); + const blockInsertionPointIsVisible = isBlockInsertionPointVisible(); const shouldShowInsertionPoint = ( clientId ) => { return ( - isBlockInsertionPointVisible() && + blockInsertionPointIsVisible && insertionPoint.rootClientId === rootClientId && blockClientIds[ insertionPoint.index ] === clientId ); }; + const selectedBlockIndex = getBlockIndex( selectedBlockClientId ); + const shouldShowBlockAtIndex = ( index ) => { + const shouldHideBlockAtIndex = ( + blockInsertionPointIsVisible && + // if `index` === `insertionPoint.index`, then block is replaceable + index === insertionPoint.index && + // only hide selected block + index === selectedBlockIndex + ); + return ! shouldHideBlockAtIndex; + }; + return { blockClientIds, blockCount: getBlockCount( rootClientId ), getBlockName, - isBlockSelected, isBlockInsertionPointVisible: isBlockInsertionPointVisible(), + shouldShowBlockAtIndex, shouldShowInsertionPoint, selectedBlock: getSelectedBlock(), selectedBlockClientId, - selectedBlockOrder: getBlockIndex( selectedBlockClientId ), + selectedBlockIndex, }; } ), withDispatch( ( dispatch ) => { diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index 5185e2099bf93e..b6961600b95c67 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -35,7 +35,6 @@ } .containerStyleAddHere { - flex: 1; flex-direction: row; background-color: $white; } From 22aafff13a955182c798009aae43a2f5685ef2fe Mon Sep 17 00:00:00 2001 From: Maxime Biais Date: Thu, 25 Jul 2019 10:55:51 +0200 Subject: [PATCH 34/52] Update video player style on mobile - Add a new gridicon play icon, from: https://github.com/Automattic/gridicons/blob/87c9fce08b4a9f184b9fb4963228757fdd4f4e74/svg-min/gridicons-play.svg - Replace the Dashicon play by this one - Update icon size and icon color - Update the overlay color --- .../src/video/gridicon-play.native.js | 7 ++++++ .../src/video/video-player.native.js | 10 ++++---- .../src/video/video-player.native.scss | 23 ++++++++----------- 3 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 packages/block-library/src/video/gridicon-play.native.js diff --git a/packages/block-library/src/video/gridicon-play.native.js b/packages/block-library/src/video/gridicon-play.native.js new file mode 100644 index 00000000000000..6f14d04f16a299 --- /dev/null +++ b/packages/block-library/src/video/gridicon-play.native.js @@ -0,0 +1,7 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; + +export default ; + diff --git a/packages/block-library/src/video/video-player.native.js b/packages/block-library/src/video/video-player.native.js index 5b67d4673b4192..31c2cdb7b64baa 100644 --- a/packages/block-library/src/video/video-player.native.js +++ b/packages/block-library/src/video/video-player.native.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { Dashicon } from '@wordpress/components'; +import { Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; /** @@ -15,6 +15,7 @@ import { default as VideoPlayer } from 'react-native-video'; * Internal dependencies */ import styles from './video-player.scss'; +import PlayIcon from './gridicon-play'; class Video extends Component { constructor() { @@ -92,10 +93,9 @@ class Video extends Component { { 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. - - - - + + + } diff --git a/packages/block-library/src/video/video-player.native.scss b/packages/block-library/src/video/video-player.native.scss index ae0dc6382f91f9..4dd74ba25e880e 100644 --- a/packages/block-library/src/video/video-player.native.scss +++ b/packages/block-library/src/video/video-player.native.scss @@ -8,25 +8,22 @@ $play-icon-size: 50; align-items: center; } -.overlay { +.overlayContainer { justify-content: center; align-items: center; align-self: center; position: absolute; - background-color: transparent; - opacity: 0.3; +} + +.blackOverlay { + width: 100%; + height: 100%; + background-color: $black; + opacity: 0.4; } .playIcon { - justify-content: center; - align-items: center; - align-self: center; position: absolute; - background-color: #000; - height: $play-icon-size; - width: $play-icon-size; - border-bottom-left-radius: $play-icon-size/8; - border-bottom-right-radius: $play-icon-size/8; - border-top-right-radius: $play-icon-size/8; - border-top-left-radius: $play-icon-size/8; + opacity: 0.7; + size: 64; } From c9cbf8a4596143b735cef2ea330b5b536f457605 Mon Sep 17 00:00:00 2001 From: Jorge Costa Date: Wed, 21 Aug 2019 14:28:58 +0100 Subject: [PATCH 35/52] Add: Disabled block count in the block manager (#17103) --- .../components/manage-blocks-modal/manager.js | 23 +++++++++++++++++-- .../components/manage-blocks-modal/style.scss | 11 +++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/packages/edit-post/src/components/manage-blocks-modal/manager.js b/packages/edit-post/src/components/manage-blocks-modal/manager.js index 7cf25463e8a4d7..1992baf8789287 100644 --- a/packages/edit-post/src/components/manage-blocks-modal/manager.js +++ b/packages/edit-post/src/components/manage-blocks-modal/manager.js @@ -1,7 +1,7 @@ /** * External dependencies */ -import { filter } from 'lodash'; +import { filter, isArray } from 'lodash'; /** * WordPress dependencies @@ -9,7 +9,7 @@ import { filter } from 'lodash'; import { withSelect } from '@wordpress/data'; import { compose, withState } from '@wordpress/compose'; import { TextControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; /** * Internal dependencies @@ -23,6 +23,7 @@ function BlockManager( { categories, hasBlockSupport, isMatchingSearchTerm, + numberOfHiddenBlocks, } ) { // Filtering occurs here (as opposed to `withSelect`) to avoid wasted // wasted renders by consequence of `Array#filter` producing a new @@ -43,6 +44,20 @@ function BlockManager( { } ) } className="edit-post-manage-blocks-modal__search" /> + { !! numberOfHiddenBlocks && ( +
    + { + sprintf( + _n( + '%1$d block is disabled.', + '%1$d blocks are disabled.', + numberOfHiddenBlocks + ), + numberOfHiddenBlocks + ) + } +
    + ) }
    Date: Thu, 15 Aug 2019 19:13:36 +1000 Subject: [PATCH 36/52] Use `400` as a valid `font-weight` --- packages/block-library/src/calendar/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/calendar/style.scss b/packages/block-library/src/calendar/style.scss index f6fc8f3a5ad7be..fb38516c38fbfe 100644 --- a/packages/block-library/src/calendar/style.scss +++ b/packages/block-library/src/calendar/style.scss @@ -18,7 +18,7 @@ } table th { - font-weight: 440; + font-weight: 400; background: $light-gray-300; } From 1aecff9d482b109d7b287dc539398ed84b8b058f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Wed, 21 Aug 2019 23:42:26 +0200 Subject: [PATCH 37/52] Writing Flow/Quote: allow splitting (#17121) * Writing Flow/Quote: allow splitting * Add extra merge e2e test --- packages/block-library/src/quote/edit.js | 11 ++++ packages/block-library/src/quote/index.js | 10 ++- .../blocks/__snapshots__/quote.test.js.snap | 62 +++++++++++++++++++ packages/e2e-tests/specs/blocks/quote.test.js | 53 ++++++++++++++++ 4 files changed, 134 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/quote/edit.js b/packages/block-library/src/quote/edit.js index 9210230f2ba02b..32793f60692a60 100644 --- a/packages/block-library/src/quote/edit.js +++ b/packages/block-library/src/quote/edit.js @@ -9,6 +9,7 @@ import classnames from 'classnames'; import { __ } from '@wordpress/i18n'; import { AlignmentToolbar, BlockControls, RichText } from '@wordpress/block-editor'; import { BlockQuotation } from '@wordpress/components'; +import { createBlock } from '@wordpress/blocks'; export default function QuoteEdit( { attributes, setAttributes, isSelected, mergeBlocks, onReplace, className } ) { const { align, value, citation } = attributes; @@ -48,6 +49,16 @@ export default function QuoteEdit( { attributes, setAttributes, isSelected, merg // translators: placeholder text used for the quote __( 'Write quote…' ) } + onReplace={ onReplace } + onSplit={ ( piece ) => + createBlock( 'core/quote', { + ...attributes, + value: piece, + } ) + } + __unstableOnSplitMiddle={ () => + createBlock( 'core/paragraph' ) + } /> { ( ! RichText.isEmpty( citation ) || isSelected ) && (

    ' ) { return { ...attributes, - citation: attributes.citation + citation, + citation, }; } return { ...attributes, value: attributes.value + value, - citation: attributes.citation + citation, + citation, }; }, deprecated, diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/quote.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/quote.test.js.snap index 57d1d187978f4c..57fcc6c61dbdd0 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/quote.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/quote.test.js.snap @@ -80,6 +80,68 @@ exports[`Quote can be merged into from a paragraph 1`] = ` " `; +exports[`Quote can be split at the end and merged back 1`] = ` +" +

    1

    + + + +

    +" +`; + +exports[`Quote can be split at the end and merged back 2`] = ` +" +

    1

    +" +`; + +exports[`Quote can be split at the end and merged back 3`] = ` +" +

    1

    +" +`; + +exports[`Quote can be split in the middle and merged back 1`] = ` +" +

    1

    c
    + + + +

    + + + +

    2

    c
    +" +`; + +exports[`Quote can be split in the middle and merged back 2`] = ` +" +

    1

    c
    + + + +

    2

    c
    +" +`; + +exports[`Quote can be split in the middle and merged back 3`] = ` +" +

    1

    c
    + + + +

    2

    c
    +" +`; + +exports[`Quote can be split in the middle and merged back 4`] = ` +" +

    1

    2

    c
    +" +`; + exports[`Quote is transformed to a heading and a quote if the quote contains a citation 1`] = ` "

    one

    diff --git a/packages/e2e-tests/specs/blocks/quote.test.js b/packages/e2e-tests/specs/blocks/quote.test.js index 7a612e24198099..38f5348e989907 100644 --- a/packages/e2e-tests/specs/blocks/quote.test.js +++ b/packages/e2e-tests/specs/blocks/quote.test.js @@ -185,4 +185,57 @@ describe( 'Quote', () => { await page.keyboard.press( 'Backspace' ); expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + + it( 'can be split at the end and merged back', async () => { + await insertBlock( 'Quote' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + + // Expect empty paragraph outside quote block. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + + // Expect empty paragraph inside quote block. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + + // Expect quote without empty paragraphs. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'can be split in the middle and merged back', async () => { + await insertBlock( 'Quote' ); + await page.keyboard.type( '1' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '2' ); + await page.keyboard.press( 'Tab' ); + await page.keyboard.type( 'c' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Enter' ); + await page.keyboard.press( 'Enter' ); + + // Expect two quote blocks and empty paragraph in the middle. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + + // Expect two quote blocks and empty paragraph in the first quote. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'Backspace' ); + + // Expect two quote blocks. + expect( await getEditedPostContent() ).toMatchSnapshot(); + + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); } ); From 0b4d9c805fb764c1ba05ebe999c76e2bf0452be6 Mon Sep 17 00:00:00 2001 From: Kerry Liu Date: Wed, 21 Aug 2019 17:08:13 -0700 Subject: [PATCH 38/52] Build: remove global install of latest npm since we want to use the paired node/npm version (#17134) * Build: remove global install of latest npm since we want to use the paired node/npm version * Also update travis to remove --latest-npm flag --- .travis.yml | 2 +- bin/install-node-nvm.sh | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a414f619ff17c..3e16c463eb6ed5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ branches: - master before_install: - - nvm install --latest-npm + - nvm install env: PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true diff --git a/bin/install-node-nvm.sh b/bin/install-node-nvm.sh index 9edc083833dc1d..d9cd1a6faf5102 100755 --- a/bin/install-node-nvm.sh +++ b/bin/install-node-nvm.sh @@ -69,9 +69,6 @@ fi echo -e $(status_message "Installing and updating NPM packages..." ) npm install -# Make sure npm is up-to-date -npm install npm -g - # There was a bug in NPM that caused changes in package-lock.json. Handle that. if [ "$TRAVIS" != "true" ] && ! git diff --no-ext-diff --exit-code package-lock.json >/dev/null; then if ask "$(warning_message "Your package-lock.json changed, which may mean there's an issue with your NPM cache. Would you like to try and automatically clean it up?" )" N 10; then From 47dfd82ce25e18458d6349cf9c40d94f8fdf2c9f Mon Sep 17 00:00:00 2001 From: Robert Anderson Date: Thu, 22 Aug 2019 15:02:39 +1000 Subject: [PATCH 39/52] Project automation: Rewrite actions using JavaScript (#17080) * Project automation: Rewrite actions using JavaScript * Project automation: Don't transpile or install all dependencies * Project automation: pull-request-automation -> project-management-automation * Project automation: Add explanatory comment for `npm install` hack * Project automation: Add debug statements * Project automation: Don't use GitHub's debug() function * Project automation: Use `payload` in tasks, not `context` * Project automation: Link to the relevant GitHub documentation --- .../actions/assign-fixed-issues/Dockerfile | 18 -- .../actions/assign-fixed-issues/entrypoint.sh | 60 ----- .../actions/first-time-contributor/Dockerfile | 18 -- .../first-time-contributor/entrypoint.sh | 43 ---- .github/actions/milestone-it/Dockerfile | 18 -- .github/actions/milestone-it/entrypoint.sh | 109 -------- .github/workflows/pull-request-automation.yml | 14 + ...-prs-opened-by-first-time-contributors.yml | 12 - ...-fixed-issues-when-pull-request-opened.yml | 12 - ...request-milestone-merged-pull-requests.yml | 12 - docs/manifest-devhub.json | 6 + package-lock.json | 75 ++++++ package.json | 2 + packages/project-management-automation/.npmrc | 1 + .../project-management-automation/README.md | 35 +++ .../project-management-automation/action.yml | 10 + .../lib/add-first-time-contributor-label.js | 39 +++ .../lib/add-milestone.js | 103 ++++++++ .../lib/assign-fixed-issues.js | 39 +++ .../lib/debug.js | 12 + .../lib/index.js | 56 ++++ .../test/add-first-time-contributor-label.js | 62 +++++ .../lib/test/add-milestone.js | 242 ++++++++++++++++++ .../lib/test/assign-fixed-issues.js | 75 ++++++ .../package.json | 28 ++ 25 files changed, 799 insertions(+), 302 deletions(-) delete mode 100644 .github/actions/assign-fixed-issues/Dockerfile delete mode 100755 .github/actions/assign-fixed-issues/entrypoint.sh delete mode 100644 .github/actions/first-time-contributor/Dockerfile delete mode 100755 .github/actions/first-time-contributor/entrypoint.sh delete mode 100644 .github/actions/milestone-it/Dockerfile delete mode 100755 .github/actions/milestone-it/entrypoint.sh create mode 100644 .github/workflows/pull-request-automation.yml delete mode 100644 .github/workflows/pull_request-add-the-first-time-contributor-label-to-prs-opened-by-first-time-contributors.yml delete mode 100644 .github/workflows/pull_request-assign-fixed-issues-when-pull-request-opened.yml delete mode 100644 .github/workflows/pull_request-milestone-merged-pull-requests.yml create mode 100644 packages/project-management-automation/.npmrc create mode 100644 packages/project-management-automation/README.md create mode 100644 packages/project-management-automation/action.yml create mode 100644 packages/project-management-automation/lib/add-first-time-contributor-label.js create mode 100644 packages/project-management-automation/lib/add-milestone.js create mode 100644 packages/project-management-automation/lib/assign-fixed-issues.js create mode 100644 packages/project-management-automation/lib/debug.js create mode 100644 packages/project-management-automation/lib/index.js create mode 100644 packages/project-management-automation/lib/test/add-first-time-contributor-label.js create mode 100644 packages/project-management-automation/lib/test/add-milestone.js create mode 100644 packages/project-management-automation/lib/test/assign-fixed-issues.js create mode 100644 packages/project-management-automation/package.json diff --git a/.github/actions/assign-fixed-issues/Dockerfile b/.github/actions/assign-fixed-issues/Dockerfile deleted file mode 100644 index e81b9490ab5fe1..00000000000000 --- a/.github/actions/assign-fixed-issues/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM debian:stable-slim - -LABEL "name"="Assign Fixed Issues" -LABEL "maintainer"="The WordPress Contributors" -LABEL "version"="1.0.0" - -LABEL "com.github.actions.name"="Assign Fixed Issues" -LABEL "com.github.actions.description"="Assigns the issues fixed by a pull request to the author of that pull request" -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/assign-fixed-issues/entrypoint.sh b/.github/actions/assign-fixed-issues/entrypoint.sh deleted file mode 100755 index 83680fcc308779..00000000000000 --- a/.github/actions/assign-fixed-issues/entrypoint.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/bash -set -e - -# 1. Proceed only when acting on an opened pull request. - -action=$(jq -r '.action' $GITHUB_EVENT_PATH) - -if [ "$action" != 'closed' ]; then - echo "Action '$action' not a close action. Aborting." - exit 0; -fi - -# 2. Find the issues that this PR 'fixes'. - -issues=$( - jq -r '.pull_request.body' $GITHUB_EVENT_PATH | perl -nle 'print $1 while / - (?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved) - :? - \ + - (?:\#?|https?:\/\/github\.com\/WordPress\/gutenberg\/issues\/) - (\d+) - /igx' -) - -if [ -z "$issues" ]; then - echo "Pull request does not 'fix' any issues. Aborting." - exit 0 -fi - -# 3. Grab the author of the PR. - -author=$(jq -r '.pull_request.user.login' $GITHUB_EVENT_PATH) - -# 4. Loop through each 'fixed' issue. - -for issue in $issues; do - - # 4a. Add the author as an asignee to the issue. This fails if the author is - # already assigned, which is expected and ignored. - - curl \ - --silent \ - -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Content-Type: application/json" \ - -d "{\"assignees\":[\"$author\"]}" \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$issue/assignees" > /dev/null - - # 3b. Label the issue as 'In Progress'. This fails if the label is already - # applied, which is expected and ignored. - - curl \ - --silent \ - -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"labels":["[Status] In Progress"]}' \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$issue/labels" > /dev/null - -done diff --git a/.github/actions/first-time-contributor/Dockerfile b/.github/actions/first-time-contributor/Dockerfile deleted file mode 100644 index 01e3c560b82219..00000000000000 --- a/.github/actions/first-time-contributor/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM debian:stable-slim - -LABEL "name"="First Time Contributor" -LABEL "maintainer"="The WordPress Contributors" -LABEL "version"="1.0.0" - -LABEL "com.github.actions.name"="First Time Contributor" -LABEL "com.github.actions.description"="Assigns the first time contributor label to pull requests" -LABEL "com.github.actions.icon"="award" -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/first-time-contributor/entrypoint.sh b/.github/actions/first-time-contributor/entrypoint.sh deleted file mode 100755 index 9360574ebe5097..00000000000000 --- a/.github/actions/first-time-contributor/entrypoint.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -# 1. Proceed only when acting on an opened pull request. -action=$(jq -r '.action' $GITHUB_EVENT_PATH) - -if [ "$action" != 'opened' ]; then - echo "Action '$action' not a close action. Aborting." - exit 0; -fi - -# 2. Get the author and pr number for the pull request. -author=$(jq -r '.pull_request.user.login' $GITHUB_EVENT_PATH) -pr_number=$(jq -r '.number' $GITHUB_EVENT_PATH) - -if [ "$pr_number" = "null" ] || [ "$author" = "null" ]; then - echo "Could not find PR number or author. $pr_number / $author" - exit 0 -fi - -# 3. Fetch the author's commit count for the repo to determine if they're a first-time contributor. -commit_count=$( - curl \ - --silent \ - -H "Accept: application/vnd.github.cloak-preview" \ - "https://api.github.com/search/commits?q=repo:$GITHUB_REPOSITORY+author:$author" \ - | jq -r '.total_count' -) - -# 4. If the response has a commit count of zero, exit early, the author is not a first time contributor. -if [ "$commit_count" != "0" ]; then - echo "Pull request #$pr_number was not created by a first-time contributor ($author)." - exit 0 -fi - -# 5. Assign the 'First Time Contributor' label. -curl \ - --silent \ - -X POST \ - -H "Authorization: token $GITHUB_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"labels":["First-time Contributor"]}' \ - "https://api.github.com/repos/$GITHUB_REPOSITORY/issues/$pr_number/labels" > /dev/null diff --git a/.github/actions/milestone-it/Dockerfile b/.github/actions/milestone-it/Dockerfile deleted file mode 100644 index af20456bcc34e5..00000000000000 --- a/.github/actions/milestone-it/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -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 deleted file mode 100755 index 8c91df0cb05ff6..00000000000000 --- a/.github/actions/milestone-it/entrypoint.sh +++ /dev/null @@ -1,109 +0,0 @@ -#!/bin/bash -set -e - -# 1. Proceed only when acting on a merge action on the master branch. - -action=$(jq -r '.action' $GITHUB_EVENT_PATH) - -if [ "$action" != 'closed' ]; then - echo "Action '$action' not a close action. Aborting." - exit 0; -fi - -merged=$(jq -r '.pull_request.merged' $GITHUB_EVENT_PATH) - -if [ "$merged" != 'true' ]; then - echo "Pull request closed without merge. Aborting." - exit 0; -fi - -base=$(jq -r '.pull_request.base.ref' $GITHUB_EVENT_PATH) - -if [ "$base" != 'master' ]; then - echo 'Milestones apply only to master merge. Aborting.' - exit 0; -fi - -# 2. 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 0; -fi - -# 3. Read current version. - -version=$( - curl \ - --silent \ - "https://raw.githubusercontent.com/$GITHUB_REPOSITORY/master/package.json" \ - | jq -r '.version' -) - -IFS='.' read -ra parts <<< "$version" -major=${parts[0]} -minor=${parts[1]} - -# 4. Determine next milestone. - -if [[ $minor == 9* ]]; then - major=$((major+1)) - minor="0" -else - minor=$((minor+1)) -fi - -milestone="Gutenberg $major.$minor" - -# 5. 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=1564358400 -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") - -# 6. 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 - -# 7. 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" -) - -# 8. 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/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml new file mode 100644 index 00000000000000..10277d5a3b45e3 --- /dev/null +++ b/.github/workflows/pull-request-automation.yml @@ -0,0 +1,14 @@ +on: pull_request +name: Pull request automation + +jobs: + pull-request-automation: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + # Changing into the action's directory and running `npm install` is much + # faster than a full project-wide `npm ci`. + - run: cd packages/project-management-automation && npm install + - uses: ./packages/project-management-automation + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull_request-add-the-first-time-contributor-label-to-prs-opened-by-first-time-contributors.yml b/.github/workflows/pull_request-add-the-first-time-contributor-label-to-prs-opened-by-first-time-contributors.yml deleted file mode 100644 index 5c5bf75d0716e6..00000000000000 --- a/.github/workflows/pull_request-add-the-first-time-contributor-label-to-prs-opened-by-first-time-contributors.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: pull_request -name: Add the First-time Contributor label -jobs: - filterOpened: - name: Filter opened - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: First Time Contributor - uses: ./.github/actions/first-time-contributor - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull_request-assign-fixed-issues-when-pull-request-opened.yml b/.github/workflows/pull_request-assign-fixed-issues-when-pull-request-opened.yml deleted file mode 100644 index bc39c694424252..00000000000000 --- a/.github/workflows/pull_request-assign-fixed-issues-when-pull-request-opened.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: pull_request -name: Assign fixed issues when pull request opened -jobs: - filterOpened: - name: Filter opened - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Assign Fixed Issues - uses: ./.github/actions/assign-fixed-issues - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pull_request-milestone-merged-pull-requests.yml b/.github/workflows/pull_request-milestone-merged-pull-requests.yml deleted file mode 100644 index 6487a9ed13b7de..00000000000000 --- a/.github/workflows/pull_request-milestone-merged-pull-requests.yml +++ /dev/null @@ -1,12 +0,0 @@ -on: pull_request -name: Milestone merged pull requests -jobs: - milestoneIt: - name: Milestone It - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@master - - name: Milestone It - uses: ./.github/actions/milestone-it - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/docs/manifest-devhub.json b/docs/manifest-devhub.json index 8c96482e340a30..277a832fd419f4 100644 --- a/docs/manifest-devhub.json +++ b/docs/manifest-devhub.json @@ -1349,6 +1349,12 @@ "markdown_source": "../packages/priority-queue/README.md", "parent": "packages" }, + { + "title": "@wordpress/project-management-automation", + "slug": "packages-project-management-automation", + "markdown_source": "../packages/project-management-automation/README.md", + "parent": "packages" + }, { "title": "@wordpress/redux-routine", "slug": "packages-redux-routine", diff --git a/package-lock.json b/package-lock.json index 2febb418532ae1..019dd2a38b2379 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,22 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@actions/core": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.0.0.tgz", + "integrity": "sha512-aMIlkx96XH4E/2YZtEOeyrYQfhlas9jIRkfGPqMwXD095Rdkzo4lB6ZmbxPQSzD+e1M+Xsm98ZhuSMYGv/AlqA==", + "dev": true + }, + "@actions/github": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@actions/github/-/github-1.0.0.tgz", + "integrity": "sha512-PPbWZ5wFAD/Vr+RCECfR3KNHjTwYln4liJBihs9tQUL0/PCFqB2lSkIh9V94AcZFHxgKk8snImjuLaBE8bKR7A==", + "dev": true, + "requires": { + "@octokit/graphql": "^2.0.1", + "@octokit/rest": "^16.15.0" + } + }, "@babel/code-frame": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", @@ -3888,6 +3904,65 @@ } } }, + "@octokit/graphql": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-2.1.3.tgz", + "integrity": "sha512-XoXJqL2ondwdnMIW3wtqJWEwcBfKk37jO/rYkoxNPEVeLBDGsGO1TCWggrAlq3keGt/O+C/7VepXnukUxwt5vA==", + "dev": true, + "requires": { + "@octokit/request": "^5.0.0", + "universal-user-agent": "^2.0.3" + }, + "dependencies": { + "@octokit/request": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.0.2.tgz", + "integrity": "sha512-z1BQr43g4kOL4ZrIVBMHwi68Yg9VbkRUyuAgqCp1rU3vbYa69+2gIld/+gHclw15bJWQnhqqyEb7h5a5EqgZ0A==", + "dev": true, + "requires": { + "@octokit/endpoint": "^5.1.0", + "@octokit/request-error": "^1.0.1", + "deprecation": "^2.0.0", + "is-plain-object": "^3.0.0", + "node-fetch": "^2.3.0", + "once": "^1.4.0", + "universal-user-agent": "^3.0.0" + }, + "dependencies": { + "universal-user-agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-3.0.0.tgz", + "integrity": "sha512-T3siHThqoj5X0benA5H0qcDnrKGXzU8TKoX15x/tQHw1hQBvIEBHjxQ2klizYsqBOO/Q+WuxoQUihadeeqDnoA==", + "dev": true, + "requires": { + "os-name": "^3.0.0" + } + } + } + }, + "is-plain-object": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-3.0.0.tgz", + "integrity": "sha512-tZIpofR+P05k8Aocp7UI/2UTa9lTJSebCXpFFoR9aibpokDj/uXBsJ8luUu0tTVYKkMU6URDUuOfJZ7koewXvg==", + "dev": true, + "requires": { + "isobject": "^4.0.0" + } + }, + "isobject": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz", + "integrity": "sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==", + "dev": true + }, + "node-fetch": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", + "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", + "dev": true + } + } + }, "@octokit/plugin-enterprise-rest": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-3.6.2.tgz", diff --git a/package.json b/package.json index aa4805e6597d89..a29163b7492d2a 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,8 @@ "@wordpress/wordcount": "file:packages/wordcount" }, "devDependencies": { + "@actions/core": "1.0.0", + "@actions/github": "1.0.0", "@babel/core": "7.4.4", "@babel/plugin-syntax-jsx": "7.2.0", "@babel/runtime-corejs3": "7.4.4", diff --git a/packages/project-management-automation/.npmrc b/packages/project-management-automation/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/project-management-automation/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/project-management-automation/README.md b/packages/project-management-automation/README.md new file mode 100644 index 00000000000000..40c85f70a67233 --- /dev/null +++ b/packages/project-management-automation/README.md @@ -0,0 +1,35 @@ +# Gutenberg project management automation + +This is a [GitHub Action](https://help.github.com/en/categories/automating-your-workflow-with-github-actions) which contains various automation to assist with managing the Gutenberg GitHub repository: + +- `add-first-time-contributor-label`: Adds the 'First Time Contributor' label to PRs opened by contributors that have not yet made a commit. +- `add-milestone`: Assigns the correct milestone to PRs once merged. +- `assign-fixed-issues`: Assigns any issues 'fixed' by a newly opened PR to the author of that PR. + +# Installation and usage + +To use the action, include it in your workflow configuration file: + +```yaml +on: pull_request +jobs: + pull-request-automation: + runs-on: ubuntu-latest + steps: + - uses: WordPress/gutenberg/packages/project-management-automation@master + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + +``` + +# API + +## Inputs + +- `github_token`: Required. GitHub API token to use for making API requests. This should be stored as a secret in the GitHub repository. + +## Outputs + +_None._ + +

    Code is Poetry.

    diff --git a/packages/project-management-automation/action.yml b/packages/project-management-automation/action.yml new file mode 100644 index 00000000000000..c5cd00678edd4d --- /dev/null +++ b/packages/project-management-automation/action.yml @@ -0,0 +1,10 @@ +name: Gutenberg project management automation +description: > + Various automation to assist with managing the Gutenberg GitHub repository. +inputs: + github_token: + description: Secret GitHub API token to use for making API requests. + required: true +runs: + using: node12 + main: lib/index.js diff --git a/packages/project-management-automation/lib/add-first-time-contributor-label.js b/packages/project-management-automation/lib/add-first-time-contributor-label.js new file mode 100644 index 00000000000000..e185acbd46c4b6 --- /dev/null +++ b/packages/project-management-automation/lib/add-first-time-contributor-label.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +const debug = require( './debug' ); + +/** + * Adds the 'First Time Contributor' label to PRs opened by contributors that + * have not yet made a commit. + * + * @param {Object} payload Pull request event payload, see https://developer.github.com/v3/activity/events/types/#pullrequestevent. + * @param {Object} octokit Initialized Octokit REST client, see https://octokit.github.io/rest.js/. + */ +async function addFirstTimeContributorLabel( payload, octokit ) { + const owner = payload.repository.owner.login; + const repo = payload.repository.name; + const author = payload.pull_request.user.login; + + debug( `add-first-time-contributor-label: Searching for commits in ${ owner }/${ repo } by @${ author }` ); + + const { total_count: totalCount } = await octokit.search.commits( { + q: `repo:${ owner }/${ repo }+author:${ author }`, + } ); + + if ( totalCount !== 0 ) { + debug( 'add-first-time-contributor-label: Commits found. Aborting' ); + return; + } + + debug( `add-first-time-contributor-label: Adding 'First Time Contributor' label to issue #${ payload.pull_request.number }` ); + + await octokit.issues.addLabels( { + owner, + repo, + issue_number: payload.pull_request.number, + labels: [ 'First-time Contributor' ], + } ); +} + +module.exports = addFirstTimeContributorLabel; diff --git a/packages/project-management-automation/lib/add-milestone.js b/packages/project-management-automation/lib/add-milestone.js new file mode 100644 index 00000000000000..8fcc9c08d4e019 --- /dev/null +++ b/packages/project-management-automation/lib/add-milestone.js @@ -0,0 +1,103 @@ +/** + * Internal dependencies + */ +const debug = require( './debug' ); + +// Milestone due dates are calculated from a known due date: +// 6.3, which was due on August 12 2019. +const REFERENCE_MAJOR = 6; +const REFERENCE_MINOR = 3; +const REFERENCE_DATE = '2019-08-12'; + +// Releases are every 14 days. +const DAYS_PER_RELEASE = 14; + +/** + * Assigns the correct milestone to PRs once merged. + * + * @param {Object} payload Pull request event payload, see https://developer.github.com/v3/activity/events/types/#pullrequestevent. + * @param {Object} octokit Initialized Octokit REST client, see https://octokit.github.io/rest.js/. + */ +async function addMilestone( payload, octokit ) { + if ( ! payload.pull_request.merged ) { + debug( 'add-milestone: Pull request is not merged. Aborting' ); + return; + } + + if ( payload.pull_request.base.ref !== 'master' ) { + debug( 'add-milestone: Pull request is not based on `master`. Aborting' ); + return; + } + + debug( 'add-milestone: Fetching current milestone' ); + + const { milestone } = await octokit.issues.get( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.pull_request.number, + } ); + + if ( milestone ) { + debug( 'add-milestone: Pull request already has a milestone. Aborting' ); + return; + } + + debug( 'add-milestone: Fetching `package.json` contents' ); + + const { content } = await octokit.repos.getContents( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + path: 'package.json', + } ); + + const { version } = JSON.parse( content ); + + let [ major, minor ] = version.split( '.' ).map( Number ); + + debug( `add-milestone: Current plugin version is ${ major }.${ minor }` ); + + if ( minor === 9 ) { + major += 1; + minor = 0; + } else { + minor += 1; + } + + const numVersionsElapsed = ( ( major - REFERENCE_MAJOR ) * 10 ) + ( minor - REFERENCE_MINOR ); + const numDaysElapsed = numVersionsElapsed * DAYS_PER_RELEASE; + + // Using UTC for the calculation ensures it's not affected by daylight savings. + const dueDate = new Date( REFERENCE_DATE ); + dueDate.setUTCDate( dueDate.getUTCDate() + numDaysElapsed ); + + debug( `add-milestone: Creating 'Gutenberg ${ major }.${ minor }' milestone, due on ${ dueDate.toISOString() }` ); + + await octokit.issues.createMilestone( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + title: `Gutenberg ${ major }.${ minor }`, + due_on: dueDate.toISOString(), + } ); + + debug( 'add-milestone: Fetching all milestones' ); + + const milestones = await octokit.issues.listMilestonesForRepo( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + } ); + + const [ { number } ] = milestones.filter( + ( { title } ) => title === `Gutenberg ${ major }.${ minor }` + ); + + debug( `add-milestone: Adding issue #${ payload.pull_request.number } to milestone #${ number }` ); + + await octokit.issues.update( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: payload.pull_request.number, + milestone: number, + } ); +} + +module.exports = addMilestone; diff --git a/packages/project-management-automation/lib/assign-fixed-issues.js b/packages/project-management-automation/lib/assign-fixed-issues.js new file mode 100644 index 00000000000000..c0780256a41022 --- /dev/null +++ b/packages/project-management-automation/lib/assign-fixed-issues.js @@ -0,0 +1,39 @@ +/** + * Internal dependencies + */ +const debug = require( './debug' ); + +/** + * Assigns any issues 'fixed' by a newly opened PR to the author of that PR. + * + * @param {Object} payload Pull request event payload, see https://developer.github.com/v3/activity/events/types/#pullrequestevent. + * @param {Object} octokit Initialized Octokit REST client, see https://octokit.github.io/rest.js/. + */ +async function assignFixedIssues( payload, octokit ) { + const regex = /(?:close|closes|closed|fix|fixes|fixed|resolve|resolves|resolved):? +(?:\#?|https?:\/\/github\.com\/WordPress\/gutenberg\/issues\/)(\d+)/gi; + + let match; + while ( ( match = regex.exec( payload.pull_request.body ) ) ) { + const [ , issue ] = match; + + debug( `assign-fixed-issues: Assigning issue #${ issue } to @${ payload.pull_request.user.login }` ); + + await octokit.issues.addAssignees( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: +issue, + assignees: [ payload.pull_request.user.login ], + } ); + + debug( `assign-fixed-issues: Applying '[Status] In Progress' label to issue #${ issue }` ); + + await octokit.issues.addLabels( { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: +issue, + labels: [ '[Status] In Progress' ], + } ); + } +} + +module.exports = assignFixedIssues; diff --git a/packages/project-management-automation/lib/debug.js b/packages/project-management-automation/lib/debug.js new file mode 100644 index 00000000000000..63bc88bca3c046 --- /dev/null +++ b/packages/project-management-automation/lib/debug.js @@ -0,0 +1,12 @@ +/** + * Prints a debug message to STDOUT in non-testing environments. + * + * @param {string} message The message to print. + */ +function debug( message ) { + if ( process.env.NODE_ENV !== 'test' ) { + process.stdout.write( message + '\n' ); + } +} + +module.exports = debug; diff --git a/packages/project-management-automation/lib/index.js b/packages/project-management-automation/lib/index.js new file mode 100644 index 00000000000000..b752af33433c76 --- /dev/null +++ b/packages/project-management-automation/lib/index.js @@ -0,0 +1,56 @@ +/** + * GitHub dependencies + */ +const { setFailed, getInput } = require( '@actions/core' ); +const { context, GitHub } = require( '@actions/github' ); + +/** + * Internal dependencies + */ +const assignFixedIssues = require( './assign-fixed-issues' ); +const addFirstTimeContributorLabel = require( './add-first-time-contributor-label' ); +const addMilestone = require( './add-milestone' ); +const debug = require( './debug' ); + +const automations = [ + { + event: 'pull_request', + action: 'opened', + task: assignFixedIssues, + }, + { + event: 'pull_request', + action: 'opened', + task: addFirstTimeContributorLabel, + }, + { + event: 'pull_request', + action: 'closed', + task: addMilestone, + }, +]; + +( async function main() { + const token = getInput( 'github_token' ); + if ( ! token ) { + setFailed( 'main: Input `github_token` is required' ); + return; + } + + const octokit = new GitHub( token ); + + debug( `main: Received event = '${ context.eventName }', action = '${ context.payload.action }'` ); + + for ( const { event, action, task } of automations ) { + if ( event === context.eventName && action === context.payload.action ) { + try { + debug( `main: Starting task ${ task.name }` ); + await task( context.payload, octokit ); + } catch ( error ) { + debug( `main: Task ${ task.name } failed with error: ${ error }` ); + } + } + } + + debug( 'main: All done!' ); +}() ); diff --git a/packages/project-management-automation/lib/test/add-first-time-contributor-label.js b/packages/project-management-automation/lib/test/add-first-time-contributor-label.js new file mode 100644 index 00000000000000..91eeb56dca339d --- /dev/null +++ b/packages/project-management-automation/lib/test/add-first-time-contributor-label.js @@ -0,0 +1,62 @@ +/** + * Internal dependencies + */ +import addFirstTimeContributorLabel from '../add-first-time-contributor-label'; + +describe( 'addFirstTimeContributorLabel', () => { + const payload = { + pull_request: { + user: { + login: 'matt', + }, + number: 123, + }, + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + + it( 'does nothing if the user has commits', async () => { + const octokit = { + search: { + commits: jest.fn( () => Promise.resolve( { total_count: 100 } ) ), + }, + issues: { + addLabels: jest.fn(), + }, + }; + + await addFirstTimeContributorLabel( payload, octokit ); + + expect( octokit.search.commits ).toHaveBeenCalledWith( { + q: 'repo:WordPress/gutenberg+author:matt', + } ); + expect( octokit.issues.addLabels ).not.toHaveBeenCalled(); + } ); + + it( 'adds the label if the user does not have commits', async () => { + const octokit = { + search: { + commits: jest.fn( () => Promise.resolve( { total_count: 0 } ) ), + }, + issues: { + addLabels: jest.fn(), + }, + }; + + await addFirstTimeContributorLabel( payload, octokit ); + + expect( octokit.search.commits ).toHaveBeenCalledWith( { + q: 'repo:WordPress/gutenberg+author:matt', + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + labels: [ 'First-time Contributor' ], + } ); + } ); +} ); diff --git a/packages/project-management-automation/lib/test/add-milestone.js b/packages/project-management-automation/lib/test/add-milestone.js new file mode 100644 index 00000000000000..edb5478d107a18 --- /dev/null +++ b/packages/project-management-automation/lib/test/add-milestone.js @@ -0,0 +1,242 @@ +/** + * Internal dependencies + */ +import addMilestone from '../add-milestone'; + +describe( 'addFirstTimeContributorLabel', () => { + it( 'does nothing if PR is not merged', async () => { + const payload = { + pull_request: { + merged: false, + }, + }; + const octokit = { + issues: { + get: jest.fn(), + createMilestone: jest.fn(), + listMilestonesForRepo: jest.fn(), + update: jest.fn(), + }, + repos: { + getContents: jest.fn(), + }, + }; + + await addMilestone( payload, octokit ); + + expect( octokit.issues.get ).not.toHaveBeenCalled(); + expect( octokit.issues.createMilestone ).not.toHaveBeenCalled(); + expect( octokit.issues.listMilestonesForRepo ).not.toHaveBeenCalled(); + expect( octokit.issues.update ).not.toHaveBeenCalled(); + expect( octokit.repos.getContents ).not.toHaveBeenCalled(); + } ); + + it( 'does nothing if base is not master', async () => { + const payload = { + pull_request: { + merged: true, + base: { + ref: 'release/5.0', + }, + }, + }; + const octokit = { + issues: { + get: jest.fn(), + createMilestone: jest.fn(), + listMilestonesForRepo: jest.fn(), + update: jest.fn(), + }, + repos: { + getContents: jest.fn(), + }, + }; + + await addMilestone( payload, octokit ); + + expect( octokit.issues.get ).not.toHaveBeenCalled(); + expect( octokit.issues.createMilestone ).not.toHaveBeenCalled(); + expect( octokit.issues.listMilestonesForRepo ).not.toHaveBeenCalled(); + expect( octokit.issues.update ).not.toHaveBeenCalled(); + expect( octokit.repos.getContents ).not.toHaveBeenCalled(); + } ); + + it( 'does nothing if PR already has a milestone', async () => { + const payload = { + pull_request: { + merged: true, + base: { + ref: 'master', + }, + number: 123, + }, + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + const octokit = { + issues: { + get: jest.fn( () => Promise.resolve( { + milestone: 'Gutenberg 6.4', + } ) ), + createMilestone: jest.fn(), + listMilestonesForRepo: jest.fn(), + update: jest.fn(), + }, + repos: { + getContents: jest.fn(), + }, + }; + + await addMilestone( payload, octokit ); + + expect( octokit.issues.get ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + } ); + expect( octokit.issues.createMilestone ).not.toHaveBeenCalled(); + expect( octokit.issues.listMilestonesForRepo ).not.toHaveBeenCalled(); + expect( octokit.issues.update ).not.toHaveBeenCalled(); + expect( octokit.repos.getContents ).not.toHaveBeenCalled(); + } ); + + it( 'correctly milestones PRs when `package.json` has a `*.[1-8]` version', async () => { + const payload = { + pull_request: { + merged: true, + base: { + ref: 'master', + }, + number: 123, + }, + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + const octokit = { + issues: { + get: jest.fn( () => Promise.resolve( { + milestone: null, + } ) ), + createMilestone: jest.fn(), + listMilestonesForRepo: jest.fn( () => Promise.resolve( [ + { title: 'Gutenberg 6.2', number: 10 }, + { title: 'Gutenberg 6.3', number: 11 }, + { title: 'Gutenberg 6.4', number: 12 }, + ] ) ), + update: jest.fn(), + }, + repos: { + getContents: jest.fn( () => Promise.resolve( { + content: JSON.stringify( { + version: '6.3.0', + } ), + } ) ), + }, + }; + + await addMilestone( payload, octokit ); + + expect( octokit.issues.get ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + } ); + expect( octokit.repos.getContents ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + path: 'package.json', + } ); + expect( octokit.issues.createMilestone ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + title: 'Gutenberg 6.4', + due_on: '2019-08-26T00:00:00.000Z', + } ); + expect( octokit.issues.listMilestonesForRepo ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + } ); + expect( octokit.issues.update ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + milestone: 12, + } ); + } ); + + it( 'correctly milestones PRs when `package.json` has a `*.9` version', async () => { + const payload = { + pull_request: { + merged: true, + base: { + ref: 'master', + }, + number: 123, + }, + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + const octokit = { + issues: { + get: jest.fn( () => Promise.resolve( { + milestone: null, + } ) ), + createMilestone: jest.fn(), + listMilestonesForRepo: jest.fn( () => Promise.resolve( [ + { title: 'Gutenberg 6.8', number: 10 }, + { title: 'Gutenberg 6.9', number: 11 }, + { title: 'Gutenberg 7.0', number: 12 }, + ] ) ), + update: jest.fn(), + }, + repos: { + getContents: jest.fn( () => Promise.resolve( { + content: JSON.stringify( { + version: '6.9.0', + } ), + } ) ), + }, + }; + + await addMilestone( payload, octokit ); + + expect( octokit.issues.get ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + } ); + expect( octokit.repos.getContents ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + path: 'package.json', + } ); + expect( octokit.issues.createMilestone ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + title: 'Gutenberg 7.0', + due_on: '2019-11-18T00:00:00.000Z', + } ); + expect( octokit.issues.listMilestonesForRepo ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + } ); + expect( octokit.issues.update ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + milestone: 12, + } ); + } ); +} ); diff --git a/packages/project-management-automation/lib/test/assign-fixed-issues.js b/packages/project-management-automation/lib/test/assign-fixed-issues.js new file mode 100644 index 00000000000000..3af702875e464b --- /dev/null +++ b/packages/project-management-automation/lib/test/assign-fixed-issues.js @@ -0,0 +1,75 @@ +/** + * Internal dependencies + */ +import assignFixedIssues from '../assign-fixed-issues'; + +describe( 'assignFixedIssues', () => { + it( 'does nothing if there are no fixed issues', async () => { + const payload = { + pull_request: { + body: 'This pull request seeks to make Gutenberg better than ever.', + }, + }; + const octokit = { + issues: { + addAssignees: jest.fn(), + addLabels: jest.fn(), + }, + }; + + await assignFixedIssues( payload, octokit ); + + expect( octokit.issues.addAssignees ).not.toHaveBeenCalled(); + expect( octokit.issues.addLabels ).not.toHaveBeenCalled(); + } ); + + it( 'assigns and labels fixed issues', async () => { + const payload = { + pull_request: { + body: 'This pull request seeks to close #123 and also fixes https://github.com/WordPress/gutenberg/issues/456.', + user: { + login: 'matt', + }, + }, + repository: { + owner: { + login: 'WordPress', + }, + name: 'gutenberg', + }, + }; + const octokit = { + issues: { + addAssignees: jest.fn( () => Promise.resolve( {} ) ), + addLabels: jest.fn( () => Promise.resolve( {} ) ), + }, + }; + + await assignFixedIssues( payload, octokit ); + + expect( octokit.issues.addAssignees ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + assignees: [ 'matt' ], + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 123, + labels: [ '[Status] In Progress' ], + } ); + expect( octokit.issues.addAssignees ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 456, + assignees: [ 'matt' ], + } ); + expect( octokit.issues.addLabels ).toHaveBeenCalledWith( { + owner: 'WordPress', + repo: 'gutenberg', + issue_number: 456, + labels: [ '[Status] In Progress' ], + } ); + } ); +} ); diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json new file mode 100644 index 00000000000000..3fa89f1f691237 --- /dev/null +++ b/packages/project-management-automation/package.json @@ -0,0 +1,28 @@ +{ + "name": "@wordpress/project-management-automation", + "version": "1.0.0-beta.0", + "description": "GitHub Action that implements various automation to assist with managing the Gutenberg GitHub repository.", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/master/packages/project-management-automation/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/project-management-automation" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "main": "lib/index.js", + "dependencies": { + "@actions/core": "^1.0.0", + "@actions/github": "^1.0.0", + "@babel/runtime": "^7.4.4" + }, + "publishConfig": { + "access": "public" + } +} From 6ecff182908d5cc3f8555879f612f75113629f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ella=20van=C2=A0Durpe?= Date: Thu, 22 Aug 2019 09:47:48 +0200 Subject: [PATCH 40/52] Writing Flow: allow undo of patterns with BACKSPACE and ESC (#14776) * RichText/Patterns/Input Interaction: allow undo of patterns with BACKSPACE and ESC * Only run input rules when inserting text * Fix typo * Simplify --- .../developers/data/data-core-block-editor.md | 12 ++++ .../src/components/rich-text/index.js | 26 ++++++++ packages/block-editor/src/store/actions.js | 14 +++++ packages/block-editor/src/store/reducer.js | 13 ++++ packages/block-editor/src/store/selectors.js | 11 ++++ .../__snapshots__/rich-text.test.js.snap | 10 +++ .../blocks/__snapshots__/list.test.js.snap | 16 +++++ packages/e2e-tests/specs/blocks/list.test.js | 41 ++++++++++++ packages/e2e-tests/specs/rich-text.test.js | 36 +++++++++++ packages/rich-text/src/component/index.js | 62 +++++++++++++------ 10 files changed, 222 insertions(+), 19 deletions(-) diff --git a/docs/designers-developers/developers/data/data-core-block-editor.md b/docs/designers-developers/developers/data/data-core-block-editor.md index f4a9d048f5e5bb..f6f80cdfec5352 100644 --- a/docs/designers-developers/developers/data/data-core-block-editor.md +++ b/docs/designers-developers/developers/data/data-core-block-editor.md @@ -20,6 +20,18 @@ _Returns_ - `boolean`: Whether the given block type is allowed to be inserted. +# **didAutomaticChange** + +Returns true if the last change was an automatic change, false otherwise. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether the last change was automatic. + # **getAdjacentBlockClientId** Returns the client ID of the block adjacent one at the given reference diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 51af2159ec92f5..fdf10696cbeda2 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -64,6 +64,7 @@ class RichTextWrapper extends Component { this.onPaste = this.onPaste.bind( this ); this.onDelete = this.onDelete.bind( this ); this.inputRule = this.inputRule.bind( this ); + this.markAutomaticChange = this.markAutomaticChange.bind( this ); } onEnter( { value, onChange, shiftKey } ) { @@ -81,6 +82,7 @@ class RichTextWrapper extends Component { onReplace( [ transformation.transform( { content: value.text } ), ] ); + this.markAutomaticChange(); } } @@ -273,6 +275,7 @@ class RichTextWrapper extends Component { const block = transformation.transform( content ); onReplace( [ block ] ); + this.markAutomaticChange(); } getAllowedFormats() { @@ -293,6 +296,16 @@ class RichTextWrapper extends Component { return formattingControls.map( ( name ) => `core/${ name }` ); } + /** + * Marks the last change as an automatic change at the next idle period to + * ensure all selection changes have been recorded. + */ + markAutomaticChange() { + window.requestIdleCallback( () => { + this.props.markAutomaticChange(); + } ); + } + render() { const { children, @@ -313,6 +326,10 @@ class RichTextWrapper extends Component { onExitFormattedText, isSelected: originalIsSelected, onCreateUndoLevel, + // eslint-disable-next-line no-unused-vars + markAutomaticChange, + didAutomaticChange, + undo, placeholder, // eslint-disable-next-line no-unused-vars allowedFormats, @@ -375,6 +392,9 @@ class RichTextWrapper extends Component { __unstableOnEnterFormattedText={ onEnterFormattedText } __unstableOnExitFormattedText={ onExitFormattedText } __unstableOnCreateUndoLevel={ onCreateUndoLevel } + __unstableMarkAutomaticChange={ this.markAutomaticChange } + __unstableDidAutomaticChange={ didAutomaticChange } + __unstableUndo={ undo } > { ( { isSelected, value, onChange, Editable } ) => <> @@ -435,6 +455,7 @@ const RichTextContainer = compose( [ getSelectionStart, getSelectionEnd, getSettings, + didAutomaticChange, } = select( 'core/block-editor' ); const selectionStart = getSelectionStart(); @@ -453,6 +474,7 @@ const RichTextContainer = compose( [ selectionStart: isSelected ? selectionStart.offset : undefined, selectionEnd: isSelected ? selectionEnd.offset : undefined, isSelected, + didAutomaticChange: didAutomaticChange(), }; } ), withDispatch( ( dispatch, { @@ -465,7 +487,9 @@ const RichTextContainer = compose( [ enterFormattedText, exitFormattedText, selectionChange, + __unstableMarkAutomaticChange, } = dispatch( 'core/block-editor' ); + const { undo } = dispatch( 'core/editor' ); return { onCreateUndoLevel: __unstableMarkLastChangeAsPersistent, @@ -474,6 +498,8 @@ const RichTextContainer = compose( [ onSelectionChange( start, end ) { selectionChange( clientId, identifier, start, end ); }, + markAutomaticChange: __unstableMarkAutomaticChange, + undo, }; } ), withFilters( 'experimentalRichText' ), diff --git a/packages/block-editor/src/store/actions.js b/packages/block-editor/src/store/actions.js index fd7d3dbbe03295..8bebe25de7057c 100644 --- a/packages/block-editor/src/store/actions.js +++ b/packages/block-editor/src/store/actions.js @@ -711,6 +711,20 @@ export function __unstableMarkLastChangeAsPersistent() { return { type: 'MARK_LAST_CHANGE_AS_PERSISTENT' }; } +/** + * Returns an action object used in signalling that the last block change is + * an automatic change, meaning it was not performed by the user, and can be + * undone using the `Escape` and `Backspace` keys. This action must be called + * after the change was made, and any actions that are a consequence of it, so + * it is recommended to be called at the next idle period to ensure all + * selection changes have been recorded. + * + * @return {Object} Action object. + */ +export function __unstableMarkAutomaticChange() { + return { type: 'MARK_AUTOMATIC_CHANGE' }; +} + /** * Returns an action object used to enable or disable the navigation mode. * diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index ca277cb1e43dd9..33a40dc81af1ad 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1251,6 +1251,18 @@ export function lastBlockAttributesChange( state, action ) { return null; } +/** + * Reducer returning automatic change state. + * + * @param {boolean} state Current state. + * @param {Object} action Dispatched action. + * + * @return {boolean} Updated state. + */ +export function didAutomaticChange( state, action ) { + return action.type === 'MARK_AUTOMATIC_CHANGE'; +} + export default combineReducers( { blocks, isTyping, @@ -1264,4 +1276,5 @@ export default combineReducers( { preferences, lastBlockAttributesChange, isNavigationMode, + didAutomaticChange, } ); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 792b27542f7a5b..9bafe5b832844a 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -1415,3 +1415,14 @@ function getReusableBlocks( state ) { export function isNavigationMode( state ) { return state.isNavigationMode; } + +/** + * Returns true if the last change was an automatic change, false otherwise. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the last change was automatic. + */ +export function didAutomaticChange( state ) { + return state.didAutomaticChange; +} 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 c2013529d0a3f8..78b78101a59a5d 100644 --- a/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap +++ b/packages/e2e-tests/specs/__snapshots__/rich-text.test.js.snap @@ -48,6 +48,10 @@ exports[`RichText should not lose selection direction 1`] = ` " `; +exports[`RichText should not undo backtick transform with backspace after selection change 1`] = `""`; + +exports[`RichText should not undo backtick transform with backspace after typing 1`] = `""`; + exports[`RichText should only mutate text data on input 1`] = ` "

    1234

    @@ -77,3 +81,9 @@ exports[`RichText should update internal selection after fresh focus 1`] = `

    12

    " `; + +exports[`RichText should undo backtick transform with backspace 1`] = ` +" +

    \`a\`

    +" +`; diff --git a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap index 9ce876ee4aebec..afb80c7420d2dc 100644 --- a/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap +++ b/packages/e2e-tests/specs/blocks/__snapshots__/list.test.js.snap @@ -200,6 +200,10 @@ exports[`List should not transform lines in block when transforming multiple blo " `; +exports[`List should not undo asterisk transform with backspace after selection change 1`] = `""`; + +exports[`List should not undo asterisk transform with backspace after typing 1`] = `""`; + exports[`List should outdent with children 1`] = ` "
    • a
      • b
        • c
    @@ -267,3 +271,15 @@ exports[`List should split into two with paragraph and merge lists 3`] = `
    • two
    " `; + +exports[`List should undo asterisk transform with backspace 1`] = ` +" +

    *

    +" +`; + +exports[`List should undo asterisk transform with escape 1`] = ` +" +

    *

    +" +`; diff --git a/packages/e2e-tests/specs/blocks/list.test.js b/packages/e2e-tests/specs/blocks/list.test.js index 54d726aca6ae84..b81010b5026a49 100644 --- a/packages/e2e-tests/specs/blocks/list.test.js +++ b/packages/e2e-tests/specs/blocks/list.test.js @@ -55,6 +55,47 @@ describe( 'List', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'should undo asterisk transform with backspace', async () => { + await clickBlockAppender(); + await page.keyboard.type( '* ' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should undo asterisk transform with escape', async () => { + await clickBlockAppender(); + await page.keyboard.type( '* ' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.press( 'Escape' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should not undo asterisk transform with backspace after typing', async () => { + await clickBlockAppender(); + await page.keyboard.type( '* a' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); + + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should not undo asterisk transform with backspace after selection change', async () => { + await clickBlockAppender(); + await page.keyboard.press( 'Enter' ); + await page.keyboard.type( '* ' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowDown' ); + await page.keyboard.press( 'Backspace' ); + + // Expect list to be deleted + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + it( 'can be created by typing "/list"', async () => { // Create a list with the slash block shortcut. await clickBlockAppender(); diff --git a/packages/e2e-tests/specs/rich-text.test.js b/packages/e2e-tests/specs/rich-text.test.js index 4a45faa12a3011..9c00e0d062c14a 100644 --- a/packages/e2e-tests/specs/rich-text.test.js +++ b/packages/e2e-tests/specs/rich-text.test.js @@ -103,6 +103,42 @@ describe( 'RichText', () => { expect( await getEditedPostContent() ).toMatchSnapshot(); } ); + it( 'should undo backtick transform with backspace', async () => { + await clickBlockAppender(); + await page.keyboard.type( '`a`' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.press( 'Backspace' ); + + // Expect "`a`" to be restored. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should not undo backtick transform with backspace after typing', async () => { + await clickBlockAppender(); + await page.keyboard.type( '`a`' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + await page.keyboard.type( 'b' ); + await page.keyboard.press( 'Backspace' ); + await page.keyboard.press( 'Backspace' ); + + // Expect "a" to be deleted. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + + it( 'should not undo backtick transform with backspace after selection change', async () => { + await clickBlockAppender(); + await page.keyboard.type( '`a`' ); + await page.evaluate( () => new Promise( window.requestIdleCallback ) ); + // Move inside format boundary. + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowLeft' ); + await page.keyboard.press( 'ArrowRight' ); + await page.keyboard.press( 'Backspace' ); + + // Expect "a" to be deleted. + expect( await getEditedPostContent() ).toMatchSnapshot(); + } ); + it( 'should not format text after code backtick', async () => { await clickBlockAppender(); await page.keyboard.type( 'A `backtick` and more.' ); diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index f75aceb79cc598..880549ab617ea6 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -12,7 +12,7 @@ import { * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE } from '@wordpress/keycodes'; +import { BACKSPACE, DELETE, ENTER, LEFT, RIGHT, SPACE, ESCAPE } from '@wordpress/keycodes'; import { withSelect } from '@wordpress/data'; import { withSafeTimeout, compose } from '@wordpress/compose'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -320,19 +320,21 @@ class RichText extends Component { return; } - if ( event && event.nativeEvent.inputType ) { - const { inputType } = event.nativeEvent; + let inputType; - // The browser formatted something or tried to insert HTML. - // Overwrite it. It will be handled later by the format library if - // needed. - if ( - inputType.indexOf( 'format' ) === 0 || - INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) - ) { - this.applyRecord( this.record ); - return; - } + if ( event ) { + inputType = event.nativeEvent.inputType; + } + + // The browser formatted something or tried to insert HTML. + // Overwrite it. It will be handled later by the format library if + // needed. + if ( inputType && ( + inputType.indexOf( 'format' ) === 0 || + INSERTION_INPUT_TYPES_TO_IGNORE.has( inputType ) + ) ) { + this.applyRecord( this.record ); + return; } const value = this.createRecord(); @@ -348,7 +350,22 @@ class RichText extends Component { this.onChange( change, { withoutHistory: true } ); - const { __unstableInputRule: inputRule, formatTypes } = this.props; + const { + __unstableInputRule: inputRule, + __unstableMarkAutomaticChange: markAutomaticChange, + formatTypes, + setTimeout, + clearTimeout, + } = this.props; + + // Create an undo level when input stops for over a second. + clearTimeout( this.onInput.timeout ); + this.onInput.timeout = setTimeout( this.onCreateUndoLevel, 1000 ); + + // Only run input rules when inserting text. + if ( inputType !== 'insertText' ) { + return; + } if ( inputRule ) { inputRule( change, this.valueToFormat ); @@ -365,11 +382,8 @@ class RichText extends Component { if ( transformed !== change ) { this.onCreateUndoLevel(); this.onChange( { ...transformed, activeFormats } ); + markAutomaticChange(); } - - // Create an undo level when input stops for over a second. - this.props.clearTimeout( this.onInput.timeout ); - this.onInput.timeout = this.props.setTimeout( this.onCreateUndoLevel, 1000 ); } onCompositionEnd() { @@ -518,7 +532,17 @@ class RichText extends Component { handleDelete( event ) { const { keyCode } = event; - if ( keyCode !== DELETE && keyCode !== BACKSPACE ) { + if ( keyCode !== DELETE && keyCode !== BACKSPACE && keyCode !== ESCAPE ) { + return; + } + + if ( this.props.__unstableDidAutomaticChange ) { + event.preventDefault(); + this.props.__unstableUndo(); + return; + } + + if ( keyCode === ESCAPE ) { return; } From ab70dc25f5d0d790b887e7cfd5286ffc8019a2cb Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Thu, 22 Aug 2019 09:44:41 +0100 Subject: [PATCH 41/52] Apply box-sizing border-box properly to the notices components (#17066) --- packages/components/src/notice/style.scss | 1 + packages/components/src/snackbar/style.scss | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/components/src/notice/style.scss b/packages/components/src/notice/style.scss index 8474146bbec680..760b9f9fc3be1d 100644 --- a/packages/components/src/notice/style.scss +++ b/packages/components/src/notice/style.scss @@ -66,6 +66,7 @@ .components-notice-list { // The notice should never be wider than the viewport, or the close button might be hidden. Especially relevant at high zoom levels. Related to https://core.trac.wordpress.org/ticket/47603#ticket. max-width: 100vw; + box-sizing: border-box; z-index: z-index(".components-notice-list"); .components-notice__content { diff --git a/packages/components/src/snackbar/style.scss b/packages/components/src/snackbar/style.scss index 4357685ffa0cbd..013d7abed012e8 100644 --- a/packages/components/src/snackbar/style.scss +++ b/packages/components/src/snackbar/style.scss @@ -8,6 +8,7 @@ padding: 16px 24px; width: 100%; max-width: 600px; + box-sizing: border-box; cursor: pointer; @include break-small() { @@ -54,6 +55,7 @@ position: absolute; z-index: z-index(".components-snackbar-list"); width: 100%; + box-sizing: border-box; } .components-snackbar-list__notice-container { From 635108e9d29b907aeb7aa82a103473ad5601d64c Mon Sep 17 00:00:00 2001 From: etoledom Date: Wed, 28 Aug 2019 10:31:39 +0200 Subject: [PATCH 42/52] [RNMobile] Native mobile release v1.11.0 (#17181) * [RNMobile] Fix crash when adding separator * Build: remove global install of latest npm since we want to use the paired node/npm version (#17134) * Build: remove global install of latest npm since we want to use the paired node/npm version * Also update travis to remove --latest-npm flag * [RNMobile] Try dark mode (iOS) (#17067) * Adding dark mode component implemented on list and list block * Adding DarkMode handling to RichText, ToolBar and SafeArea * Mobile: Using DarkMode as HOC * iOS DarkMode: Modified colors on block list and block container * iOS DarkMode: Improved Header Toolbar colors * iOS DarkMode: Removing background from buttons * iOS DarkMode warning and unsupported * iOS DarkMode: MediaPlaceholder * iOS DarkMode: BottomSheets * iOS DarkMode: Inserter * iOS DarkMode: DefaultBlockAppender * iOS DarkMode: PostTite * Update hardcoded colors with variables * iOS DarkMode: Fix bottom-sheet cell value color * iOS DarkMode: More - PageBreak - Add Block Here * iOS DarkMode: Better text color * iOS Darkmode: Code block * iOS DarkMode: HTML View * iOS DarkMode: Improve colors on SafeArea * Fix toolbar not avoiding keyboard regression * Fix native unit tests * Fix gutenberg-mobile unit tests * Adding RNDarkMode mocks * RNMobile: Fix crash when viewing HTML on iOS * [RNMobile] Remove toolbar from html view * [RNMobile] Fix MaxListenersExceededWarning caused by dark-mode event emitter (#17186) * Fix MaxListenersExceededWarning caused by dark-mode event emitter * Checking for setMaxListeners trying to avoid CI error * Adding remove listener to DarkMode HOC * DarkMode: Binding this.onModeChanged to `this` * DarkMode: Adding conditional needed to pass UI Tests on CI * Fix focus title on new posts regression (#17180) * BottomSheet: Setting DashIcon color directly when theme is default (light) (#17193) --- .../components/block-list/block.native.scss | 2 - .../src/components/block-list/index.native.js | 16 +++--- .../components/block-list/style.native.scss | 18 ++++++- .../default-block-appender/style.native.scss | 1 - .../src/components/inserter/index.native.js | 12 +++-- .../src/components/inserter/menu.native.js | 14 +++-- .../src/components/inserter/style.native.scss | 17 ++++++ .../media-placeholder/index.native.js | 12 +++-- .../media-placeholder/styles.native.scss | 8 +++ .../src/components/warning/index.native.js | 18 ++++--- .../src/components/warning/style.native.scss | 12 +++++ .../block-library/src/code/edit.native.js | 13 +++-- .../block-library/src/code/theme.native.scss | 11 ++++ .../block-library/src/image/edit.native.js | 9 +++- .../src/image/styles.native.scss | 4 ++ .../block-library/src/missing/edit.native.js | 19 ++++--- .../src/missing/style.native.scss | 12 +++++ .../block-library/src/more/edit.native.js | 11 ++-- .../block-library/src/more/editor.native.scss | 12 ++++- .../block-library/src/nextpage/edit.native.js | 11 ++-- .../src/nextpage/editor.native.scss | 13 ++++- packages/block-library/src/separator/edit.js | 28 ++++------ .../src/separator/separator-settings.js | 26 +++++++++ .../separator/separator-settings.native.js | 3 ++ .../block-library/src/video/edit.native.js | 9 +++- .../block-library/src/video/style.native.scss | 4 ++ .../components/src/button/index.native.js | 2 +- packages/components/src/index.native.js | 1 + .../src/mobile/bottom-sheet/cell.native.js | 29 +++++++--- .../src/mobile/bottom-sheet/index.native.js | 20 ++++--- .../mobile/bottom-sheet/styles.native.scss | 24 +++++++++ .../src/mobile/dark-mode/index.native.js | 53 +++++++++++++++++++ .../mobile/html-text-input/index.native.js | 8 ++- .../html-text-input/style-common.native.scss | 10 +++- .../mobile/html-text-input/style.android.scss | 2 - .../src/mobile/html-text-input/style.ios.scss | 6 ++- .../src/primitives/svg/style.native.scss | 14 ++++- .../components/src/toolbar/style.native.scss | 4 ++ .../src/toolbar/toolbar-container.native.js | 5 +- .../header/header-toolbar/index.native.js | 6 ++- .../header/header-toolbar/style.native.scss | 5 ++ .../src/components/layout/index.native.js | 28 +++++----- .../src/components/layout/style.native.scss | 13 +++++ .../components/visual-editor/index.native.js | 8 +-- .../visual-editor/style.native.scss | 4 ++ packages/edit-post/src/editor.native.js | 21 ++++++++ .../src/components/provider/index.native.js | 17 ------ .../src/link/test/modal.native.js | 8 +-- .../rich-text/src/component/index.native.js | 15 ++++-- .../rich-text/src/component/style.native.scss | 14 ++++- .../__mocks__/react-native-dark-mode/index.js | 0 test/native/__mocks__/styleMock.js | 11 +++- test/native/setup.js | 9 ++++ 53 files changed, 507 insertions(+), 145 deletions(-) create mode 100644 packages/block-library/src/separator/separator-settings.js create mode 100644 packages/block-library/src/separator/separator-settings.native.js create mode 100644 packages/components/src/mobile/dark-mode/index.native.js create mode 100644 test/native/__mocks__/react-native-dark-mode/index.js diff --git a/packages/block-editor/src/components/block-list/block.native.scss b/packages/block-editor/src/components/block-list/block.native.scss index b997cddc723073..a7dbae3b46516b 100644 --- a/packages/block-editor/src/components/block-list/block.native.scss +++ b/packages/block-editor/src/components/block-list/block.native.scss @@ -3,7 +3,6 @@ } .blockContainer { - background-color: $white; padding-left: 16px; padding-right: 16px; padding-top: 12px; @@ -11,7 +10,6 @@ } .blockContainerFocused { - background-color: $white; padding-left: 16px; padding-right: 16px; padding-top: 12px; diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index facef0cc5e01ca..10a5cfcbcf1bae 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { KeyboardAwareFlatList, ReadableContentView } from '@wordpress/components'; +import { KeyboardAwareFlatList, ReadableContentView, useStyle, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -103,7 +103,7 @@ export class BlockList extends Component { innerRef={ this.scrollViewInnerRef } extraScrollHeight={ innerToolbarHeight + 10 } keyboardShouldPersistTaps="always" - style={ styles.list } + style={ useStyle( styles.list, styles.listDark, this.context ) } data={ this.props.blockClientIds } extraData={ [ this.props.isFullyBordered ] } keyExtractor={ identity } @@ -126,6 +126,7 @@ export class BlockList extends Component { } renderItem( { item: clientId, index } ) { + const blockHolderFocusedStyle = useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark, this.props.theme ); const { shouldShowBlockAtIndex, shouldShowInsertionPoint } = this.props; return ( @@ -138,18 +139,20 @@ export class BlockList extends Component { rootClientId={ this.props.rootClientId } onCaretVerticalPositionChange={ this.onCaretVerticalPositionChange } borderStyle={ this.blockHolderBorderStyle() } - focusedBorderColor={ styles.blockHolderFocused.borderColor } + focusedBorderColor={ blockHolderFocusedStyle } /> ) } ); } renderAddBlockSeparator() { + const lineStyle = useStyle( styles.lineStyleAddHere, styles.lineStyleAddHereDark, this.props.theme ); + const labelStyle = useStyle( styles.labelStyleAddHere, styles.labelStyleAddHereDark, this.props.theme ); return ( - - { __( 'ADD BLOCK HERE' ) } - + + { __( 'ADD BLOCK HERE' ) } + ); } @@ -228,5 +231,6 @@ export default compose( [ replaceBlock, }; } ), + withTheme, ] )( BlockList ); diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index b6961600b95c67..e1f8e96abc948f 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -8,6 +8,10 @@ flex: 1; } +.listDark { + background: #1c1c1e; +} + .switch { flex-direction: row; justify-content: flex-start; @@ -26,6 +30,10 @@ height: 2px; } +.lineStyleAddHereDark { + background-color: $gray-50; +} + .labelStyleAddHere { flex: 1; text-align: center; @@ -34,9 +42,12 @@ font-weight: bold; } +.labelStyleAddHereDark { + color: $gray-20; +} + .containerStyleAddHere { flex-direction: row; - background-color: $white; } .blockHolderSemiBordered { @@ -54,7 +65,6 @@ } .blockContainerFocused { - background-color: $white; padding-left: 16; padding-right: 16; padding-top: 12; @@ -65,6 +75,10 @@ border-color: $gray-lighten-30; } +.blockHolderFocusedDark { + border-color: $gray-70; +} + .blockListFooter { height: 80px; } diff --git a/packages/block-editor/src/components/default-block-appender/style.native.scss b/packages/block-editor/src/components/default-block-appender/style.native.scss index 5193611fa45d58..9e2ffd2f293b01 100644 --- a/packages/block-editor/src/components/default-block-appender/style.native.scss +++ b/packages/block-editor/src/components/default-block-appender/style.native.scss @@ -5,7 +5,6 @@ } .blockContainer { - background-color: $white; padding-top: 0; padding-left: 16px; padding-right: 16px; diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js index b20eb8387992d6..1a757a65a502b2 100644 --- a/packages/block-editor/src/components/inserter/index.native.js +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Dropdown, ToolbarButton, Dashicon } from '@wordpress/components'; +import { Dropdown, ToolbarButton, Dashicon, withTheme, useStyle } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -14,10 +14,10 @@ import { getUnregisteredTypeHandlerName } from '@wordpress/blocks'; import styles from './style.scss'; import InserterMenu from './menu'; -const defaultRenderToggle = ( { onToggle, disabled } ) => ( +const defaultRenderToggle = ( { onToggle, disabled, style } ) => ( ) } + icon={ ( ) } onClick={ onToggle } extraProps={ { hint: __( 'Double tap to add a block' ) } } isDisabled={ disabled } @@ -56,9 +56,10 @@ class Inserter extends Component { const { disabled, renderToggle = defaultRenderToggle, + theme, } = this.props; - - return renderToggle( { onToggle, isOpen, disabled } ); + const style = useStyle( styles.addBlockButton, styles.addBlockButtonDark, theme ); + return renderToggle( { onToggle, isOpen, disabled, style } ); } /** @@ -118,4 +119,5 @@ export default compose( [ items: inserterItems.filter( ( { name } ) => name !== getUnregisteredTypeHandlerName() ), }; } ), + withTheme, ] )( Inserter ); diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 7870f41954afe3..3b0e27a156ddb8 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -14,7 +14,7 @@ import { } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { withInstanceId, compose } from '@wordpress/compose'; -import { BottomSheet, Icon } from '@wordpress/components'; +import { BottomSheet, Icon, withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies @@ -63,6 +63,9 @@ export class InserterMenu extends Component { render() { const numberOfColumns = this.calculateNumberOfColumns(); const bottomPadding = styles.contentBottomPadding; + const modalIconWrapperStyle = useStyle( styles.modalIconWrapper, styles.modalIconWrapperDark, this.props.theme ); + const modalIconStyle = useStyle( styles.modalIcon, styles.modalIconDark, this.props.theme ); + const modalItemLabelStyle = useStyle( styles.modalItemLabel, styles.modalItemLabelDark, this.props.theme ); return ( this.props.onSelect( item ) }> - - - + + + - { item.title } + { item.title } } @@ -213,4 +216,5 @@ export default compose( }; } ), withInstanceId, + withTheme, )( InserterMenu ); diff --git a/packages/block-editor/src/components/inserter/style.native.scss b/packages/block-editor/src/components/inserter/style.native.scss index e10b685dda406c..899b3f676827db 100644 --- a/packages/block-editor/src/components/inserter/style.native.scss +++ b/packages/block-editor/src/components/inserter/style.native.scss @@ -37,6 +37,10 @@ align-items: center; } +.modalIconWrapperDark { + background-color: rgba($white, 0.07); +} + .modalIcon { width: 32px; height: 32px; @@ -45,6 +49,10 @@ fill: $gray-dark; } +.modalIconDark { + fill: $white; +} + .modalItemLabel { background-color: transparent; padding-left: 2; @@ -56,9 +64,18 @@ color: $gray-dark; } +.modalItemLabelDark { + color: $white; +} + .addBlockButton { color: $blue-wordpress; border: 2px; border-radius: 10px; border-color: $blue-wordpress; } + +.addBlockButtonDark { + color: $blue-30; + border-color: $blue-30; +} diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index a19a28588200c9..e04361b71f3307 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -8,6 +8,7 @@ import { View, Text, TouchableWithoutFeedback } from 'react-native'; */ import { __, sprintf } from '@wordpress/i18n'; import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/block-editor'; +import { withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies @@ -15,7 +16,7 @@ import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/bloc import styles from './styles.scss'; function MediaPlaceholder( props ) { - const { mediaType, labels = {}, icon, onSelectURL } = props; + const { mediaType, labels = {}, icon, onSelectURL, theme } = props; const isImage = MEDIA_TYPE_IMAGE === mediaType; const isVideo = MEDIA_TYPE_VIDEO === mediaType; @@ -46,6 +47,9 @@ function MediaPlaceholder( props ) { accessibilityHint = __( 'Double tap to select a video' ); } + const emptyStateContainerStyle = useStyle( styles.emptyStateContainer, styles.emptyStateContainerDark, theme ); + const emptyStateTitleStyle = useStyle( styles.emptyStateTitle, styles.emptyStateTitleDark, theme ); + return ( - + { getMediaOptions() } { icon } - + { placeholderTitle } @@ -83,4 +87,4 @@ function MediaPlaceholder( props ) { ); } -export default MediaPlaceholder; +export default withTheme( MediaPlaceholder ); diff --git a/packages/block-editor/src/components/media-placeholder/styles.native.scss b/packages/block-editor/src/components/media-placeholder/styles.native.scss index f76be4b8f36a42..7a192f078e2090 100644 --- a/packages/block-editor/src/components/media-placeholder/styles.native.scss +++ b/packages/block-editor/src/components/media-placeholder/styles.native.scss @@ -11,6 +11,10 @@ padding-bottom: 12; } +.emptyStateContainerDark { + background-color: $background-dark-secondary; +} + .emptyStateTitle { text-align: center; margin-top: 8; @@ -19,6 +23,10 @@ color: #2e4453; } +.emptyStateTitleDark { + color: $white; +} + .emptyStateDescription { text-align: center; color: $blue-wordpress; diff --git a/packages/block-editor/src/components/warning/index.native.js b/packages/block-editor/src/components/warning/index.native.js index 7d0cced3408cca..071fd30a0738a3 100644 --- a/packages/block-editor/src/components/warning/index.native.js +++ b/packages/block-editor/src/components/warning/index.native.js @@ -6,7 +6,7 @@ import { View, Text } from 'react-native'; /** * WordPress dependencies */ -import { Icon } from '@wordpress/components'; +import { Icon, withTheme, useStyle } from '@wordpress/components'; import { normalizeIconObject } from '@wordpress/blocks'; /** @@ -14,29 +14,33 @@ import { normalizeIconObject } from '@wordpress/blocks'; */ import styles from './style.scss'; -function Warning( { title, message, icon, iconClass, ...viewProps } ) { +function Warning( { title, message, icon, iconClass, theme, ...viewProps } ) { icon = icon && normalizeIconObject( icon ); + const internalIconClass = 'warning-icon' + '-' + theme; + const titleStyle = useStyle( styles.title, styles.titleDark, theme ); + const messageStyle = useStyle( styles.message, styles.messageDark, theme ); + return ( { icon && ( ) } { title && ( - { title } + { title } ) } { message && ( - { message } + { message } ) } ); } -export default Warning; +export default withTheme( Warning ); diff --git a/packages/block-editor/src/components/warning/style.native.scss b/packages/block-editor/src/components/warning/style.native.scss index a96045ada1fd55..2bfc52da454d25 100644 --- a/packages/block-editor/src/components/warning/style.native.scss +++ b/packages/block-editor/src/components/warning/style.native.scss @@ -10,6 +10,10 @@ justify-content: center; } +.containerDark { + background-color: $background-dark-secondary; +} + .icon { width: 24px; height: 24px; @@ -24,8 +28,16 @@ color: $gray-dark; } +.titleDark { + color: $white; +} + .message { text-align: center; color: $gray-text-min; font-size: 12; } + +.messageDark { + color: $white; +} diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 3f021790c2a403..0363fcfebc61d0 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -6,12 +6,13 @@ import { View } from 'react-native'; /** * WordPress dependencies */ +import { PlainText } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; +import { withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies */ -import { PlainText } from '@wordpress/block-editor'; import { escape, unescape } from './utils'; /** @@ -21,14 +22,16 @@ import styles from './theme.scss'; // Note: styling is applied directly to the (nested) PlainText component. Web-side components // apply it to the container 'div' but we don't have a proper proposal for cascading styling yet. -export default function CodeEdit( props ) { - const { attributes, setAttributes, style, onFocus, onBlur } = props; +export function CodeEdit( props ) { + const { attributes, setAttributes, style, onFocus, onBlur, theme } = props; + const codeStyle = useStyle( styles.blockCode, styles.blockCodeDark, theme ); + const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark, theme ); return ( setAttributes( { content: escape( content ) } ) } @@ -38,8 +41,10 @@ export default function CodeEdit( props ) { onFocus={ onFocus } onBlur={ onBlur } fontFamily={ ( styles.blockCode.fontFamily ) } + placeholderTextColor={ placeholderStyle.color } /> </View> ); } +export default withTheme( CodeEdit ); diff --git a/packages/block-library/src/code/theme.native.scss b/packages/block-library/src/code/theme.native.scss index 668b9f92dd1f53..40a4ba9bfcbfdf 100644 --- a/packages/block-library/src/code/theme.native.scss +++ b/packages/block-library/src/code/theme.native.scss @@ -4,3 +4,14 @@ font-family: $default-monospace-font; } +.blockCodeDark { + color: $white; +} + +.placeholder { + color: $gray; +} + +.placeholderDark { + color: $gray-50; +} diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 3eb19a6bfa991b..a21d4bb72830f5 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -19,7 +19,10 @@ import { Icon, Toolbar, ToolbarButton, + withTheme, + useStyle, } from '@wordpress/components'; + import { Caption, MediaPlaceholder, @@ -192,11 +195,13 @@ class ImageEdit extends React.Component { } getIcon( isRetryIcon ) { + const iconStyle = useStyle( styles.icon, styles.iconDark, this.props.theme ); + if ( isRetryIcon ) { return <Icon icon={ SvgIconRetry } { ...styles.iconRetry } />; } - return <Icon icon={ SvgIcon } { ...styles.icon } />; + return <Icon icon={ SvgIcon } { ...iconStyle } />; } render() { @@ -368,4 +373,4 @@ class ImageEdit extends React.Component { } } -export default ImageEdit; +export default withTheme( ImageEdit ); diff --git a/packages/block-library/src/image/styles.native.scss b/packages/block-library/src/image/styles.native.scss index 81578bd734ba3e..aaafc3ae5457c8 100644 --- a/packages/block-library/src/image/styles.native.scss +++ b/packages/block-library/src/image/styles.native.scss @@ -45,3 +45,7 @@ width: 100%; height: 100%; } + +.iconDark { + fill: $white; +} diff --git a/packages/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js index 838c64d88b56c4..c5a604144b7e7b 100644 --- a/packages/block-library/src/missing/edit.native.js +++ b/packages/block-library/src/missing/edit.native.js @@ -6,7 +6,7 @@ import { View, Text } from 'react-native'; /** * WordPress dependencies */ -import { Icon } from '@wordpress/components'; +import { Icon, withTheme, useStyle } from '@wordpress/components'; import { coreBlocks } from '@wordpress/block-library'; import { normalizeIconObject } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; @@ -17,18 +17,25 @@ import { __ } from '@wordpress/i18n'; */ import styles from './style.scss'; -export default class UnsupportedBlockEdit extends Component { +export class UnsupportedBlockEdit extends Component { render() { const { originalName } = this.props.attributes; + const theme = this.props.theme; const blockType = coreBlocks[ originalName ]; + const title = blockType ? blockType.settings.title : __( 'Unsupported' ); - const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; + const titleStyle = useStyle( styles.unsupportedBlockMessage, styles.unsupportedBlockMessageDark, theme ); + const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; + const iconStyle = useStyle( styles.unsupportedBlockIcon, styles.unsupportedBlockIconDark, theme ); + const iconClassName = 'unsupported-icon' + '-' + theme; return ( - <View style={ styles.unsupportedBlock }> - <Icon className="unsupported-icon" icon={ icon && icon.src ? icon.src : icon } /> - <Text style={ styles.unsupportedBlockMessage }>{ title }</Text> + <View style={ useStyle( styles.unsupportedBlock, styles.unsupportedBlockDark, theme ) }> + <Icon className={ iconClassName } icon={ icon && icon.src ? icon.src : icon } color={ iconStyle.color } /> + <Text style={ titleStyle }>{ title }</Text> </View> ); } } + +export default withTheme( UnsupportedBlockEdit ); diff --git a/packages/block-library/src/missing/style.native.scss b/packages/block-library/src/missing/style.native.scss index 95a86122d70983..c3f9c3a935c630 100644 --- a/packages/block-library/src/missing/style.native.scss +++ b/packages/block-library/src/missing/style.native.scss @@ -10,13 +10,25 @@ justify-content: center; } +.unsupportedBlockDark { + background-color: $background-dark-secondary; +} + .unsupportedBlockIcon { color: $gray-dark; } +.unsupportedBlockIconDark { + color: $white; +} + .unsupportedBlockMessage { margin-top: 2; text-align: center; color: $gray-dark; font-size: 14; } + +.unsupportedBlockMessageDark { + color: $white; +} diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index 8b369284cb5962..e177213a7f1f6f 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -9,13 +9,14 @@ import Hr from 'react-native-hr'; */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; +import { withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies */ import styles from './editor.scss'; -export default class MoreEdit extends Component { +export class MoreEdit extends Component { constructor() { super( ...arguments ); @@ -28,6 +29,8 @@ export default class MoreEdit extends Component { const { customText } = this.props.attributes; const { defaultText } = this.state; const content = customText || defaultText; + const textStyle = useStyle( styles.moreText, styles.moreTextDark, this.props.theme ); + const lineStyle = useStyle( styles.moreLine, styles.moreLineDark, this.props.theme ); return ( <View> @@ -35,10 +38,12 @@ export default class MoreEdit extends Component { text={ content } marginLeft={ 0 } marginRight={ 0 } - textStyle={ styles[ 'block-library-more__text' ] } - lineStyle={ styles[ 'block-library-more__line' ] } + textStyle={ textStyle } + lineStyle={ lineStyle } /> </View> ); } } + +export default withTheme( MoreEdit ); diff --git a/packages/block-library/src/more/editor.native.scss b/packages/block-library/src/more/editor.native.scss index eb4a1d60d9431f..b0dece50736e6b 100644 --- a/packages/block-library/src/more/editor.native.scss +++ b/packages/block-library/src/more/editor.native.scss @@ -1,13 +1,21 @@ // @format -.block-library-more__line { +.moreLine { background-color: $gray-lighten-20; height: 2; } -.block-library-more__text { +.moreLineDark { + background-color: $gray-50; +} + +.moreText { color: $gray; text-decoration-style: solid; text-transform: uppercase; } +.moreTextDark { + color: $gray-20; +} + diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js index e3aa69b15e5e41..be9ad283576401 100644 --- a/packages/block-library/src/nextpage/edit.native.js +++ b/packages/block-library/src/nextpage/edit.native.js @@ -8,16 +8,19 @@ import Hr from 'react-native-hr'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; +import { withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies */ import styles from './editor.scss'; -export default function NextPageEdit( { attributes, isSelected, onFocus } ) { +export function NextPageEdit( { attributes, isSelected, onFocus, theme } ) { const { customText = __( 'Page break' ) } = attributes; const accessibilityTitle = attributes.customText || ''; const accessibilityState = isSelected ? [ 'selected' ] : []; + const textStyle = useStyle( styles.nextpageText, styles.nextpageTextDark, theme ); + const lineStyle = useStyle( styles.nextpageLine, styles.nextpageLineDark, theme ); return ( <View @@ -35,8 +38,10 @@ export default function NextPageEdit( { attributes, isSelected, onFocus } ) { <Hr text={ customText } marginLeft={ 0 } marginRight={ 0 } - textStyle={ styles[ 'block-library-nextpage__text' ] } - lineStyle={ styles[ 'block-library-nextpage__line' ] } /> + textStyle={ textStyle } + lineStyle={ lineStyle } /> </View> ); } + +export default withTheme( NextPageEdit ); diff --git a/packages/block-library/src/nextpage/editor.native.scss b/packages/block-library/src/nextpage/editor.native.scss index 869851fdd37c63..01ed97ac577473 100644 --- a/packages/block-library/src/nextpage/editor.native.scss +++ b/packages/block-library/src/nextpage/editor.native.scss @@ -1,12 +1,21 @@ // @format -.block-library-nextpage__line { +.nextpageLine { background-color: $gray-lighten-20; height: 2; } -.block-library-nextpage__text { +.nextpageLineDark { + background-color: $gray-50; +} + +.nextpageText { color: $gray; text-decoration-style: solid; text-transform: uppercase; } + +.nextpageTextDark { + color: $gray-20; +} + diff --git a/packages/block-library/src/separator/edit.js b/packages/block-library/src/separator/edit.js index 050bd7423fb665..30dd0e85d71549 100644 --- a/packages/block-library/src/separator/edit.js +++ b/packages/block-library/src/separator/edit.js @@ -6,13 +6,12 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; import { HorizontalRule } from '@wordpress/components'; -import { - InspectorControls, - withColors, - PanelColorSettings, -} from '@wordpress/block-editor'; +import { withColors } from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import SeparatorSettings from './separator-settings'; function SeparatorEdit( { color, setColor, className } ) { return ( @@ -29,19 +28,10 @@ function SeparatorEdit( { color, setColor, className } ) { color: color.color, } } /> - <InspectorControls> - <PanelColorSettings - title={ __( 'Color Settings' ) } - colorSettings={ [ - { - value: color.color, - onChange: setColor, - label: __( 'Color' ), - }, - ] } - > - </PanelColorSettings> - </InspectorControls> + <SeparatorSettings + color={ color } + setColor={ setColor } + /> </> ); } diff --git a/packages/block-library/src/separator/separator-settings.js b/packages/block-library/src/separator/separator-settings.js new file mode 100644 index 00000000000000..bb3a3a57aa1fa0 --- /dev/null +++ b/packages/block-library/src/separator/separator-settings.js @@ -0,0 +1,26 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { + InspectorControls, + PanelColorSettings, +} from '@wordpress/block-editor'; + +const SeparatorSettings = ( { color, setColor } ) => ( + <InspectorControls> + <PanelColorSettings + title={ __( 'Color Settings' ) } + colorSettings={ [ + { + value: color.color, + onChange: setColor, + label: __( 'Color' ), + }, + ] } + > + </PanelColorSettings> + </InspectorControls> +); + +export default SeparatorSettings; diff --git a/packages/block-library/src/separator/separator-settings.native.js b/packages/block-library/src/separator/separator-settings.native.js new file mode 100644 index 00000000000000..d2bdd8ef6443a3 --- /dev/null +++ b/packages/block-library/src/separator/separator-settings.native.js @@ -0,0 +1,3 @@ +// Mobile has no separator settings at this time, so render nothing +const SeparatorSettings = () => null; +export default SeparatorSettings; diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 3b0a9d186d3084..9d004832d37f69 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -20,7 +20,10 @@ import { Icon, Toolbar, ToolbarButton, + withTheme, + useStyle, } from '@wordpress/components'; + import { Caption, MediaPlaceholder, @@ -147,11 +150,13 @@ class VideoEdit extends React.Component { } getIcon( isRetryIcon, isMediaPlaceholder ) { + const iconStyle = useStyle( style.icon, style.iconDark, this.props.theme ); + if ( isRetryIcon ) { return <Icon icon={ SvgIconRetry } { ...style.icon } />; } - return <Icon icon={ SvgIcon } { ...( ! isMediaPlaceholder ? style.iconUploading : style.icon ) } />; + return <Icon icon={ SvgIcon } { ...( ! isMediaPlaceholder ? style.iconUploading : iconStyle ) } />; } render() { @@ -262,4 +267,4 @@ class VideoEdit extends React.Component { } } -export default VideoEdit; +export default withTheme( VideoEdit ); diff --git a/packages/block-library/src/video/style.native.scss b/packages/block-library/src/video/style.native.scss index dd4d70ae0a4752..5eb36be46605e3 100644 --- a/packages/block-library/src/video/style.native.scss +++ b/packages/block-library/src/video/style.native.scss @@ -59,6 +59,10 @@ height: 100%; } +.iconDark { + fill: $white; +} + .iconUploading { fill: $gray-lighten-20; width: 100%; diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index 7a90dd53131661..78d10226e26def 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -20,7 +20,6 @@ const styles = StyleSheet.create( { justifyContent: 'center', alignItems: 'center', aspectRatio: 1, - backgroundColor: 'white', }, buttonActive: { flex: 1, @@ -63,6 +62,7 @@ export default function Button( props ) { } = props; const isDisabled = ariaDisabled || disabled; + const buttonViewStyle = { opacity: isDisabled ? 0.2 : 1, ...( ariaPressed ? styles.buttonActive : styles.buttonInactive ), diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 4984cdde2fd36d..d09b157160003c 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -27,3 +27,4 @@ export { default as KeyboardAvoidingView } from './mobile/keyboard-avoiding-view export { default as KeyboardAwareFlatList } from './mobile/keyboard-aware-flat-list'; export { default as Picker } from './mobile/picker'; export { default as ReadableContentView } from './mobile/readable-content-view'; +export * from './mobile/dark-mode'; diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index cebfc6a91c1826..cbc10928478704 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -16,8 +16,10 @@ import { __, _x, sprintf } from '@wordpress/i18n'; */ import styles from './styles.scss'; import platformStyles from './cellStyles.scss'; +// `useStyle as getStyle`: Hack to avoid lint thinking this is a React Hook +import { withTheme, useStyle as getStyle } from '../dark-mode'; -export default class BottomSheetCell extends Component { +class BottomSheetCell extends Component { constructor( props ) { super( ...arguments ); this.state = { @@ -48,12 +50,15 @@ export default class BottomSheetCell extends Component { editable = true, separatorType, style = {}, + theme, ...valueProps } = this.props; const showValue = value !== undefined; const isValueEditable = editable && onChangeValue !== undefined; - const defaultLabelStyle = showValue || icon !== undefined ? styles.cellLabel : styles.cellLabelCentered; + const cellLabelStyle = getStyle( styles.cellLabel, styles.cellTextDark, theme ); + const cellLabelCenteredStyle = getStyle( styles.cellLabelCentered, styles.cellTextDark, theme ); + const defaultLabelStyle = showValue || icon !== undefined ? cellLabelStyle : cellLabelCenteredStyle; const drawSeparator = ( separatorType && separatorType !== 'none' ) || separatorStyle === undefined; const onCellPress = () => { @@ -75,22 +80,26 @@ export default class BottomSheetCell extends Component { }; const separatorStyle = () => { - const leftMarginStyle = { ...styles.cellSeparator, ...platformStyles.separatorMarginLeft }; + //eslint-disable-next-line @wordpress/no-unused-vars-before-return + const defaultSeparatorStyle = getStyle( styles.separator, styles.separatorDark, theme ); + const cellSeparatorStyle = getStyle( styles.cellSeparator, styles.cellSeparatorDark, theme ); + const leftMarginStyle = { ...cellSeparatorStyle, ...platformStyles.separatorMarginLeft }; switch ( separatorType ) { case 'leftMargin': return leftMarginStyle; case 'fullWidth': - return styles.separator; + return defaultSeparatorStyle; case 'none': return undefined; case undefined: - return showValue ? leftMarginStyle : styles.separator; + return showValue ? leftMarginStyle : defaultSeparatorStyle; } }; const getValueComponent = () => { const styleRTL = I18nManager.isRTL && styles.cellValueRTL; - const finalStyle = { ...styles.cellValue, ...valueStyle, ...styleRTL }; + const cellValueStyle = getStyle( styles.cellValue, styles.cellTextDark, theme ); + const finalStyle = { ...cellValueStyle, ...valueStyle, ...styleRTL }; // To be able to show the `middle` ellipsizeMode on editable cells // we show the TextInput just when the user wants to edit the value, @@ -114,7 +123,7 @@ export default class BottomSheetCell extends Component { /> ) : ( <Text - style={ { ...styles.cellValue, ...valueStyle } } + style={ { ...cellValueStyle, ...valueStyle } } numberOfLines={ 1 } ellipsizeMode={ 'middle' } > @@ -142,6 +151,8 @@ export default class BottomSheetCell extends Component { ); }; + const iconStyle = getStyle( styles.icon, styles.iconDark, theme ); + return ( <TouchableOpacity accessible={ ! this.state.isEditingValue } @@ -159,7 +170,7 @@ export default class BottomSheetCell extends Component { <View style={ styles.cellRowContainer }> { icon && ( <View style={ styles.cellRowContainer }> - <Dashicon icon={ icon } size={ 24 } /> + <Dashicon icon={ icon } size={ 24 } color={ iconStyle.color } /> <View style={ platformStyles.labelIconSeparator } /> </View> ) } @@ -177,3 +188,5 @@ export default class BottomSheetCell extends Component { ); } } + +export default withTheme( BottomSheetCell ); diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 9106709cea25ef..e3209713e49449 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -19,6 +19,7 @@ import Cell from './cell'; import PickerCell from './picker-cell'; import SwitchCell from './switch-cell'; import KeyboardAvoidingView from './keyboard-avoiding-view'; +import { withTheme, useStyle } from '../dark-mode'; class BottomSheet extends Component { constructor() { @@ -63,6 +64,7 @@ class BottomSheet extends Component { hideHeader, style = {}, contentStyle = {}, + theme, } = this.props; const panResponder = PanResponder.create( { @@ -118,6 +120,8 @@ class BottomSheet extends Component { }, }; + const backgroundStyle = useStyle( styles.background, styles.backgroundDark, theme ); + return ( <Modal isVisible={ isVisible } @@ -139,7 +143,7 @@ class BottomSheet extends Component { > <KeyboardAvoidingView behavior={ Platform.OS === 'ios' && 'padding' } - style={ { ...styles.background, borderColor: 'rgba(0, 0, 0, 0.1)', ...style } } + style={ { ...backgroundStyle, borderColor: 'rgba(0, 0, 0, 0.1)', ...style } } keyboardVerticalOffset={ -this.state.safeAreaBottomInset } > <View style={ styles.dragIndicator } /> @@ -160,10 +164,12 @@ function getWidth() { return Math.min( Dimensions.get( 'window' ).width, styles.background.maxWidth ); } -BottomSheet.getWidth = getWidth; -BottomSheet.Button = Button; -BottomSheet.Cell = Cell; -BottomSheet.PickerCell = PickerCell; -BottomSheet.SwitchCell = SwitchCell; +const ThemedBottomSheet = withTheme( BottomSheet ); + +ThemedBottomSheet.getWidth = getWidth; +ThemedBottomSheet.Button = Button; +ThemedBottomSheet.Cell = Cell; +ThemedBottomSheet.PickerCell = PickerCell; +ThemedBottomSheet.SwitchCell = SwitchCell; -export default BottomSheet; +export default ThemedBottomSheet; diff --git a/packages/components/src/mobile/bottom-sheet/styles.native.scss b/packages/components/src/mobile/bottom-sheet/styles.native.scss index 53764ee4fe38f1..8f153715c16705 100644 --- a/packages/components/src/mobile/bottom-sheet/styles.native.scss +++ b/packages/components/src/mobile/bottom-sheet/styles.native.scss @@ -21,6 +21,10 @@ width: 100%; } +.separatorDark { + background-color: $gray-70; +} + .emptyHeaderSpace { height: 14; } @@ -34,6 +38,10 @@ padding-bottom: 0; } +.backgroundDark { + background-color: $background-dark-elevated; +} + .content { padding: 0 16px 0 16px; } @@ -86,6 +94,10 @@ width: 100%; } +.cellSeparatorDark { + background-color: $gray-70; +} + .cellRowContainer { flex-direction: row; align-items: center; @@ -115,6 +127,18 @@ flex: 1; } +.cellTextDark { + color: $white; +} + .cellValueRTL { text-align: left; } + +.icon { + color: #7b9ab1; +} + +.iconDark { + color: #c3c4c7; +} diff --git a/packages/components/src/mobile/dark-mode/index.native.js b/packages/components/src/mobile/dark-mode/index.native.js new file mode 100644 index 00000000000000..d2b13020f027f9 --- /dev/null +++ b/packages/components/src/mobile/dark-mode/index.native.js @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import { eventEmitter, initialMode } from 'react-native-dark-mode'; +import React from 'react'; + +// This was failing on CI +if ( eventEmitter.setMaxListeners ) { + eventEmitter.setMaxListeners( 150 ); +} + +export function useStyle( light, dark, theme ) { + const finalDark = { + ...light, + ...dark, + }; + + return theme === 'dark' ? finalDark : light; +} + +// This function takes a component... +export function withTheme( WrappedComponent ) { + return class extends React.Component { + constructor( props ) { + super( props ); + + this.onModeChanged = this.onModeChanged.bind( this ); + + this.state = { + mode: initialMode, + }; + } + + onModeChanged( newMode ) { + this.setState( { mode: newMode } ); + } + + componentDidMount() { + this.subscription = eventEmitter.on( 'currentModeChanged', this.onModeChanged ); + } + + componentWillUnmount() { + // Conditional needed to pass UI Tests on CI + if ( eventEmitter.removeListener ) { + eventEmitter.removeListener( 'currentModeChanged', this.onModeChanged ); + } + } + + render() { + return <WrappedComponent theme={ this.state.mode } { ...this.props } />; + } + }; +} diff --git a/packages/components/src/mobile/html-text-input/index.native.js b/packages/components/src/mobile/html-text-input/index.native.js index e25f7a1af71c4d..1408be65c90530 100644 --- a/packages/components/src/mobile/html-text-input/index.native.js +++ b/packages/components/src/mobile/html-text-input/index.native.js @@ -15,6 +15,7 @@ import { withInstanceId, compose } from '@wordpress/compose'; /** * Internal dependencies */ +import { withTheme, useStyle } from '../dark-mode'; import HTMLInputContainer from './container'; import styles from './style.scss'; @@ -60,6 +61,8 @@ export class HTMLTextInput extends Component { } render() { + const htmlStyle = useStyle( styles.htmlView, styles.htmlViewDark, this.props.theme ); + const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark, this.props.theme ); return ( <HTMLInputContainer parentHeight={ this.props.parentHeight }> <TextInput @@ -70,6 +73,7 @@ export class HTMLTextInput extends Component { style={ styles.htmlViewTitle } value={ this.props.title } placeholder={ __( 'Add title' ) } + placeholderTextColor={ placeholderStyle.color } onChangeText={ this.props.editTitle } /> <TextInput @@ -77,11 +81,12 @@ export class HTMLTextInput extends Component { accessibilityLabel="html-view-content" textAlignVertical="top" multiline - style={ styles.htmlView } + style={ htmlStyle } value={ this.state.value } onChangeText={ this.edit } onBlur={ this.stopEditing } placeholder={ __( 'Start writing…' ) } + placeholderTextColor={ placeholderStyle.color } scrollEnabled={ HTMLInputContainer.scrollEnabled } /> </HTMLInputContainer> @@ -117,4 +122,5 @@ export default compose( [ }; } ), withInstanceId, + withTheme, ] )( HTMLTextInput ); diff --git a/packages/components/src/mobile/html-text-input/style-common.native.scss b/packages/components/src/mobile/html-text-input/style-common.native.scss index 4db5b985161402..c1ac9f155d4c79 100644 --- a/packages/components/src/mobile/html-text-input/style-common.native.scss +++ b/packages/components/src/mobile/html-text-input/style-common.native.scss @@ -1,6 +1,6 @@ $padding: 8; -$backgroundColor: $white; $htmlFont: $default-monospace-font; +$textColorDark: $white; .keyboardAvoidingView { position: absolute; @@ -13,3 +13,11 @@ $htmlFont: $default-monospace-font; .container { flex: 1; } + +.placeholder { + color: $gray; +} + +.placeholderDark { + color: $gray-50; +} diff --git a/packages/components/src/mobile/html-text-input/style.android.scss b/packages/components/src/mobile/html-text-input/style.android.scss index 10594358722c37..1dca01274d75b5 100644 --- a/packages/components/src/mobile/html-text-input/style.android.scss +++ b/packages/components/src/mobile/html-text-input/style.android.scss @@ -2,7 +2,6 @@ .htmlView { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; @@ -11,7 +10,6 @@ .htmlViewTitle { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; diff --git a/packages/components/src/mobile/html-text-input/style.ios.scss b/packages/components/src/mobile/html-text-input/style.ios.scss index 8b13392b95a9ae..97cf00a7512ff5 100644 --- a/packages/components/src/mobile/html-text-input/style.ios.scss +++ b/packages/components/src/mobile/html-text-input/style.ios.scss @@ -4,15 +4,17 @@ $title-height: 32; .htmlView { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-bottom: $title-height + $padding; } +.htmlViewDark { + color: $textColorDark; +} + .htmlViewTitle { font-family: $htmlFont; - background-color: $backgroundColor; padding-left: $padding; padding-right: $padding; padding-top: $padding; diff --git a/packages/components/src/primitives/svg/style.native.scss b/packages/components/src/primitives/svg/style.native.scss index e5a64ea140d0d8..595372b06329e1 100644 --- a/packages/components/src/primitives/svg/style.native.scss +++ b/packages/components/src/primitives/svg/style.native.scss @@ -13,12 +13,22 @@ fill: currentColor; } -.unsupported-icon { +.unsupported-icon-light { color: $gray-dark; fill: currentColor; } -.warning-icon { +.unsupported-icon-dark { + color: $white; + fill: currentColor; +} + +.warning-icon-light { color: $gray-dark; fill: currentColor; } + +.warning-icon-dark { + color: $white; + fill: currentColor; +} diff --git a/packages/components/src/toolbar/style.native.scss b/packages/components/src/toolbar/style.native.scss index 1e0d176e275d80..3038ea8e491d8d 100644 --- a/packages/components/src/toolbar/style.native.scss +++ b/packages/components/src/toolbar/style.native.scss @@ -5,3 +5,7 @@ padding-left: 5px; padding-right: 5px; } + +.containerDark { + border-color: #515459; +} diff --git a/packages/components/src/toolbar/toolbar-container.native.js b/packages/components/src/toolbar/toolbar-container.native.js index 887991d2ea1238..f9d019450266c2 100644 --- a/packages/components/src/toolbar/toolbar-container.native.js +++ b/packages/components/src/toolbar/toolbar-container.native.js @@ -7,11 +7,12 @@ import { View } from 'react-native'; * Internal dependencies */ import styles from './style.scss'; +import { withTheme, useStyle } from '../mobile/dark-mode'; const ToolbarContainer = ( props ) => ( - <View style={ [ styles.container, props.passedStyle ] }> + <View style={ [ useStyle( styles.container, styles.containerDark, props.theme ), props.passedStyle ] }> { props.children } </View> ); -export default ToolbarContainer; +export default withTheme( ToolbarContainer ); diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 7c33856a629180..97ca3a62349bc5 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -15,7 +15,7 @@ import { Inserter, BlockToolbar, } from '@wordpress/block-editor'; -import { Toolbar, ToolbarButton } from '@wordpress/components'; +import { Toolbar, ToolbarButton, useStyle, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -31,6 +31,7 @@ function HeaderToolbar( { showInserter, showKeyboardHideButton, clearSelectedBlock, + theme, } ) { const scrollViewRef = useRef( null ); const scrollToStart = () => { @@ -38,7 +39,7 @@ function HeaderToolbar( { }; return ( - <View style={ styles.container }> + <View style={ useStyle( styles.container, styles.containerDark, theme ) }> <ScrollView ref={ scrollViewRef } onContentSizeChange={ scrollToStart } @@ -99,4 +100,5 @@ export default compose( [ clearSelectedBlock: dispatch( 'core/block-editor' ).clearSelectedBlock, } ) ), withViewportMatch( { isLargeViewport: 'medium' } ), + withTheme, ] )( HeaderToolbar ); diff --git a/packages/edit-post/src/components/header/header-toolbar/style.native.scss b/packages/edit-post/src/components/header/header-toolbar/style.native.scss index 0aa03b90ed81cc..9210ce5506d620 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.native.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.native.scss @@ -7,6 +7,11 @@ border-top-width: 1px; } +.containerDark { + background-color: $background-dark-elevated; + border-top-color: $background-dark-elevated; +} + .scrollableContent { flex-grow: 1; // Fixes RTL issue on Android. } diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 3242628c021b89..5b6813067211d2 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -11,7 +11,7 @@ import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView } from '@wordpress/components'; +import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView, useStyle, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -75,7 +75,7 @@ class Layout extends Component { renderHTML() { return ( - <HTMLTextInput /> + <HTMLTextInput parentHeight={ this.state.rootViewHeight } /> ); } @@ -101,8 +101,10 @@ class Layout extends Component { mode, } = this.props; + const isHtmlView = mode === 'text'; + // add a margin view at the bottom for the header - const marginBottom = Platform.OS === 'android' ? headerToolbarStyles.container.height : 0; + const marginBottom = Platform.OS === 'android' && ! isHtmlView ? headerToolbarStyles.container.height : 0; const toolbarKeyboardAvoidingViewStyle = { ...styles.toolbarKeyboardAvoidingView, @@ -112,18 +114,19 @@ class Layout extends Component { }; return ( - <SafeAreaView style={ styles.container } onLayout={ this.onRootViewLayout }> - <View style={ { flex: 1 } }> - { mode === 'text' ? this.renderHTML() : this.renderVisual() } + <SafeAreaView style={ useStyle( styles.container, styles.containerDark, this.props.theme ) } onLayout={ this.onRootViewLayout }> + <View style={ useStyle( styles.background, styles.backgroundDark, this.props.theme ) }> + { isHtmlView ? this.renderHTML() : this.renderVisual() } </View> <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } }> </View> - <KeyboardAvoidingView - parentHeight={ this.state.rootViewHeight } - style={ toolbarKeyboardAvoidingViewStyle } - > - <Header /> - </KeyboardAvoidingView> + { ! isHtmlView && ( + <KeyboardAvoidingView + parentHeight={ this.state.rootViewHeight } + style={ toolbarKeyboardAvoidingViewStyle } + > + <Header /> + </KeyboardAvoidingView> ) } </SafeAreaView> ); } @@ -143,4 +146,5 @@ export default compose( [ mode: getEditorMode(), }; } ), + withTheme, ] )( Layout ); diff --git a/packages/edit-post/src/components/layout/style.native.scss b/packages/edit-post/src/components/layout/style.native.scss index e6d7e241bcd0df..7a5026d6664dc9 100644 --- a/packages/edit-post/src/components/layout/style.native.scss +++ b/packages/edit-post/src/components/layout/style.native.scss @@ -5,6 +5,19 @@ background-color: #fff; } +.containerDark { + background-color: $background-dark-elevated; +} + +.background { + flex: 1; + background-color: $white; +} + +.backgroundDark { + background-color: $black; +} + .toolbarKeyboardAvoidingView { position: absolute; bottom: 0; diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index 15ca4ed9d451bc..9d1925356a9138 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -7,7 +7,7 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { BlockList } from '@wordpress/block-editor'; import { PostTitle } from '@wordpress/editor'; -import { ReadableContentView } from '@wordpress/components'; +import { ReadableContentView, withTheme, useStyle } from '@wordpress/components'; /** * Internal dependencies @@ -20,8 +20,9 @@ class VisualEditor extends Component { editTitle, setTitleRef, title, + theme, } = this.props; - + const blockHolderFocusedStyle = useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark, theme ); return ( <ReadableContentView> <PostTitle @@ -34,7 +35,7 @@ class VisualEditor extends Component { styles.blockHolderFullBordered : styles.blockHolderSemiBordered } - focusedBorderColor={ styles.blockHolderFocused.borderColor } + focusedBorderColor={ blockHolderFocusedStyle.borderColor } accessibilityLabel="post-title" /> </ReadableContentView> @@ -81,4 +82,5 @@ export default compose( [ }, }; } ), + withTheme, ] )( VisualEditor ); diff --git a/packages/edit-post/src/components/visual-editor/style.native.scss b/packages/edit-post/src/components/visual-editor/style.native.scss index 02b49a1515584c..4ade220b5dd9e9 100644 --- a/packages/edit-post/src/components/visual-editor/style.native.scss +++ b/packages/edit-post/src/components/visual-editor/style.native.scss @@ -15,3 +15,7 @@ .blockHolderFocused { border-color: $gray-lighten-30; } + +.blockHolderFocusedDark { + border-color: $gray-70; +} diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 23e0bad6a9e60a..87acaaf96669c0 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -3,6 +3,7 @@ */ import memize from 'memize'; import { size, map, without } from 'lodash'; +import { subscribeSetFocusOnTitle } from 'react-native-gutenberg-bridge'; /** * WordPress dependencies @@ -31,6 +32,8 @@ class Editor extends Component { this.getEditorSettings = memize( this.getEditorSettings, { maxSize: 1, } ); + + this.setTitleRef = this.setTitleRef.bind( this ); } getEditorSettings( @@ -66,6 +69,24 @@ class Editor extends Component { return settings; } + componentDidMount() { + this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { + if ( this.postTitleRef ) { + this.postTitleRef.focus(); + } + } ); + } + + componentWillUnmount() { + if ( this.subscriptionParentSetFocusOnTitle ) { + this.subscriptionParentSetFocusOnTitle.remove(); + } + } + + setTitleRef( titleRef ) { + this.postTitleRef = titleRef; + } + render() { const { settings, diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index d19da119ef0ba5..7a055f438f4d91 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -5,7 +5,6 @@ import RNReactNativeGutenbergBridge, { subscribeParentGetHtml, subscribeParentToggleHTMLMode, subscribeUpdateHtml, - subscribeSetFocusOnTitle, subscribeSetTitle, } from 'react-native-gutenberg-bridge'; @@ -28,8 +27,6 @@ class NativeEditorProvider extends Component { // Keep a local reference to `post` to detect changes this.post = props.post; - - this.setTitleRef = this.setTitleRef.bind( this ); } componentDidMount() { @@ -48,12 +45,6 @@ class NativeEditorProvider extends Component { this.subscriptionParentUpdateHtml = subscribeUpdateHtml( ( payload ) => { this.updateHtmlAction( payload.html ); } ); - - this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { - if ( this.postTitleRef ) { - this.postTitleRef.focus(); - } - } ); } componentWillUnmount() { @@ -72,10 +63,6 @@ class NativeEditorProvider extends Component { if ( this.subscriptionParentUpdateHtml ) { this.subscriptionParentUpdateHtml.remove(); } - - if ( this.subscriptionParentSetFocusOnTitle ) { - this.subscriptionParentSetFocusOnTitle.remove(); - } } componentDidUpdate( prevProps ) { @@ -87,10 +74,6 @@ class NativeEditorProvider extends Component { } } - setTitleRef( titleRef ) { - this.postTitleRef = titleRef; - } - serializeToNativeAction() { if ( this.props.mode === 'text' ) { this.updateHtmlAction( this.props.getEditedPostContent() ); diff --git a/packages/format-library/src/link/test/modal.native.js b/packages/format-library/src/link/test/modal.native.js index 03594a4ef1c380..44153ea3a40185 100644 --- a/packages/format-library/src/link/test/modal.native.js +++ b/packages/format-library/src/link/test/modal.native.js @@ -23,7 +23,7 @@ describe( 'LinksUI', () => { onRemove={ onRemove } onClose={ jest.fn() } /> - ).dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI + ).dive().dive(); // -> dive() removes the HOC layer that was blocking access to ModalLinkUI // When @@ -52,8 +52,10 @@ describe( 'LinksUI', () => { // When // Simulate user typing on the URL Cell. - const bottomSheet = wrapper.find( 'BottomSheet' ).first(); - const cell = bottomSheet.find( 'BottomSheetCell' ).first(); + const bottomSheet = wrapper.dive().find( 'BottomSheet' ).first(); + // withTheme is type "_class", we search for it and dive into BottomSheetCell + const cell = bottomSheet.dive().find( '_class' ).first().dive(); + cell.simulate( 'changeValue', 'wordpress.com' ); // Close the BottomSheet diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index fe705d8ad5b386..6191e627d56962 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -20,6 +20,7 @@ import { childrenBlock } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; import { BACKSPACE } from '@wordpress/keycodes'; import { isURL } from '@wordpress/url'; +import { useStyle, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -770,25 +771,28 @@ export class RichText extends Component { style, __unstableIsSelected: isSelected, children, + theme, } = this.props; const record = this.getRecord(); const html = this.getHtmlToRender( record, tagName ); - let minHeight = styles[ 'rich-text' ].minHeight; + let minHeight = styles.richText.minHeight; if ( style && style.minHeight ) { minHeight = style.minHeight; } + const placeholderStyle = useStyle( styles.richTextPlaceholder, styles.richTextPlaceholderDark, theme ); + const { color: defaultPlaceholderTextColor, - } = styles[ 'rich-text-placeholder' ]; + } = placeholderStyle; const { color: defaultColor, textDecorationColor: defaultTextDecorationColor, fontFamily: defaultFontFamily, - } = styles[ 'rich-text' ]; + } = useStyle( styles.richText, styles.richTextDark, theme ); let selection = null; if ( this.needsSelectionUpdate ) { @@ -817,6 +821,8 @@ export class RichText extends Component { this.firedAfterTextChanged = false; } + const dynamicStyle = useStyle( style, styles.richTextDark, theme ); + return ( <View> { children && children( { @@ -833,7 +839,7 @@ export class RichText extends Component { } } } style={ { - ...style, + ...dynamicStyle, minHeight: Math.max( minHeight, this.state.height ), } } text={ { text: html, eventCount: this.lastEventCount, selection } } @@ -878,4 +884,5 @@ export default compose( [ withSelect( ( select ) => ( { formatTypes: select( 'core/rich-text' ).getFormatTypes(), } ) ), + withTheme, ] )( RichText ); diff --git a/packages/rich-text/src/component/style.native.scss b/packages/rich-text/src/component/style.native.scss index 6481c415694127..4ed93f7f70239d 100644 --- a/packages/rich-text/src/component/style.native.scss +++ b/packages/rich-text/src/component/style.native.scss @@ -1,11 +1,21 @@ -.rich-text { +.richText { font-family: $default-regular-font; min-height: $min-height-paragraph; color: $gray-900; text-decoration-color: $blue-500; } -.rich-text-placeholder { +.richTextDark { + color: $white; + text-decoration-color: $blue-30; + background-color: $black; +} + +.richTextPlaceholder { color: $gray; } + +.richTextPlaceholderDark { + color: $gray-50; +} diff --git a/test/native/__mocks__/react-native-dark-mode/index.js b/test/native/__mocks__/react-native-dark-mode/index.js new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/test/native/__mocks__/styleMock.js b/test/native/__mocks__/styleMock.js index 182af41388dc0a..4e63b494feff94 100644 --- a/test/native/__mocks__/styleMock.js +++ b/test/native/__mocks__/styleMock.js @@ -15,7 +15,7 @@ module.exports = { blockCode: { fontFamily: 'serif', }, - 'rich-text': { + richText: { fontFamily: 'serif', minHeight: 30, }, @@ -66,4 +66,13 @@ module.exports = { iconUploading: { fill: 'gray', }, + placeholder: { + color: 'gray', + }, + richTextPlaceholder: { + color: 'gray', + }, + unsupportedBlockIcon: { + color: 'white', + }, }; diff --git a/test/native/setup.js b/test/native/setup.js index cfd2417a11ad9d..c0e3e0942a9180 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -21,6 +21,15 @@ jest.mock( 'react-native-gutenberg-bridge', () => { }; } ); +jest.mock( 'react-native-dark-mode', () => { + return { + eventEmitter: { + on: jest.fn(), + }, + initialMode: 'light', + }; +} ); + jest.mock( 'react-native-modal', () => () => 'Modal' ); jest.mock( 'react-native-hr', () => () => 'Hr' ); From 643c1b2b3a43e2d977acdfe63b7f3eb14c0c53f1 Mon Sep 17 00:00:00 2001 From: etoledom <etoledom@icloud.com> Date: Wed, 28 Aug 2019 12:16:19 +0200 Subject: [PATCH 43/52] Activate Travis CI on rnmobile/master branch (#17229) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 3e16c463eb6ed5..4a9ab1b7fe9429 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ cache: branches: only: - master + - rnmobile/master before_install: - nvm install From a78f204df645ce86890814796b41498248f70dad Mon Sep 17 00:00:00 2001 From: Tugdual de Kerviler <dekervit@gmail.com> Date: Thu, 29 Aug 2019 16:53:06 +0200 Subject: [PATCH 44/52] Add native support for the MediaText block (#16305) * First working version of the MediaText component for native mobile * Fix adding a block to an innerblock list * Disable mediaText on production * MediaText native: improve editor visuals * Move BlockToolbar from BlockList to Layout * Remove BlockEditorProvider from BlockList and add native version of EditorProvider to Editor. Plus support InsertionPoint and BlockListAppender * Update BlockMover for native to hide if locked or if it's the only block * Make the vertical align button work, add more styling options for toolbar buttons * Make sure registerCoreBlocks does not break in production * Copy docblock comment from the web version for registerCoreBlocks * Fix focusing on the media placeholder * Only support adding image for now * Update usage of MediaPlaceholder in MediaContainer * Enable autoScroll for just the out most block list * Fix JS Unit tests * Roll back to IconButton refactor and fix tests * Fix BlockVerticalAlignmentToolbar buttons style on mobile * Fix thing for web and ensure ariaPressed is always passed down * Use AriaPressed directly to style SVG on mobile * Update snapshots --- .../src/components/block-icon/index.native.js | 30 +++ .../block-list-appender/index.native.js | 54 +++++ .../block-list-appender/style.native.scss | 9 + .../src/components/block-list/index.native.js | 1 + .../block-list/insertion-point.native.js | 46 +++++ .../components/block-mover/index.native.js | 82 ++++---- .../src/components/index.native.js | 3 + .../components/inner-blocks/index.native.js | 182 +++++++++++++++++ .../src/components/url-input/test/button.js | 8 +- packages/block-library/src/index.native.js | 42 +++- .../src/media-text/edit.native.js | 186 +++++++++++++++++ .../src/media-text/media-container.native.js | 191 ++++++++++++++++++ .../src/media-text/style.native.scss | 29 +++ packages/blocks/src/api/index.native.js | 4 + packages/components/src/icon-button/index.js | 6 +- .../components/src/icon-button/test/index.js | 2 +- packages/components/src/icon/index.js | 20 +- packages/components/src/icon/test/index.js | 10 +- .../keyboard-aware-flat-list/index.ios.js | 2 + .../src/primitives/svg/index.native.js | 4 +- .../src/primitives/svg/style.native.scss | 6 +- .../test/__snapshots__/index.js.snap | 45 +++-- .../components/visual-editor/index.native.js | 1 + 23 files changed, 879 insertions(+), 84 deletions(-) create mode 100644 packages/block-editor/src/components/block-icon/index.native.js create mode 100644 packages/block-editor/src/components/block-list-appender/index.native.js create mode 100644 packages/block-editor/src/components/block-list-appender/style.native.scss create mode 100644 packages/block-editor/src/components/block-list/insertion-point.native.js create mode 100644 packages/block-editor/src/components/inner-blocks/index.native.js create mode 100644 packages/block-library/src/media-text/edit.native.js create mode 100644 packages/block-library/src/media-text/media-container.native.js create mode 100644 packages/block-library/src/media-text/style.native.scss diff --git a/packages/block-editor/src/components/block-icon/index.native.js b/packages/block-editor/src/components/block-icon/index.native.js new file mode 100644 index 00000000000000..4dddda5ecce8f0 --- /dev/null +++ b/packages/block-editor/src/components/block-icon/index.native.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Path, Icon, SVG } from '@wordpress/components'; + +export default function BlockIcon( { icon, showColors = false } ) { + if ( get( icon, [ 'src' ] ) === 'block-default' ) { + icon = { + src: <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><Path d="M19 7h-1V5h-4v2h-4V5H6v2H5c-1.1 0-2 .9-2 2v10h18V9c0-1.1-.9-2-2-2zm0 10H5V9h14v8z" /></SVG>, + }; + } + + const renderedIcon = <Icon icon={ icon && icon.src ? icon.src : icon } />; + const style = showColors ? { + backgroundColor: icon && icon.background, + color: icon && icon.foreground, + } : {}; + + return ( + <View style={ style }> + { renderedIcon } + </View> + ); +} diff --git a/packages/block-editor/src/components/block-list-appender/index.native.js b/packages/block-editor/src/components/block-list-appender/index.native.js new file mode 100644 index 00000000000000..9a6a24c5575172 --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/index.native.js @@ -0,0 +1,54 @@ +/** + * External dependencies + */ +import { last } from 'lodash'; + +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { getDefaultBlockName } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import DefaultBlockAppender from '../default-block-appender'; +import styles from './style.scss'; + +function BlockListAppender( { + blockClientIds, + rootClientId, + canInsertDefaultBlock, + isLocked, +} ) { + if ( isLocked ) { + return null; + } + + if ( canInsertDefaultBlock ) { + return ( + <DefaultBlockAppender + rootClientId={ rootClientId } + lastBlockClientId={ last( blockClientIds ) } + containerStyle={ styles.blockListAppender } + placeholder={ blockClientIds.length > 0 ? '' : null } + /> + ); + } + + return null; +} + +export default withSelect( ( select, { rootClientId } ) => { + const { + getBlockOrder, + canInsertBlockType, + getTemplateLock, + } = select( 'core/block-editor' ); + + return { + isLocked: !! getTemplateLock( rootClientId ), + blockClientIds: getBlockOrder( rootClientId ), + canInsertDefaultBlock: canInsertBlockType( getDefaultBlockName(), rootClientId ), + }; +} )( BlockListAppender ); diff --git a/packages/block-editor/src/components/block-list-appender/style.native.scss b/packages/block-editor/src/components/block-list-appender/style.native.scss new file mode 100644 index 00000000000000..60734f5e9c2106 --- /dev/null +++ b/packages/block-editor/src/components/block-list-appender/style.native.scss @@ -0,0 +1,9 @@ + +.blockListAppender { + background-color: $white; + padding-left: 16; + padding-right: 16; + padding-top: 12; + padding-bottom: 0; // will be flushed into inline toolbar height + border-color: transparent; +} diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 10a5cfcbcf1bae..7c67de71c342f0 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -100,6 +100,7 @@ export class BlockList extends Component { <KeyboardAwareFlatList { ...( Platform.OS === 'android' ? { removeClippedSubviews: false } : {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541 accessibilityLabel="block-list" + autoScroll={ this.props.autoScroll } innerRef={ this.scrollViewInnerRef } extraScrollHeight={ innerToolbarHeight + 10 } keyboardShouldPersistTaps="always" diff --git a/packages/block-editor/src/components/block-list/insertion-point.native.js b/packages/block-editor/src/components/block-list/insertion-point.native.js new file mode 100644 index 00000000000000..d6caa88f407fba --- /dev/null +++ b/packages/block-editor/src/components/block-list/insertion-point.native.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +const BlockInsertionPoint = ( { showInsertionPoint } ) => { + if ( ! showInsertionPoint ) { + return null; + } + + return ( + <View style={ styles.containerStyleAddHere } > + <View style={ styles.lineStyleAddHere }></View> + <Text style={ styles.labelStyleAddHere } >{ __( 'ADD BLOCK HERE' ) }</Text> + <View style={ styles.lineStyleAddHere }></View> + </View> + ); +}; + +export default withSelect( ( select, { clientId, rootClientId } ) => { + const { + getBlockIndex, + getBlockInsertionPoint, + isBlockInsertionPointVisible, + } = select( 'core/block-editor' ); + const blockIndex = getBlockIndex( clientId, rootClientId ); + const insertionPoint = getBlockInsertionPoint(); + const showInsertionPoint = ( + isBlockInsertionPointVisible() && + insertionPoint.index === blockIndex && + insertionPoint.rootClientId === rootClientId + ); + + return { showInsertionPoint }; +} )( BlockInsertionPoint ); diff --git a/packages/block-editor/src/components/block-mover/index.native.js b/packages/block-editor/src/components/block-mover/index.native.js index 8962dc22f49e45..40bc4d550b7ab6 100644 --- a/packages/block-editor/src/components/block-mover/index.native.js +++ b/packages/block-editor/src/components/block-mover/index.native.js @@ -14,51 +14,59 @@ import { withInstanceId, compose } from '@wordpress/compose'; const BlockMover = ( { isFirst, isLast, + isLocked, onMoveDown, onMoveUp, firstIndex, -} ) => ( - <> - <ToolbarButton - title={ ! isFirst ? - sprintf( - /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ - __( 'Move block up from row %1$s to row %2$s' ), - firstIndex + 1, - firstIndex - ) : - __( 'Move block up' ) - } - isDisabled={ isFirst } - onClick={ onMoveUp } - icon="arrow-up-alt" - extraProps={ { hint: __( 'Double tap to move the block up' ) } } - /> + rootClientId, +} ) => { + if ( isLocked || ( isFirst && isLast && ! rootClientId ) ) { + return null; + } - <ToolbarButton - title={ ! isLast ? - sprintf( - /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ - __( 'Move block down from row %1$s to row %2$s' ), - firstIndex + 1, - firstIndex + 2 - ) : - __( 'Move block down' ) - } - isDisabled={ isLast } - onClick={ onMoveDown } - icon="arrow-down-alt" - extraProps={ { hint: __( 'Double tap to move the block down' ) } } - /> - </> -); + return ( + <> + <ToolbarButton + title={ ! isFirst ? + sprintf( + /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ + __( 'Move block up from row %1$s to row %2$s' ), + firstIndex + 1, + firstIndex + ) : + __( 'Move block up' ) + } + isDisabled={ isFirst } + onClick={ onMoveUp } + icon="arrow-up-alt" + extraProps={ { hint: __( 'Double tap to move the block up' ) } } + /> + + <ToolbarButton + title={ ! isLast ? + sprintf( + /* translators: accessibility text. %1: current block position (number). %2: next block position (number) */ + __( 'Move block down from row %1$s to row %2$s' ), + firstIndex + 1, + firstIndex + 2 + ) : + __( 'Move block down' ) + } + isDisabled={ isLast } + onClick={ onMoveDown } + icon="arrow-down-alt" + extraProps={ { hint: __( 'Double tap to move the block down' ) } } + /> + </> + ); +}; export default compose( withSelect( ( select, { clientIds } ) => { - const { getBlockIndex, getBlockRootClientId, getBlockOrder } = select( 'core/block-editor' ); + const { getBlockIndex, getTemplateLock, getBlockRootClientId, getBlockOrder } = select( 'core/block-editor' ); const normalizedClientIds = castArray( clientIds ); const firstClientId = first( normalizedClientIds ); - const rootClientId = getBlockRootClientId( first( normalizedClientIds ) ); + const rootClientId = getBlockRootClientId( firstClientId ); const blockOrder = getBlockOrder( rootClientId ); const firstIndex = getBlockIndex( firstClientId, rootClientId ); const lastIndex = getBlockIndex( last( normalizedClientIds ), rootClientId ); @@ -67,6 +75,8 @@ export default compose( firstIndex, isFirst: firstIndex === 0, isLast: lastIndex === blockOrder.length - 1, + isLocked: getTemplateLock( rootClientId ) === 'all', + rootClientId, }; } ), withDispatch( ( dispatch, { clientIds, rootClientId } ) => { diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 5f13de7d91b4ce..7c6906da0066a7 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -2,9 +2,12 @@ export { default as BlockControls } from './block-controls'; export { default as BlockEdit } from './block-edit'; export { default as BlockFormatControls } from './block-format-controls'; +export { default as BlockIcon } from './block-icon'; +export { default as BlockVerticalAlignmentToolbar } from './block-vertical-alignment-toolbar'; export * from './colors'; export * from './font-sizes'; export { default as AlignmentToolbar } from './alignment-toolbar'; +export { default as InnerBlocks } from './inner-blocks'; export { default as InspectorControls } from './inspector-controls'; export { default as PlainText } from './plain-text'; export { diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js new file mode 100644 index 00000000000000..d0515d6d8a58ac --- /dev/null +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import { pick, isEqual } from 'lodash'; + +/** + * WordPress dependencies + */ +import { Component } from '@wordpress/element'; +import { withSelect, withDispatch } from '@wordpress/data'; +import { synchronizeBlocksWithTemplate, withBlockContentContext } from '@wordpress/blocks'; +import isShallowEqual from '@wordpress/is-shallow-equal'; +import { compose } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import ButtonBlockAppender from './button-block-appender'; +import DefaultBlockAppender from './default-block-appender'; + +/** + * Internal dependencies + */ +import BlockList from '../block-list'; +import { withBlockEditContext } from '../block-edit/context'; + +class InnerBlocks extends Component { + constructor() { + super( ...arguments ); + this.state = { + templateInProcess: !! this.props.template, + }; + this.updateNestedSettings(); + } + + getTemplateLock() { + const { + templateLock, + parentLock, + } = this.props; + return templateLock === undefined ? parentLock : templateLock; + } + + componentDidMount() { + const { innerBlocks } = this.props.block; + // only synchronize innerBlocks with template if innerBlocks are empty or a locking all exists + if ( innerBlocks.length === 0 || this.getTemplateLock() === 'all' ) { + this.synchronizeBlocksWithTemplate(); + } + + if ( this.state.templateInProcess ) { + this.setState( { + templateInProcess: false, + } ); + } + } + + componentDidUpdate( prevProps ) { + const { template, block } = this.props; + const { innerBlocks } = block; + + this.updateNestedSettings(); + // only synchronize innerBlocks with template if innerBlocks are empty or a locking all exists + if ( innerBlocks.length === 0 || this.getTemplateLock() === 'all' ) { + const hasTemplateChanged = ! isEqual( template, prevProps.template ); + if ( hasTemplateChanged ) { + this.synchronizeBlocksWithTemplate(); + } + } + } + + /** + * Called on mount or when a mismatch exists between the templates and + * inner blocks, synchronizes inner blocks with the template, replacing + * current blocks. + */ + synchronizeBlocksWithTemplate() { + const { template, block, replaceInnerBlocks } = this.props; + const { innerBlocks } = block; + + // Synchronize with templates. If the next set differs, replace. + const nextBlocks = synchronizeBlocksWithTemplate( innerBlocks, template ); + if ( ! isEqual( nextBlocks, innerBlocks ) ) { + replaceInnerBlocks( nextBlocks ); + } + } + + updateNestedSettings() { + const { + blockListSettings, + allowedBlocks, + updateNestedSettings, + } = this.props; + + const newSettings = { + allowedBlocks, + templateLock: this.getTemplateLock(), + }; + + if ( ! isShallowEqual( blockListSettings, newSettings ) ) { + updateNestedSettings( newSettings ); + } + } + + render() { + const { + clientId, + renderAppender, + template, + __experimentalTemplateOptions: templateOptions, + } = this.props; + const { templateInProcess } = this.state; + + const isPlaceholder = template === null && !! templateOptions; + + return ( + <> + { ! templateInProcess && ( + isPlaceholder ? + null : + <BlockList + rootClientId={ clientId } + renderAppender={ renderAppender } + /> + ) } + </> + ); + } +} + +InnerBlocks = compose( [ + withBlockEditContext( ( context ) => pick( context, [ 'clientId' ] ) ), + withSelect( ( select, ownProps ) => { + const { + isBlockSelected, + hasSelectedInnerBlock, + getBlock, + getBlockListSettings, + getBlockRootClientId, + getTemplateLock, + } = select( 'core/block-editor' ); + const { clientId } = ownProps; + const block = getBlock( clientId ); + const rootClientId = getBlockRootClientId( clientId ); + + return { + block, + blockListSettings: getBlockListSettings( clientId ), + hasOverlay: block.name !== 'core/template' && ! isBlockSelected( clientId ) && ! hasSelectedInnerBlock( clientId, true ), + parentLock: getTemplateLock( rootClientId ), + }; + } ), + withDispatch( ( dispatch, ownProps ) => { + const { + replaceInnerBlocks, + updateBlockListSettings, + } = dispatch( 'core/block-editor' ); + const { block, clientId, templateInsertUpdatesSelection = true } = ownProps; + + return { + replaceInnerBlocks( blocks ) { + replaceInnerBlocks( clientId, blocks, block.innerBlocks.length === 0 && templateInsertUpdatesSelection ); + }, + updateNestedSettings( settings ) { + dispatch( updateBlockListSettings( clientId, settings ) ); + }, + }; + } ), +] )( InnerBlocks ); + +// Expose default appender placeholders as components. +InnerBlocks.DefaultBlockAppender = DefaultBlockAppender; +InnerBlocks.ButtonBlockAppender = ButtonBlockAppender; + +InnerBlocks.Content = withBlockContentContext( + ( { BlockContent } ) => <BlockContent /> +); + +/** + * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/inner-blocks/README.md + */ +export default InnerBlocks; diff --git a/packages/block-editor/src/components/url-input/test/button.js b/packages/block-editor/src/components/url-input/test/button.js index 427ea0ca8531e8..8088d61ea5016e 100644 --- a/packages/block-editor/src/components/url-input/test/button.js +++ b/packages/block-editor/src/components/url-input/test/button.js @@ -63,17 +63,17 @@ describe( 'URLInputButton', () => { } ); it( 'should close the form when user submits it', () => { const wrapper = TestUtils.renderIntoDocument( <URLInputButton /> ); - const buttonElement = () => TestUtils.findRenderedDOMComponentWithClass( + const buttonElement = () => TestUtils.scryRenderedDOMComponentsWithClass( wrapper, 'components-toolbar__control' ); - const formElement = () => TestUtils.findRenderedDOMComponentWithTag( + const formElement = () => TestUtils.scryRenderedDOMComponentsWithTag( wrapper, 'form' ); - TestUtils.Simulate.click( buttonElement() ); + TestUtils.Simulate.click( buttonElement().shift() ); expect( wrapper.state.expanded ).toBe( true ); - TestUtils.Simulate.submit( formElement() ); + TestUtils.Simulate.submit( formElement().shift() ); expect( wrapper.state.expanded ).toBe( false ); // eslint-disable-next-line react/no-find-dom-node ReactDOM.unmountComponentAtNode( ReactDOM.findDOMNode( wrapper ).parentNode ); diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index b3411411ee75fd..ebfeefe38b9dd1 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -101,6 +101,33 @@ export const coreBlocks = [ return memo; }, {} ); +/** + * Function to register an individual block. + * + * @param {Object} block The block to be registered. + * + */ +const registerBlock = ( block ) => { + if ( ! block ) { + return; + } + const { metadata, settings, name } = block; + registerBlockType( name, { + ...metadata, + ...settings, + } ); +}; + +/** + * Function to register core blocks provided by the block editor. + * + * @example + * ```js + * import { registerCoreBlocks } from '@wordpress/block-library'; + * + * registerCoreBlocks(); + * ``` + */ export const registerCoreBlocks = () => { [ paragraph, @@ -114,13 +141,10 @@ export const registerCoreBlocks = () => { separator, list, quote, - ].forEach( ( { metadata, name, settings } ) => { - registerBlockType( name, { - ...metadata, - ...settings, - } ); - } ); -}; + // eslint-disable-next-line no-undef + typeof __DEV__ !== 'undefined' && __DEV__ ? mediaText : null, + ].forEach( registerBlock ); -setDefaultBlockName( paragraph.name ); -setUnregisteredTypeHandlerName( missing.name ); + setDefaultBlockName( paragraph.name ); + setUnregisteredTypeHandlerName( missing.name ); +}; diff --git a/packages/block-library/src/media-text/edit.native.js b/packages/block-library/src/media-text/edit.native.js new file mode 100644 index 00000000000000..3cfb91b48ce949 --- /dev/null +++ b/packages/block-library/src/media-text/edit.native.js @@ -0,0 +1,186 @@ +/** + * External dependencies + */ +import { get } from 'lodash'; +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { __, _x } from '@wordpress/i18n'; +import { + BlockControls, + BlockVerticalAlignmentToolbar, + InnerBlocks, + withColors, +} from '@wordpress/block-editor'; +import { Component } from '@wordpress/element'; +import { + Toolbar, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import MediaContainer from './media-container'; +import styles from './style.scss'; + +/** + * Constants + */ +const ALLOWED_BLOCKS = [ 'core/button', 'core/paragraph', 'core/heading', 'core/list' ]; +const TEMPLATE = [ + [ 'core/paragraph', { fontSize: 'large', placeholder: _x( 'Content…', 'content placeholder' ) } ], +]; +// this limits the resize to a safe zone to avoid making broken layouts +const WIDTH_CONSTRAINT_PERCENTAGE = 15; +const applyWidthConstraints = ( width ) => Math.max( WIDTH_CONSTRAINT_PERCENTAGE, Math.min( width, 100 - WIDTH_CONSTRAINT_PERCENTAGE ) ); + +class MediaTextEdit extends Component { + constructor() { + super( ...arguments ); + + this.onSelectMedia = this.onSelectMedia.bind( this ); + this.onWidthChange = this.onWidthChange.bind( this ); + this.commitWidthChange = this.commitWidthChange.bind( this ); + this.state = { + mediaWidth: null, + }; + } + + onSelectMedia( media ) { + const { setAttributes } = this.props; + + let mediaType; + let src; + // for media selections originated from a file upload. + if ( media.media_type ) { + if ( media.media_type === 'image' ) { + mediaType = 'image'; + } else { + // only images and videos are accepted so if the media_type is not an image we can assume it is a video. + // video contain the media type of 'file' in the object returned from the rest api. + mediaType = 'video'; + } + } else { // for media selections originated from existing files in the media library. + mediaType = media.type; + } + + if ( mediaType === 'image' ) { + // Try the "large" size URL, falling back to the "full" size URL below. + src = get( media, [ 'sizes', 'large', 'url' ] ) || get( media, [ 'media_details', 'sizes', 'large', 'source_url' ] ); + } + + setAttributes( { + mediaAlt: media.alt, + mediaId: media.id, + mediaType, + mediaUrl: src || media.url, + imageFill: undefined, + focalPoint: undefined, + } ); + } + + onWidthChange( width ) { + this.setState( { + mediaWidth: applyWidthConstraints( width ), + } ); + } + + commitWidthChange( width ) { + const { setAttributes } = this.props; + + setAttributes( { + mediaWidth: applyWidthConstraints( width ), + } ); + this.setState( { + mediaWidth: null, + } ); + } + + renderMediaArea() { + const { attributes } = this.props; + const { mediaAlt, mediaId, mediaPosition, mediaType, mediaUrl, mediaWidth, imageFill, focalPoint } = attributes; + + return ( + <MediaContainer + onSelectMedia={ this.onSelectMedia } + onWidthChange={ this.onWidthChange } + commitWidthChange={ this.commitWidthChange } + onFocus={ this.props.onFocus } + { ...{ mediaAlt, mediaId, mediaType, mediaUrl, mediaPosition, mediaWidth, imageFill, focalPoint } } + /> + ); + } + + render() { + const { + attributes, + backgroundColor, + setAttributes, + } = this.props; + const { + isStackedOnMobile, + mediaPosition, + mediaWidth, + verticalAlignment, + } = attributes; + const temporaryMediaWidth = this.state.mediaWidth || mediaWidth; + const widthString = `${ temporaryMediaWidth }%`; + const containerStyles = { + ...styles[ 'wp-block-media-text' ], + ...styles[ `is-vertically-aligned-${ verticalAlignment }` ], + ...( mediaPosition === 'right' ? styles[ 'has-media-on-the-right' ] : {} ), + ...( isStackedOnMobile ? styles[ 'is-stacked-on-mobile' ] : {} ), + ...( isStackedOnMobile && mediaPosition === 'right' ? styles[ 'is-stacked-on-mobile.has-media-on-the-right' ] : {} ), + backgroundColor: backgroundColor.color, + }; + const innerBlockWidth = 100 - temporaryMediaWidth; + const innerBlockWidthString = `${ innerBlockWidth }%`; + + const toolbarControls = [ { + icon: 'align-pull-left', + title: __( 'Show media on left' ), + isActive: mediaPosition === 'left', + onClick: () => setAttributes( { mediaPosition: 'left' } ), + }, { + icon: 'align-pull-right', + title: __( 'Show media on right' ), + isActive: mediaPosition === 'right', + onClick: () => setAttributes( { mediaPosition: 'right' } ), + } ]; + + const onVerticalAlignmentChange = ( alignment ) => { + setAttributes( { verticalAlignment: alignment } ); + }; + + return ( + <> + <BlockControls> + <Toolbar + controls={ toolbarControls } + /> + <BlockVerticalAlignmentToolbar + onChange={ onVerticalAlignmentChange } + value={ verticalAlignment } + isCollapsed={ false } + /> + </BlockControls> + <View style={ containerStyles }> + <View style={ { width: widthString } }> + { this.renderMediaArea() } + </View> + <View style={ { width: innerBlockWidthString } }> + <InnerBlocks + allowedBlocks={ ALLOWED_BLOCKS } + template={ TEMPLATE } + templateInsertUpdatesSelection={ false } + /> + </View> + </View> + </> + ); + } +} + +export default withColors( 'backgroundColor' )( MediaTextEdit ); diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js new file mode 100644 index 00000000000000..ab9056cf46dc37 --- /dev/null +++ b/packages/block-library/src/media-text/media-container.native.js @@ -0,0 +1,191 @@ +/** + * External dependencies + */ +import { View, Image, ImageBackground } from 'react-native'; + +/** + * WordPress dependencies + */ +import { IconButton, Toolbar, withNotices } from '@wordpress/components'; +import { + BlockControls, + BlockIcon, + MediaPlaceholder, + MEDIA_TYPE_IMAGE, + MediaUpload, +} from '@wordpress/block-editor'; +import { Component } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import icon from './media-container-icon'; + +export function calculatePreferedImageSize( image, container ) { + const maxWidth = container.clientWidth; + const exceedMaxWidth = image.width > maxWidth; + const ratio = image.height / image.width; + const width = exceedMaxWidth ? maxWidth : image.width; + const height = exceedMaxWidth ? maxWidth * ratio : image.height; + return { width, height }; +} + +class MediaContainer extends Component { + constructor() { + super( ...arguments ); + this.onUploadError = this.onUploadError.bind( this ); + this.calculateSize = this.calculateSize.bind( this ); + this.onLayout = this.onLayout.bind( this ); + this.onSelectURL = this.onSelectURL.bind( this ); + + this.state = { + width: 0, + height: 0, + }; + + if ( this.props.mediaUrl ) { + this.onMediaChange(); + } + } + + onUploadError( message ) { + const { noticeOperations } = this.props; + noticeOperations.removeAllNotices(); + noticeOperations.createErrorNotice( message ); + } + + onSelectURL( mediaId, mediaUrl ) { + const { onSelectMedia } = this.props; + + onSelectMedia( { + media_type: 'image', + id: mediaId, + src: mediaUrl, + } ); + } + + renderToolbarEditButton() { + const { mediaId } = this.props; + return ( + <BlockControls> + <Toolbar> + <MediaUpload + onSelectURL={ this.onSelectURL } + mediaType={ MEDIA_TYPE_IMAGE } + value={ mediaId } + render={ ( { open } ) => ( + <IconButton + className="components-toolbar__control" + label={ __( 'Edit media' ) } + icon="edit" + onClick={ open } + /> + ) } + /> + </Toolbar> + </BlockControls> + ); + } + + componentDidUpdate( prevProps ) { + if ( prevProps.mediaUrl !== this.props.mediaUrl ) { + this.onMediaChange(); + } + } + + onMediaChange() { + const mediaType = this.props.mediaType; + if ( mediaType === 'video' ) { + + } else if ( mediaType === 'image' ) { + Image.getSize( this.props.mediaUrl, ( width, height ) => { + this.media = { width, height }; + this.calculateSize(); + } ); + } + } + + calculateSize() { + if ( this.media === undefined || this.container === undefined ) { + return; + } + + const { width, height } = calculatePreferedImageSize( this.media, this.container ); + this.setState( { width, height } ); + } + + onLayout( event ) { + const { width, height } = event.nativeEvent.layout; + this.container = { + clientWidth: width, + clientHeight: height, + }; + this.calculateSize(); + } + + renderImage() { + const { mediaAlt, mediaUrl } = this.props; + + return ( + <View onLayout={ this.onLayout }> + <ImageBackground + accessible={ true } + //disabled={ ! isSelected } + accessibilityLabel={ mediaAlt } + accessibilityHint={ __( 'Double tap and hold to edit' ) } + accessibilityRole={ 'imagebutton' } + style={ { width: this.state.width, height: this.state.height } } + resizeMethod="scale" + source={ { uri: mediaUrl } } + key={ mediaUrl } + > + </ImageBackground> + </View> + ); + } + + renderVideo() { + const style = { videoContainer: {} }; + return ( + <View onLayout={ this.onLayout }> + <View style={ style.videoContainer }> + { /* TODO: show video preview */ } + </View> + </View> + ); + } + + renderPlaceholder() { + return ( + <MediaPlaceholder + icon={ <BlockIcon icon={ icon } /> } + labels={ { + title: __( 'Media area' ), + } } + onSelectURL={ this.onSelectURL } + mediaType={ MEDIA_TYPE_IMAGE } + onFocus={ this.props.onFocus } + /> + ); + } + + render() { + const { mediaUrl, mediaType } = this.props; + if ( mediaType && mediaUrl ) { + let mediaElement = null; + switch ( mediaType ) { + case 'image': + mediaElement = this.renderImage(); + break; + case 'video': + mediaElement = this.renderVideo(); + break; + } + return mediaElement; + } + return this.renderPlaceholder(); + } +} + +export default withNotices( MediaContainer ); diff --git a/packages/block-library/src/media-text/style.native.scss b/packages/block-library/src/media-text/style.native.scss new file mode 100644 index 00000000000000..f1c3550f29c1e9 --- /dev/null +++ b/packages/block-library/src/media-text/style.native.scss @@ -0,0 +1,29 @@ +.wp-block-media-text { + display: flex; + align-items: flex-start; + flex-direction: row; +} + +.has-media-on-the-right { + flex-direction: row-reverse; +} + +.is-stacked-on-mobile { + flex-direction: column; + + &.has-media-on-the-right { + flex-direction: column-reverse; + } +} + +.is-vertically-aligned-top { + align-items: flex-start; +} + +.is-vertically-aligned-center { + align-items: center; +} + +.is-vertically-aligned-bottom { + align-items: flex-end; +} diff --git a/packages/blocks/src/api/index.native.js b/packages/blocks/src/api/index.native.js index 3b3be8f28c3a46..f8d4c03298aebe 100644 --- a/packages/blocks/src/api/index.native.js +++ b/packages/blocks/src/api/index.native.js @@ -35,5 +35,9 @@ export { isUnmodifiedDefaultBlock, normalizeIconObject, } from './utils'; +export { + doBlocksMatchTemplate, + synchronizeBlocksWithTemplate, +} from './templates'; export { pasteHandler, getPhrasingContentSchema } from './raw-handling'; export { default as children } from './children'; diff --git a/packages/components/src/icon-button/index.js b/packages/components/src/icon-button/index.js index f64d7f6777e934..377feab7601cfc 100644 --- a/packages/components/src/icon-button/index.js +++ b/packages/components/src/icon-button/index.js @@ -2,7 +2,7 @@ * External dependencies */ import classnames from 'classnames'; -import { isArray, isString } from 'lodash'; +import { isArray } from 'lodash'; /** * WordPress dependencies @@ -14,7 +14,7 @@ import { forwardRef } from '@wordpress/element'; */ import Tooltip from '../tooltip'; import Button from '../button'; -import Dashicon from '../dashicon'; +import Icon from '../icon'; function IconButton( props, ref ) { const { @@ -56,7 +56,7 @@ function IconButton( props, ref ) { className={ classes } ref={ ref } > - { isString( icon ) ? <Dashicon icon={ icon } ariaPressed={ ariaPressed } /> : icon } + <Icon icon={ icon } ariaPressed={ ariaPressed } /> { children } </Button> ); diff --git a/packages/components/src/icon-button/test/index.js b/packages/components/src/icon-button/test/index.js index e824ed1d556ec9..f4a8d4330d3cc2 100644 --- a/packages/components/src/icon-button/test/index.js +++ b/packages/components/src/icon-button/test/index.js @@ -24,7 +24,7 @@ describe( 'IconButton', () => { it( 'should render a Dashicon component matching the wordpress icon', () => { const iconButton = shallow( <IconButton icon="wordpress" /> ); - expect( iconButton.find( 'Dashicon' ).shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); + expect( iconButton.find( 'Icon' ).dive().shallow().hasClass( 'dashicons-wordpress' ) ).toBe( true ); } ); it( 'should render child elements when passed as children', () => { diff --git a/packages/components/src/icon/index.js b/packages/components/src/icon/index.js index 61a4fb0a2d6c47..5e762d1d5c8a98 100644 --- a/packages/components/src/icon/index.js +++ b/packages/components/src/icon/index.js @@ -6,20 +6,26 @@ import { cloneElement, createElement, Component, isValidElement } from '@wordpre /** * Internal dependencies */ -import { Dashicon, SVG } from '../'; +import Dashicon from '../dashicon'; +import { SVG } from '../primitives'; function Icon( { icon = null, size, ...additionalProps } ) { - let iconSize; + // Dashicons should be 20x20 by default. + const dashiconSize = size || 20; if ( 'string' === typeof icon ) { - // Dashicons should be 20x20 by default - iconSize = size || 20; - return <Dashicon icon={ icon } size={ iconSize } { ...additionalProps } />; + return <Dashicon icon={ icon } size={ dashiconSize } { ...additionalProps } />; } - // Any other icons should be 24x24 by default - iconSize = size || 24; + if ( icon && Dashicon === icon.type ) { + return cloneElement( icon, { + size: dashiconSize, + ...additionalProps, + } ); + } + // Icons should be 24x24 by default. + const iconSize = size || 24; if ( 'function' === typeof icon ) { if ( icon.prototype instanceof Component ) { return createElement( icon, { size: iconSize, ...additionalProps } ); diff --git a/packages/components/src/icon/test/index.js b/packages/components/src/icon/test/index.js index 053b0cf390ff58..a645568c1e2eee 100644 --- a/packages/components/src/icon/test/index.js +++ b/packages/components/src/icon/test/index.js @@ -11,6 +11,7 @@ import { Component } from '@wordpress/element'; /** * Internal dependencies */ +import Dashicon from '../../dashicon'; import Icon from '../'; import { Path, SVG } from '../../'; @@ -31,12 +32,18 @@ describe( 'Icon', () => { expect( wrapper.find( 'Dashicon' ).prop( 'icon' ) ).toBe( 'format-image' ); } ); - it( 'renders a dashicon and with a default size of 20', () => { + it( 'renders a dashicon by slug and with a default size of 20', () => { const wrapper = shallow( <Icon icon="format-image" /> ); expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); } ); + it( 'renders a dashicon by element and with a default size of 20', () => { + const wrapper = shallow( <Icon icon={ <Dashicon icon="format-image" /> } /> ); + + expect( wrapper.find( 'Dashicon' ).prop( 'size' ) ).toBe( 20 ); + } ); + it( 'renders a function', () => { const wrapper = shallow( <Icon icon={ () => <span /> } /> ); @@ -98,6 +105,7 @@ describe( 'Icon', () => { describe.each( [ [ 'dashicon', { icon: 'format-image' } ], + [ 'dashicon element', { icon: <Dashicon icon="format-image" /> } ], [ 'element', { icon: <span /> } ], [ 'svg element', { icon: svg } ], [ 'component', { icon: MyComponent } ], 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 index fb9dab39a03f2b..33c56715268971 100644 --- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js +++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js @@ -8,6 +8,7 @@ export const KeyboardAwareFlatList = ( { extraScrollHeight, shouldPreventAutomaticScroll, innerRef, + autoScroll, ...listProps } ) => ( <KeyboardAwareScrollView @@ -17,6 +18,7 @@ export const KeyboardAwareFlatList = ( { keyboardShouldPersistTaps="handled" extraScrollHeight={ extraScrollHeight } extraHeight={ 0 } + enableAutomaticScroll={ autoScroll === undefined ? false : autoScroll } innerRef={ ( ref ) => { this.scrollViewRef = ref; innerRef( ref ); diff --git a/packages/components/src/primitives/svg/index.native.js b/packages/components/src/primitives/svg/index.native.js index b0272e6b5a7b9f..4ee8dbae9b798d 100644 --- a/packages/components/src/primitives/svg/index.native.js +++ b/packages/components/src/primitives/svg/index.native.js @@ -18,7 +18,9 @@ export { export const SVG = ( props ) => { const stylesFromClasses = ( props.className || '' ).split( ' ' ).map( ( element ) => styles[ element ] ).filter( Boolean ); - const styleValues = Object.assign( {}, props.style, ...stylesFromClasses ); + const stylesFromAriaPressed = props.ariaPressed ? styles[ 'is-active' ] : styles[ 'components-toolbar__control' ]; + const styleValues = Object.assign( {}, props.style, stylesFromAriaPressed, ...stylesFromClasses ); + const safeProps = { ...props, style: styleValues }; return ( diff --git a/packages/components/src/primitives/svg/style.native.scss b/packages/components/src/primitives/svg/style.native.scss index 595372b06329e1..95dd5b9856bd7a 100644 --- a/packages/components/src/primitives/svg/style.native.scss +++ b/packages/components/src/primitives/svg/style.native.scss @@ -1,9 +1,11 @@ -.dashicon { +.dashicon, +.components-toolbar__control { color: #7b9ab1; fill: currentColor; } -.dashicon-active { +.dashicon-active, +.is-active { color: #fff; fill: currentColor; } 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 3f01ba28572d48..5096eaa7c803b5 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 @@ -74,22 +74,16 @@ exports[`MoreMenu should match snapshot 1`] = ` onMouseLeave={[Function]} type="button" > - <Dashicon + <Icon icon="ellipsis" key="0,0" > - <SVG - aria-hidden={true} - className="dashicon dashicons-ellipsis" - focusable="false" - height={20} - role="img" - viewBox="0 0 20 20" - width={20} - xmlns="http://www.w3.org/2000/svg" + <Dashicon + icon="ellipsis" + size={20} > - <svg - aria-hidden="true" + <SVG + aria-hidden={true} className="dashicon dashicons-ellipsis" focusable="false" height={20} @@ -98,16 +92,27 @@ exports[`MoreMenu should match snapshot 1`] = ` width={20} xmlns="http://www.w3.org/2000/svg" > - <Path - d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + <svg + aria-hidden="true" + className="dashicon dashicons-ellipsis" + focusable="false" + height={20} + role="img" + viewBox="0 0 20 20" + width={20} + xmlns="http://www.w3.org/2000/svg" > - <path + <Path d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" - /> - </Path> - </svg> - </SVG> - </Dashicon> + > + <path + d="M5 10c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2zm12-2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-7 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" + /> + </Path> + </svg> + </SVG> + </Dashicon> + </Icon> </button> </ForwardRef(Button)> </Tooltip> diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index 9d1925356a9138..b7c53b12aad545 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -53,6 +53,7 @@ class VisualEditor extends Component { header={ this.renderHeader() } isFullyBordered={ isFullyBordered } safeAreaBottomInset={ safeAreaBottomInset } + autoScroll={ true } /> ); } From 3db95b76b55905c0ed43a81c8ce7695cb52470e3 Mon Sep 17 00:00:00 2001 From: Drapich Piotr <drapich.piotr@gmail.com> Date: Fri, 30 Aug 2019 09:05:11 +0200 Subject: [PATCH 45/52] MediaUpload and MediaPlaceholder unify props (#17145) --- .../components/media-placeholder/README.md | 16 ++- .../media-placeholder/index.native.js | 11 ++- .../src/components/media-upload/README.md | 10 +- .../components/media-upload/index.native.js | 99 ++++++++++--------- .../media-upload/test/index.native.js | 18 ++-- .../block-library/src/image/edit.native.js | 18 ++-- .../block-library/src/video/edit.native.js | 12 +-- 7 files changed, 105 insertions(+), 79 deletions(-) diff --git a/packages/block-editor/src/components/media-placeholder/README.md b/packages/block-editor/src/components/media-placeholder/README.md index 59795b5469c8cc..bde6c7598042ba 100644 --- a/packages/block-editor/src/components/media-placeholder/README.md +++ b/packages/block-editor/src/components/media-placeholder/README.md @@ -37,6 +37,7 @@ This property is similar to the `allowedTypes` property. The difference is the f - Type: `String` - Required: No +- Platform: Web ### addToGallery @@ -46,6 +47,7 @@ If false the gallery media modal opens in the edit mode where the user can edit - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### allowedTypes @@ -57,6 +59,7 @@ This property is similar to the `accept` property. The difference is the format - Type: `Array` - Required: No +- Platform: Web | Mobile ### className @@ -64,6 +67,7 @@ Class name added to the placeholder. - Type: `String` - Required: No +- Platform: Web ### icon @@ -71,6 +75,7 @@ Icon to display left of the title. When passed as a `String`, the icon will be r - Type: `String|WPComponent` - Required: No +- Platform: Web | Mobile ### isAppender @@ -80,6 +85,7 @@ If false the default placeholder style is used. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### labels @@ -87,7 +93,7 @@ An object that can contain a `title` and `instructions` properties. These proper - Type: `Object` - Required: No - +- Platform: Web | Mobile ### multiple @@ -96,6 +102,7 @@ Whether to allow multiple selection of files or not. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### onError @@ -103,6 +110,7 @@ Callback called when an upload error happens. - Type: `Function` - Required: No +- Platform: Web ### onSelect @@ -111,6 +119,11 @@ The call back receives an array with the new files. Each element of the collecti - Type: `Function` - Required: Yes +- Platform: Web | Mobile + +The argument of the callback is an object containing the following properties: +- Web: `{ url, alt, id, link, caption, sizes, media_details }` +- Mobile: `{ id, url }` ### value @@ -118,6 +131,7 @@ Media ID (or media IDs if multiple is true) to be selected by default when openi - Type: `Number|Array` - Required: No +- Platform: Web ## Extend diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index e04361b71f3307..e224cce83c45e4 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -16,10 +16,11 @@ import { withTheme, useStyle } from '@wordpress/components'; import styles from './styles.scss'; function MediaPlaceholder( props ) { - const { mediaType, labels = {}, icon, onSelectURL, theme } = props; + const { allowedTypes = [], labels = {}, icon, onSelect, theme } = props; - const isImage = MEDIA_TYPE_IMAGE === mediaType; - const isVideo = MEDIA_TYPE_VIDEO === mediaType; + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); let placeholderTitle = labels.title; if ( placeholderTitle === undefined ) { @@ -52,8 +53,8 @@ function MediaPlaceholder( props ) { return ( <MediaUpload - mediaType={ mediaType } - onSelectURL={ onSelectURL } + allowedTypes={ allowedTypes } + onSelect={ onSelect } render={ ( { open, getMediaOptions } ) => { return ( <TouchableWithoutFeedback diff --git a/packages/block-editor/src/components/media-upload/README.md b/packages/block-editor/src/components/media-upload/README.md index 6e0a67d9f187c6..afcc4f368a6f13 100644 --- a/packages/block-editor/src/components/media-upload/README.md +++ b/packages/block-editor/src/components/media-upload/README.md @@ -63,6 +63,7 @@ If allowedTypes is unset all mime types should be allowed. - Type: `Array` - Required: No +- Platform: Web | Mobile ### multiple @@ -71,6 +72,7 @@ Whether to allow multiple selections or not. - Type: `Boolean` - Required: No - Default: false +- Platform: Web ### value @@ -78,6 +80,7 @@ Media ID (or media IDs if multiple is true) to be selected by default when openi - Type: `Number|Array` - Required: No +- Platform: Web ### onSelect @@ -85,6 +88,7 @@ Callback called when the media modal is closed, the selected media are passed as - Type: `Function` - Required: Yes +- Platform: Web | Mobile ### title @@ -93,6 +97,7 @@ Title displayed in the media modal. - Type: `String` - Required: No - Default: `Select or Upload Media` +- Platform: Web ### modalClass @@ -100,7 +105,7 @@ CSS class added to the media modal frame. - Type: `String` - Required: No - +- Platform: Web ### addToGallery @@ -111,6 +116,7 @@ Only applies if `gallery === true`. - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ### gallery @@ -119,6 +125,7 @@ If true, the component will initiate all the states required to represent a gall - Type: `Boolean` - Required: No - Default: `false` +- Platform: Web ## render @@ -126,6 +133,7 @@ A callback invoked to render the Button opening the media library. - Type: `Function` - Required: Yes +- Platform: Web | Mobile The first argument of the callback is an object containing the following properties: diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 14ed5d8dbe6236..88208d961c3d2a 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -23,16 +23,27 @@ export const MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY = 'wordpress_med export const OPTION_TAKE_VIDEO = __( 'Take a Video' ); export const OPTION_TAKE_PHOTO = __( 'Take a Photo' ); +export const OPTION_TAKE_PHOTO_OR_VIDEO = __( 'Take a Photo or Video' ); export class MediaUpload extends React.Component { + constructor( props ) { + super( props ); + this.onPickerPresent = this.onPickerPresent.bind( this ); + this.onPickerChange = this.onPickerChange.bind( this ); + this.onPickerSelect = this.onPickerSelect.bind( this ); + } getTakeMediaLabel() { - const { mediaType } = this.props; + const { allowedTypes = [] } = this.props; + + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); - if ( mediaType === MEDIA_TYPE_IMAGE ) { + if ( isImage ) { return OPTION_TAKE_PHOTO; - } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + } else if ( isVideo ) { return OPTION_TAKE_VIDEO; - } + } return OPTION_TAKE_PHOTO_OR_VIDEO; } getMediaOptionsItems() { @@ -44,11 +55,15 @@ export class MediaUpload extends React.Component { } getChooseFromDeviceIcon() { - const { mediaType } = this.props; + const { allowedTypes = [] } = this.props; + + const isOneType = allowedTypes.length === 1; + const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); + const isVideo = isOneType && allowedTypes.includes( MEDIA_TYPE_VIDEO ); - if ( mediaType === MEDIA_TYPE_IMAGE ) { + if ( isImage || ! isOneType ) { return 'format-image'; - } else if ( mediaType === MEDIA_TYPE_VIDEO ) { + } else if ( isVideo ) { return 'format-video'; } } @@ -61,58 +76,44 @@ export class MediaUpload extends React.Component { return 'wordpress-alt'; } - render() { - const { mediaType } = this.props; - - const onMediaLibraryButtonPressed = () => { - requestMediaPickFromMediaLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; - - const onMediaUploadButtonPressed = () => { - requestMediaPickFromDeviceLibrary( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; - - const onMediaCaptureButtonPressed = () => { - requestMediaPickFromDeviceCamera( [ mediaType ], ( mediaId, mediaUrl ) => { - if ( mediaId ) { - this.props.onSelectURL( mediaId, mediaUrl ); - } - } ); - }; + onPickerPresent() { + if ( this.picker ) { + this.picker.presentPicker(); + } + } - const mediaOptions = this.getMediaOptionsItems(); + onPickerSelect( requestFunction ) { + const { allowedTypes = [], onSelect } = this.props; + requestFunction( allowedTypes, ( id, url ) => { + if ( id ) { + onSelect( { id, url } ); + } + } ); + } - let picker; + onPickerChange( value ) { + if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { + this.onPickerSelect( requestMediaPickFromDeviceLibrary ); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { + this.onPickerSelect( requestMediaPickFromDeviceCamera ); + } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { + this.onPickerSelect( requestMediaPickFromMediaLibrary ); + } + } - const onPickerPresent = () => { - picker.presentPicker(); - }; + render() { + const mediaOptions = this.getMediaOptionsItems(); const getMediaOptions = () => ( <Picker hideCancelButton={ true } - ref={ ( instance ) => picker = instance } + ref={ ( instance ) => this.picker = instance } options={ mediaOptions } - onChange={ ( value ) => { - if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_CHOOSE_FROM_DEVICE ) { - onMediaUploadButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_TAKE_MEDIA ) { - onMediaCaptureButtonPressed(); - } else if ( value === MEDIA_UPLOAD_BOTTOM_SHEET_VALUE_WORD_PRESS_LIBRARY ) { - onMediaLibraryButtonPressed(); - } - } } + onChange={ this.onPickerChange } /> ); - return this.props.render( { open: onPickerPresent, getMediaOptions } ); + + return this.props.render( { open: this.onPickerPresent, getMediaOptions } ); } } 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 index 6eca7575ac408b..d1eec5a560b7b7 100644 --- a/packages/block-editor/src/components/media-upload/test/index.native.js +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -29,14 +29,14 @@ const MEDIA_ID = 123; describe( 'MediaUpload component', () => { it( 'renders without crashing', () => { const wrapper = shallow( - <MediaUpload render={ () => {} } /> + <MediaUpload allowedTypes={ [] } render={ () => {} } /> ); expect( wrapper ).toBeTruthy(); } ); it( 'opens media options picker', () => { const wrapper = shallow( - <MediaUpload render={ ( { open, getMediaOptions } ) => { + <MediaUpload allowedTypes={ [] } render={ ( { open, getMediaOptions } ) => { return ( <TouchableWithoutFeedback onPress={ open }> { getMediaOptions() } @@ -51,7 +51,7 @@ describe( 'MediaUpload component', () => { const expectOptionForMediaType = ( mediaType, expectedOption ) => { const wrapper = shallow( <MediaUpload - mediaType={ mediaType } + allowedTypes={ [ mediaType ] } render={ ( { open, getMediaOptions } ) => { return ( <TouchableWithoutFeedback onPress={ open }> @@ -72,12 +72,12 @@ describe( 'MediaUpload component', () => { callback( MEDIA_ID, MEDIA_URL ); } ); - const onSelectURL = jest.fn(); + const onSelect = jest.fn(); const wrapper = shallow( <MediaUpload - mediaType={ MEDIA_TYPE_VIDEO } - onSelectURL={ onSelectURL } + allowedTypes={ [ MEDIA_TYPE_VIDEO ] } + onSelect={ onSelect } render={ ( { open, getMediaOptions } ) => { return ( <TouchableWithoutFeedback onPress={ open }> @@ -87,10 +87,12 @@ describe( 'MediaUpload component', () => { } } /> ); wrapper.find( 'Picker' ).simulate( 'change', option ); + const media = { id: MEDIA_ID, url: MEDIA_URL }; + expect( requestFunction ).toHaveBeenCalledTimes( 1 ); - expect( onSelectURL ).toHaveBeenCalledTimes( 1 ); - expect( onSelectURL ).toHaveBeenCalledWith( MEDIA_ID, MEDIA_URL ); + expect( onSelect ).toHaveBeenCalledTimes( 1 ); + expect( onSelect ).toHaveBeenCalledWith( media ); }; it( 'can select media from device library', () => { diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index a21d4bb72830f5..2d059b04c5ca41 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -84,9 +84,9 @@ class ImageEdit extends React.Component { if ( attributes.id && attributes.url && ! isURL( attributes.url ) ) { if ( attributes.url.indexOf( 'file:' ) === 0 ) { - requestMediaImport( attributes.url, ( mediaId, mediaUri ) => { - if ( mediaUri ) { - setAttributes( { url: mediaUri, id: mediaId } ); + requestMediaImport( attributes.url, ( id, url ) => { + if ( url ) { + setAttributes( { id, url } ); } } ); } @@ -178,9 +178,9 @@ class ImageEdit extends React.Component { } ); } - onSelectMediaUploadOption( mediaId, mediaUrl ) { + onSelectMediaUploadOption( { id, url } ) { const { setAttributes } = this.props; - setAttributes( { url: mediaUrl, id: mediaId } ); + setAttributes( { id, url } ); } onFocusCaption() { @@ -265,8 +265,8 @@ class ImageEdit extends React.Component { return ( <View style={ { flex: 1 } } > <MediaPlaceholder - mediaType={ MEDIA_TYPE_IMAGE } - onSelectURL={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_IMAGE ] } + onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( false ) } onFocus={ this.props.onFocus } /> @@ -363,8 +363,8 @@ class ImageEdit extends React.Component { ); return ( - <MediaUpload mediaType={ MEDIA_TYPE_IMAGE } - onSelectURL={ this.onSelectMediaUploadOption } + <MediaUpload allowedTypes={ [ MEDIA_TYPE_IMAGE ] } + onSelect={ this.onSelectMediaUploadOption } render={ ( { open, getMediaOptions } ) => { return getImageComponent( open, getMediaOptions ); } } diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 9d004832d37f69..4b803ab9d97a95 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -136,9 +136,9 @@ class VideoEdit extends React.Component { this.setState( { isUploadInProgress: false } ); } - onSelectMediaUploadOption( mediaId, mediaUrl ) { + onSelectMediaUploadOption( { id, url } ) { const { setAttributes } = this.props; - setAttributes( { id: mediaId, src: mediaUrl } ); + setAttributes( { id, src: url } ); } onVideoContanerLayout( event ) { @@ -165,8 +165,8 @@ class VideoEdit extends React.Component { const { videoContainerHeight } = this.state; const toolbarEditButton = ( - <MediaUpload mediaType={ MEDIA_TYPE_VIDEO } - onSelectURL={ this.onSelectMediaUploadOption } + <MediaUpload allowedTypes={ [ MEDIA_TYPE_VIDEO ] } + onSelect={ this.onSelectMediaUploadOption } render={ ( { open, getMediaOptions } ) => { return ( <Toolbar> @@ -186,8 +186,8 @@ class VideoEdit extends React.Component { return ( <View style={ { flex: 1 } } > <MediaPlaceholder - mediaType={ MEDIA_TYPE_VIDEO } - onSelectURL={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_VIDEO ] } + onSelect={ this.onSelectMediaUploadOption } icon={ this.getIcon( false, true ) } onFocus={ this.props.onFocus } /> From 7aa44a28a269ce0b350c5b0513de25670c292397 Mon Sep 17 00:00:00 2001 From: Luke Walczak <lukasz.walczak.pwr@gmail.com> Date: Fri, 30 Aug 2019 11:03:46 +0200 Subject: [PATCH 46/52] Unify media placeholder and upload props within media-text (#17268) --- .../src/media-text/media-container.native.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/media-text/media-container.native.js b/packages/block-library/src/media-text/media-container.native.js index ab9056cf46dc37..0b47812e5f5ac0 100644 --- a/packages/block-library/src/media-text/media-container.native.js +++ b/packages/block-library/src/media-text/media-container.native.js @@ -37,7 +37,7 @@ class MediaContainer extends Component { this.onUploadError = this.onUploadError.bind( this ); this.calculateSize = this.calculateSize.bind( this ); this.onLayout = this.onLayout.bind( this ); - this.onSelectURL = this.onSelectURL.bind( this ); + this.onSelectMediaUploadOption = this.onSelectMediaUploadOption.bind( this ); this.state = { width: 0, @@ -55,13 +55,13 @@ class MediaContainer extends Component { noticeOperations.createErrorNotice( message ); } - onSelectURL( mediaId, mediaUrl ) { + onSelectMediaUploadOption( { id, url } ) { const { onSelectMedia } = this.props; onSelectMedia( { media_type: 'image', - id: mediaId, - src: mediaUrl, + id, + url, } ); } @@ -71,8 +71,8 @@ class MediaContainer extends Component { <BlockControls> <Toolbar> <MediaUpload - onSelectURL={ this.onSelectURL } - mediaType={ MEDIA_TYPE_IMAGE } + onSelect={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_IMAGE ] } value={ mediaId } render={ ( { open } ) => ( <IconButton @@ -163,8 +163,8 @@ class MediaContainer extends Component { labels={ { title: __( 'Media area' ), } } - onSelectURL={ this.onSelectURL } - mediaType={ MEDIA_TYPE_IMAGE } + onSelect={ this.onSelectMediaUploadOption } + allowedTypes={ [ MEDIA_TYPE_IMAGE ] } onFocus={ this.props.onFocus } /> ); From f9fa455452e9c49eb102d7d5d94b275f622bc905 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.sicart@gmail.com> Date: Fri, 30 Aug 2019 11:06:27 +0200 Subject: [PATCH 47/52] [RNMobile] Fix dismiss keyboard button for the post title (#17260) --- .../header/header-toolbar/index.native.js | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 97ca3a62349bc5..278b0af30548e1 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -30,8 +30,8 @@ function HeaderToolbar( { undo, showInserter, showKeyboardHideButton, - clearSelectedBlock, theme, + onHideKeyboard, } ) { const scrollViewRef = useRef( null ); const scrollToStart = () => { @@ -76,7 +76,7 @@ function HeaderToolbar( { <ToolbarButton title={ __( 'Hide keyboard' ) } icon="keyboard-hide" - onClick={ clearSelectedBlock } + onClick={ onHideKeyboard } extraProps={ { hint: __( 'Tap to hide the keyboard' ) } } /> </Toolbar> @@ -94,11 +94,19 @@ export default compose( [ showInserter: select( 'core/edit-post' ).getEditorMode() === 'visual' && select( 'core/editor' ).getEditorSettings().richEditingEnabled, isTextModeEnabled: select( 'core/edit-post' ).getEditorMode() === 'text', } ) ), - withDispatch( ( dispatch ) => ( { - redo: dispatch( 'core/editor' ).redo, - undo: dispatch( 'core/editor' ).undo, - clearSelectedBlock: dispatch( 'core/block-editor' ).clearSelectedBlock, - } ) ), + withDispatch( ( dispatch ) => { + const { clearSelectedBlock } = dispatch( 'core/block-editor' ); + const { togglePostTitleSelection } = dispatch( 'core/editor' ); + + return { + redo: dispatch( 'core/editor' ).redo, + undo: dispatch( 'core/editor' ).undo, + onHideKeyboard() { + clearSelectedBlock(); + togglePostTitleSelection( false ); + }, + }; + } ), withViewportMatch( { isLargeViewport: 'medium' } ), withTheme, ] )( HeaderToolbar ); From 7b12673e59097506bb11c99dc2f844b3adbe7a3e Mon Sep 17 00:00:00 2001 From: etoledom <etoledom@icloud.com> Date: Fri, 30 Aug 2019 18:09:31 +0200 Subject: [PATCH 48/52] Recover border colors (#17269) --- .../block-editor/src/components/block-list/block.native.js | 2 +- .../block-editor/src/components/block-list/index.native.js | 2 +- packages/edit-post/src/components/layout/index.native.js | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.native.js b/packages/block-editor/src/components/block-list/block.native.js index b87f95445e11f2..69503bf7151e24 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -131,7 +131,7 @@ class BlockListBlock extends Component { > { isValid && this.getBlockForType() } { ! isValid && - <BlockInvalidWarning blockTitle={ title } icon={ icon } /> + <BlockInvalidWarning blockTitle={ title } icon={ icon } /> } </View> { isSelected && <BlockMobileToolbar clientId={ clientId } /> } diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 7c67de71c342f0..5a78753c1249bc 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -140,7 +140,7 @@ export class BlockList extends Component { rootClientId={ this.props.rootClientId } onCaretVerticalPositionChange={ this.onCaretVerticalPositionChange } borderStyle={ this.blockHolderBorderStyle() } - focusedBorderColor={ blockHolderFocusedStyle } + focusedBorderColor={ blockHolderFocusedStyle.borderColor } /> ) } </ReadableContentView> ); diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 5b6813067211d2..5a10ed14cb9a56 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -118,8 +118,7 @@ class Layout extends Component { <View style={ useStyle( styles.background, styles.backgroundDark, this.props.theme ) }> { isHtmlView ? this.renderHTML() : this.renderVisual() } </View> - <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } }> - </View> + <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } } /> { ! isHtmlView && ( <KeyboardAvoidingView parentHeight={ this.state.rootViewHeight } From 14d482bbf2414a521cd2138c369c001f1b529483 Mon Sep 17 00:00:00 2001 From: Matt Chowning <matt.chowning@automattic.com> Date: Tue, 6 Aug 2019 17:04:35 -0400 Subject: [PATCH 49/52] [RNMobile] Insure tapping at end of post inserts at end Previously, tapping at the end of the post would insert a block immediately after the currently selected block. In addition, this commit is cleaning out a few unusued props in the block-list file. --- .../src/components/block-list/index.native.js | 32 +++---------------- 1 file changed, 4 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 5a78753c1249bc..a2e7fbf9511479 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -33,31 +33,12 @@ export class BlockList extends Component { this.renderDefaultBlockAppender = this.renderDefaultBlockAppender.bind( this ); this.onCaretVerticalPositionChange = this.onCaretVerticalPositionChange.bind( this ); this.scrollViewInnerRef = this.scrollViewInnerRef.bind( this ); - this.getNewBlockInsertionIndex = this.getNewBlockInsertionIndex.bind( this ); + this.addBlockToEndOfPost = this.addBlockToEndOfPost.bind( this ); this.shouldFlatListPreventAutomaticScroll = this.shouldFlatListPreventAutomaticScroll.bind( this ); } - finishBlockAppendingOrReplacing( newBlock ) { - // now determine whether we need to replace the currently selected block (if it's empty) - // or just add a new block as usual - if ( this.isReplaceable( this.props.selectedBlock ) ) { - // do replace here - this.props.replaceBlock( this.props.selectedBlockClientId, newBlock ); - } else { - this.props.insertBlock( newBlock, this.getNewBlockInsertionIndex() ); - } - } - - getNewBlockInsertionIndex() { - if ( this.props.isPostTitleSelected ) { - // if post title selected, insert at top of post - return 0; - } else if ( this.props.selectedBlockIndex === -1 ) { - // if no block selected, insert at end of post - return this.props.blockCount; - } - // insert after selected block - return this.props.selectedBlockIndex + 1; + addBlockToEndOfPost( newBlock ) { + this.props.insertBlock( newBlock, this.props.blockCount ); } blockHolderBorderStyle() { @@ -162,7 +143,7 @@ export class BlockList extends Component { const paragraphBlock = createBlock( 'core/paragraph' ); return ( <TouchableWithoutFeedback onPress={ () => { - this.finishBlockAppendingOrReplacing( paragraphBlock ); + this.addBlockToEndOfPost( paragraphBlock ); } } > <View style={ styles.blockListFooter } /> </TouchableWithoutFeedback> @@ -174,10 +155,8 @@ export default compose( [ withSelect( ( select, { rootClientId } ) => { const { getBlockCount, - getBlockName, getBlockIndex, getBlockOrder, - getSelectedBlock, getSelectedBlockClientId, getBlockInsertionPoint, isBlockInsertionPointVisible, @@ -210,13 +189,10 @@ export default compose( [ return { blockClientIds, blockCount: getBlockCount( rootClientId ), - getBlockName, isBlockInsertionPointVisible: isBlockInsertionPointVisible(), shouldShowBlockAtIndex, shouldShowInsertionPoint, - selectedBlock: getSelectedBlock(), selectedBlockClientId, - selectedBlockIndex, }; } ), withDispatch( ( dispatch ) => { From 89664ebce2a16230e68efd66609bc3fc67d22b2a Mon Sep 17 00:00:00 2001 From: Luke Walczak <lukasz.walczak.pwr@gmail.com> Date: Tue, 3 Sep 2019 12:18:11 +0200 Subject: [PATCH 50/52] Support group block on mobile (#17251) * First working version of the MediaText component for native mobile * Fix adding a block to an innerblock list * Disable mediaText on production * MediaText native: improve editor visuals * Move BlockToolbar from BlockList to Layout * Remove BlockEditorProvider from BlockList and add native version of EditorProvider to Editor. Plus support InsertionPoint and BlockListAppender * Update BlockMover for native to hide if locked or if it's the only block * Make the vertical align button work, add more styling options for toolbar buttons * Make sure registerCoreBlocks does not break in production * Copy docblock comment from the web version for registerCoreBlocks * Fix focusing on the media placeholder * Only support adding image for now * Update usage of MediaPlaceholder in MediaContainer * Enable autoScroll for just the out most block list * Fix JS Unit tests * Roll back to IconButton refactor and fix tests * Fix BlockVerticalAlignmentToolbar buttons style on mobile * Fix thing for web and ensure ariaPressed is always passed down * Use AriaPressed directly to style SVG on mobile * Update snapshots * Support group block on mobile * Extend shouldShowInsertionPoint condition to be false when group is selected * Code refactor * Update package-lock --- .../block-list-appender/index.native.js | 7 +++ .../src/components/block-list/index.native.js | 36 ++++++++----- .../button-block-appender/index.native.js | 48 +++++++++++++++++ .../button-block-appender/styles.native.scss | 17 +++++++ .../components/inner-blocks/index.native.js | 1 + .../block-library/src/group/edit.native.js | 51 +++++++++++++++++++ .../src/group/editor.native.scss | 6 +++ packages/block-library/src/index.native.js | 5 +- .../components/src/button/index.native.js | 5 +- packages/components/src/index.native.js | 1 + 10 files changed, 161 insertions(+), 16 deletions(-) create mode 100644 packages/block-editor/src/components/button-block-appender/index.native.js create mode 100644 packages/block-editor/src/components/button-block-appender/styles.native.scss create mode 100644 packages/block-library/src/group/edit.native.js create mode 100644 packages/block-library/src/group/editor.native.scss diff --git a/packages/block-editor/src/components/block-list-appender/index.native.js b/packages/block-editor/src/components/block-list-appender/index.native.js index 9a6a24c5575172..6d8fb4e6cee976 100644 --- a/packages/block-editor/src/components/block-list-appender/index.native.js +++ b/packages/block-editor/src/components/block-list-appender/index.native.js @@ -20,11 +20,18 @@ function BlockListAppender( { rootClientId, canInsertDefaultBlock, isLocked, + renderAppender: CustomAppender, } ) { if ( isLocked ) { return null; } + if ( CustomAppender ) { + return ( + <CustomAppender /> + ); + } + if ( canInsertDefaultBlock ) { return ( <DefaultBlockAppender diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index a2e7fbf9511479..2dcf6b487a18f7 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -19,7 +19,7 @@ import { KeyboardAwareFlatList, ReadableContentView, useStyle, withTheme } from */ import styles from './style.scss'; import BlockListBlock from './block'; -import DefaultBlockAppender from '../default-block-appender'; +import BlockListAppender from '../block-list-appender'; const innerToolbarHeight = 44; @@ -60,23 +60,21 @@ export class BlockList extends Component { renderDefaultBlockAppender() { return ( <ReadableContentView> - <DefaultBlockAppender + <BlockListAppender rootClientId={ this.props.rootClientId } - containerStyle={ [ - styles.blockContainerFocused, - this.blockHolderBorderStyle(), - { borderColor: 'transparent' }, - ] } + renderAppender={ this.props.renderAppender } /> </ReadableContentView> ); } render() { + const { clearSelectedBlock, blockClientIds, isFullyBordered, title, header, withFooter = true, renderAppender } = this.props; + return ( <View style={ { flex: 1 } } - onAccessibilityEscape={ this.props.clearSelectedBlock } + onAccessibilityEscape={ clearSelectedBlock } > <KeyboardAwareFlatList { ...( Platform.OS === 'android' ? { removeClippedSubviews: false } : {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541 @@ -86,16 +84,23 @@ export class BlockList extends Component { extraScrollHeight={ innerToolbarHeight + 10 } keyboardShouldPersistTaps="always" style={ useStyle( styles.list, styles.listDark, this.context ) } - data={ this.props.blockClientIds } - extraData={ [ this.props.isFullyBordered ] } + data={ blockClientIds } + extraData={ [ isFullyBordered ] } keyExtractor={ identity } renderItem={ this.renderItem } shouldPreventAutomaticScroll={ this.shouldFlatListPreventAutomaticScroll } - title={ this.props.title } - ListHeaderComponent={ this.props.header } + title={ title } + ListHeaderComponent={ header } ListEmptyComponent={ this.renderDefaultBlockAppender } - ListFooterComponent={ this.renderBlockListFooter } + ListFooterComponent={ withFooter && this.renderBlockListFooter } /> + + { renderAppender && blockClientIds.length > 0 && + <BlockListAppender + rootClientId={ this.props.rootClientId } + renderAppender={ this.props.renderAppender } + /> + } </View> ); } @@ -160,12 +165,15 @@ export default compose( [ getSelectedBlockClientId, getBlockInsertionPoint, isBlockInsertionPointVisible, + getSelectedBlock, } = select( 'core/block-editor' ); const selectedBlockClientId = getSelectedBlockClientId(); const blockClientIds = getBlockOrder( rootClientId ); const insertionPoint = getBlockInsertionPoint(); const blockInsertionPointIsVisible = isBlockInsertionPointVisible(); + const selectedBlock = getSelectedBlock(); + const isSelectedGroup = selectedBlock && selectedBlock.name === 'core/group'; const shouldShowInsertionPoint = ( clientId ) => { return ( blockInsertionPointIsVisible && @@ -177,7 +185,7 @@ export default compose( [ const selectedBlockIndex = getBlockIndex( selectedBlockClientId ); const shouldShowBlockAtIndex = ( index ) => { const shouldHideBlockAtIndex = ( - blockInsertionPointIsVisible && + ! isSelectedGroup && blockInsertionPointIsVisible && // if `index` === `insertionPoint.index`, then block is replaceable index === insertionPoint.index && // only hide selected block diff --git a/packages/block-editor/src/components/button-block-appender/index.native.js b/packages/block-editor/src/components/button-block-appender/index.native.js new file mode 100644 index 00000000000000..b01d3b65109856 --- /dev/null +++ b/packages/block-editor/src/components/button-block-appender/index.native.js @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Button, Dashicon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import Inserter from '../inserter'; +import styles from './styles.scss'; + +function ButtonBlockAppender( { rootClientId } ) { + return ( + <> + <Inserter + rootClientId={ rootClientId } + renderToggle={ ( { onToggle, disabled, isOpen } ) => ( + <Button + onClick={ onToggle } + aria-expanded={ isOpen } + disabled={ disabled } + fixedRatio={ false } + > + <View style={ [ styles.appender, { flex: 1 } ] }> + <Dashicon + icon="plus-alt" + style={ styles.addBlockButton } + color={ styles.addBlockButton.color } + size={ styles.addBlockButton.size } + /> + </View> + </Button> + ) } + isAppender + /> + </> + ); +} + +/** + * @see https://github.com/WordPress/gutenberg/blob/master/packages/block-editor/src/components/button-block-appender/README.md + */ +export default ButtonBlockAppender; diff --git a/packages/block-editor/src/components/button-block-appender/styles.native.scss b/packages/block-editor/src/components/button-block-appender/styles.native.scss new file mode 100644 index 00000000000000..0fd4cf46274e02 --- /dev/null +++ b/packages/block-editor/src/components/button-block-appender/styles.native.scss @@ -0,0 +1,17 @@ +.appender { + align-items: center; + justify-content: center; + background-color: $gray-light; + padding: 12px; + background-color: $white; + border: $border-width solid $light-gray-500; + border-radius: 4px; +} + +.addBlockButton { + color: $white; + background-color: $gray; + border-radius: $icon-button-size-small / 2; + overflow: hidden; + size: $icon-button-size-small; +} diff --git a/packages/block-editor/src/components/inner-blocks/index.native.js b/packages/block-editor/src/components/inner-blocks/index.native.js index d0515d6d8a58ac..2b6a6c9320c923 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -121,6 +121,7 @@ class InnerBlocks extends Component { <BlockList rootClientId={ clientId } renderAppender={ renderAppender } + withFooter={ false } /> ) } </> diff --git a/packages/block-library/src/group/edit.native.js b/packages/block-library/src/group/edit.native.js new file mode 100644 index 00000000000000..32c87e3863c881 --- /dev/null +++ b/packages/block-library/src/group/edit.native.js @@ -0,0 +1,51 @@ + +/** + * External dependencies + */ +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { withSelect } from '@wordpress/data'; +import { compose } from '@wordpress/compose'; +import { + InnerBlocks, + withColors, +} from '@wordpress/block-editor'; +/** + * Internal dependencies + */ +import styles from './editor.scss'; + +function GroupEdit( { + hasInnerBlocks, + isSelected, +} ) { + if ( ! isSelected && ! hasInnerBlocks ) { + return ( + <View style={ styles.groupPlaceholder } /> + ); + } + + return ( + <InnerBlocks + renderAppender={ isSelected && InnerBlocks.ButtonBlockAppender } + /> + ); +} + +export default compose( [ + withColors( 'backgroundColor' ), + withSelect( ( select, { clientId } ) => { + const { + getBlock, + } = select( 'core/block-editor' ); + + const block = getBlock( clientId ); + + return { + hasInnerBlocks: !! ( block && block.innerBlocks.length ), + }; + } ), +] )( GroupEdit ); diff --git a/packages/block-library/src/group/editor.native.scss b/packages/block-library/src/group/editor.native.scss new file mode 100644 index 00000000000000..5edfa582287ef5 --- /dev/null +++ b/packages/block-library/src/group/editor.native.scss @@ -0,0 +1,6 @@ +.groupPlaceholder { + padding: 12px; + background-color: $white; + border: $border-width dashed $light-gray-500; + border-radius: 4px; +} diff --git a/packages/block-library/src/index.native.js b/packages/block-library/src/index.native.js index ebfeefe38b9dd1..89635d7218b847 100644 --- a/packages/block-library/src/index.native.js +++ b/packages/block-library/src/index.native.js @@ -49,6 +49,7 @@ import * as textColumns from './text-columns'; import * as verse from './verse'; import * as video from './video'; import * as tagCloud from './tag-cloud'; +import * as group from './group'; export const coreBlocks = [ // Common blocks are grouped at the top to prioritize their display @@ -142,7 +143,9 @@ export const registerCoreBlocks = () => { list, quote, // eslint-disable-next-line no-undef - typeof __DEV__ !== 'undefined' && __DEV__ ? mediaText : null, + !! __DEV__ ? mediaText : null, + // eslint-disable-next-line no-undef + !! __DEV__ ? group : null, ].forEach( registerBlock ); setDefaultBlockName( paragraph.name ); diff --git a/packages/components/src/button/index.native.js b/packages/components/src/button/index.native.js index 78d10226e26def..f7fdd94d13f85d 100644 --- a/packages/components/src/button/index.native.js +++ b/packages/components/src/button/index.native.js @@ -19,6 +19,8 @@ const styles = StyleSheet.create( { flexDirection: 'row', justifyContent: 'center', alignItems: 'center', + }, + fixedRatio: { aspectRatio: 1, }, buttonActive: { @@ -28,7 +30,6 @@ const styles = StyleSheet.create( { alignItems: 'center', borderRadius: 6, borderColor: '#2e4453', - aspectRatio: 1, backgroundColor: '#2e4453', }, subscriptInactive: { @@ -55,6 +56,7 @@ export default function Button( props ) { onClick, disabled, hint, + fixedRatio = true, 'aria-disabled': ariaDisabled, 'aria-label': ariaLabel, 'aria-pressed': ariaPressed, @@ -65,6 +67,7 @@ export default function Button( props ) { const buttonViewStyle = { opacity: isDisabled ? 0.2 : 1, + ...( fixedRatio && styles.fixedRatio ), ...( ariaPressed ? styles.buttonActive : styles.buttonInactive ), }; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index d09b157160003c..209507ec96b440 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -10,6 +10,7 @@ export { default as Spinner } from './spinner'; export { createSlotFill, Slot, Fill, Provider as SlotFillProvider } from './slot-fill'; export { default as BaseControl } from './base-control'; export { default as TextareaControl } from './textarea-control'; +export { default as Button } from './button'; // Higher-Order Components export { default as withConstrainedTabbing } from './higher-order/with-constrained-tabbing'; From fc8c3dabc14455c025594f0692fe1df3038c0987 Mon Sep 17 00:00:00 2001 From: Luke Walczak <lukasz.walczak.pwr@gmail.com> Date: Wed, 4 Sep 2019 14:02:20 +0200 Subject: [PATCH 51/52] Remove redundant bg color within button appender (#17325) --- .../src/components/button-block-appender/styles.native.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/block-editor/src/components/button-block-appender/styles.native.scss b/packages/block-editor/src/components/button-block-appender/styles.native.scss index 0fd4cf46274e02..3a6549980af87d 100644 --- a/packages/block-editor/src/components/button-block-appender/styles.native.scss +++ b/packages/block-editor/src/components/button-block-appender/styles.native.scss @@ -1,7 +1,6 @@ .appender { align-items: center; justify-content: center; - background-color: $gray-light; padding: 12px; background-color: $white; border: $border-width solid $light-gray-500; From 264b178ef9bd94d4e0539c1271fb92b5b4490efa Mon Sep 17 00:00:00 2001 From: etoledom <etoledom@icloud.com> Date: Wed, 4 Sep 2019 14:03:38 +0200 Subject: [PATCH 52/52] [RNMobile] DarkMode improvements (#17309) * Remove the need to import `useStyle` and pass the theme prop on every instance that `withStyle` is used * Implement dark-mode refactor on all components * Fix broken native tests * Fix default block appender background color on DarkMode * DarkMode: Make `useStyle` a class function --- .../block-list-appender/style.native.scss | 1 - .../src/components/block-list/index.native.js | 10 +++---- .../components/block-list/style.native.scss | 4 --- .../src/components/inserter/index.native.js | 6 ++-- .../src/components/inserter/menu.native.js | 9 +++--- .../media-placeholder/index.native.js | 8 ++--- .../src/components/warning/index.native.js | 10 +++---- .../block-library/src/code/edit.native.js | 8 ++--- .../block-library/src/image/edit.native.js | 4 +-- .../block-library/src/missing/edit.native.js | 10 +++---- .../block-library/src/more/edit.native.js | 10 ++++--- .../block-library/src/nextpage/edit.native.js | 8 ++--- .../block-library/src/video/edit.native.js | 4 +-- .../src/mobile/bottom-sheet/cell.native.js | 17 +++++------ .../src/mobile/bottom-sheet/index.native.js | 6 ++-- .../src/mobile/dark-mode/index.native.js | 29 +++++++++++-------- .../mobile/html-text-input/index.native.js | 7 +++-- .../html-text-input/test/index.native.js | 9 +++++- .../src/toolbar/toolbar-container.native.js | 8 ++--- .../header/header-toolbar/index.native.js | 6 ++-- .../src/components/layout/index.native.js | 7 +++-- .../components/visual-editor/index.native.js | 6 ++-- .../rich-text/src/component/index.native.js | 10 +++---- .../src/component/test/index.native.js | 5 ++++ 24 files changed, 107 insertions(+), 95 deletions(-) diff --git a/packages/block-editor/src/components/block-list-appender/style.native.scss b/packages/block-editor/src/components/block-list-appender/style.native.scss index 60734f5e9c2106..edb2c6f809ebcc 100644 --- a/packages/block-editor/src/components/block-list-appender/style.native.scss +++ b/packages/block-editor/src/components/block-list-appender/style.native.scss @@ -1,6 +1,5 @@ .blockListAppender { - background-color: $white; padding-left: 16; padding-right: 16; padding-top: 12; diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js index 2dcf6b487a18f7..54b6b62f08c96b 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { createBlock, isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { KeyboardAwareFlatList, ReadableContentView, useStyle, withTheme } from '@wordpress/components'; +import { KeyboardAwareFlatList, ReadableContentView, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -83,7 +83,7 @@ export class BlockList extends Component { innerRef={ this.scrollViewInnerRef } extraScrollHeight={ innerToolbarHeight + 10 } keyboardShouldPersistTaps="always" - style={ useStyle( styles.list, styles.listDark, this.context ) } + style={ styles.list } data={ blockClientIds } extraData={ [ isFullyBordered ] } keyExtractor={ identity } @@ -113,7 +113,7 @@ export class BlockList extends Component { } renderItem( { item: clientId, index } ) { - const blockHolderFocusedStyle = useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark, this.props.theme ); + const blockHolderFocusedStyle = this.props.useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark ); const { shouldShowBlockAtIndex, shouldShowInsertionPoint } = this.props; return ( <ReadableContentView> @@ -133,8 +133,8 @@ export class BlockList extends Component { } renderAddBlockSeparator() { - const lineStyle = useStyle( styles.lineStyleAddHere, styles.lineStyleAddHereDark, this.props.theme ); - const labelStyle = useStyle( styles.labelStyleAddHere, styles.labelStyleAddHereDark, this.props.theme ); + const lineStyle = this.props.useStyle( styles.lineStyleAddHere, styles.lineStyleAddHereDark ); + const labelStyle = this.props.useStyle( styles.labelStyleAddHere, styles.labelStyleAddHereDark ); return ( <View style={ styles.containerStyleAddHere } > <View style={ lineStyle }></View> diff --git a/packages/block-editor/src/components/block-list/style.native.scss b/packages/block-editor/src/components/block-list/style.native.scss index e1f8e96abc948f..36be89c4c067cd 100644 --- a/packages/block-editor/src/components/block-list/style.native.scss +++ b/packages/block-editor/src/components/block-list/style.native.scss @@ -8,10 +8,6 @@ flex: 1; } -.listDark { - background: #1c1c1e; -} - .switch { flex-direction: row; justify-content: flex-start; diff --git a/packages/block-editor/src/components/inserter/index.native.js b/packages/block-editor/src/components/inserter/index.native.js index 1a757a65a502b2..b64c460f14bd67 100644 --- a/packages/block-editor/src/components/inserter/index.native.js +++ b/packages/block-editor/src/components/inserter/index.native.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { Dropdown, ToolbarButton, Dashicon, withTheme, useStyle } from '@wordpress/components'; +import { Dropdown, ToolbarButton, Dashicon, withTheme } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -56,9 +56,9 @@ class Inserter extends Component { const { disabled, renderToggle = defaultRenderToggle, - theme, + useStyle, } = this.props; - const style = useStyle( styles.addBlockButton, styles.addBlockButtonDark, theme ); + const style = useStyle( styles.addBlockButton, styles.addBlockButtonDark ); return renderToggle( { onToggle, isOpen, disabled, style } ); } diff --git a/packages/block-editor/src/components/inserter/menu.native.js b/packages/block-editor/src/components/inserter/menu.native.js index 3b0e27a156ddb8..9dbf9b5b162713 100644 --- a/packages/block-editor/src/components/inserter/menu.native.js +++ b/packages/block-editor/src/components/inserter/menu.native.js @@ -14,7 +14,7 @@ import { } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { withInstanceId, compose } from '@wordpress/compose'; -import { BottomSheet, Icon, withTheme, useStyle } from '@wordpress/components'; +import { BottomSheet, Icon, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -61,11 +61,12 @@ export class InserterMenu extends Component { } render() { + const { useStyle } = this.props; const numberOfColumns = this.calculateNumberOfColumns(); const bottomPadding = styles.contentBottomPadding; - const modalIconWrapperStyle = useStyle( styles.modalIconWrapper, styles.modalIconWrapperDark, this.props.theme ); - const modalIconStyle = useStyle( styles.modalIcon, styles.modalIconDark, this.props.theme ); - const modalItemLabelStyle = useStyle( styles.modalItemLabel, styles.modalItemLabelDark, this.props.theme ); + const modalIconWrapperStyle = useStyle( styles.modalIconWrapper, styles.modalIconWrapperDark ); + const modalIconStyle = useStyle( styles.modalIcon, styles.modalIconDark ); + const modalItemLabelStyle = useStyle( styles.modalItemLabel, styles.modalItemLabelDark ); return ( <BottomSheet diff --git a/packages/block-editor/src/components/media-placeholder/index.native.js b/packages/block-editor/src/components/media-placeholder/index.native.js index e224cce83c45e4..d2b1bd0d3bf23f 100644 --- a/packages/block-editor/src/components/media-placeholder/index.native.js +++ b/packages/block-editor/src/components/media-placeholder/index.native.js @@ -8,7 +8,7 @@ import { View, Text, TouchableWithoutFeedback } from 'react-native'; */ import { __, sprintf } from '@wordpress/i18n'; import { MediaUpload, MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO } from '@wordpress/block-editor'; -import { withTheme, useStyle } from '@wordpress/components'; +import { withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -16,7 +16,7 @@ import { withTheme, useStyle } from '@wordpress/components'; import styles from './styles.scss'; function MediaPlaceholder( props ) { - const { allowedTypes = [], labels = {}, icon, onSelect, theme } = props; + const { allowedTypes = [], labels = {}, icon, onSelect, useStyle } = props; const isOneType = allowedTypes.length === 1; const isImage = isOneType && allowedTypes.includes( MEDIA_TYPE_IMAGE ); @@ -48,8 +48,8 @@ function MediaPlaceholder( props ) { accessibilityHint = __( 'Double tap to select a video' ); } - const emptyStateContainerStyle = useStyle( styles.emptyStateContainer, styles.emptyStateContainerDark, theme ); - const emptyStateTitleStyle = useStyle( styles.emptyStateTitle, styles.emptyStateTitleDark, theme ); + const emptyStateContainerStyle = useStyle( styles.emptyStateContainer, styles.emptyStateContainerDark ); + const emptyStateTitleStyle = useStyle( styles.emptyStateTitle, styles.emptyStateTitleDark ); return ( <MediaUpload diff --git a/packages/block-editor/src/components/warning/index.native.js b/packages/block-editor/src/components/warning/index.native.js index 071fd30a0738a3..e6c13b5dd75a99 100644 --- a/packages/block-editor/src/components/warning/index.native.js +++ b/packages/block-editor/src/components/warning/index.native.js @@ -6,7 +6,7 @@ import { View, Text } from 'react-native'; /** * WordPress dependencies */ -import { Icon, withTheme, useStyle } from '@wordpress/components'; +import { Icon, withTheme } from '@wordpress/components'; import { normalizeIconObject } from '@wordpress/blocks'; /** @@ -14,15 +14,15 @@ import { normalizeIconObject } from '@wordpress/blocks'; */ import styles from './style.scss'; -function Warning( { title, message, icon, iconClass, theme, ...viewProps } ) { +function Warning( { title, message, icon, iconClass, theme, useStyle, ...viewProps } ) { icon = icon && normalizeIconObject( icon ); const internalIconClass = 'warning-icon' + '-' + theme; - const titleStyle = useStyle( styles.title, styles.titleDark, theme ); - const messageStyle = useStyle( styles.message, styles.messageDark, theme ); + const titleStyle = useStyle( styles.title, styles.titleDark ); + const messageStyle = useStyle( styles.message, styles.messageDark ); return ( <View - style={ useStyle( styles.container, styles.containerDark, theme ) } + style={ useStyle( styles.container, styles.containerDark ) } { ...viewProps } > { icon && ( diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 0363fcfebc61d0..a8afae8f0195b4 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -8,7 +8,7 @@ import { View } from 'react-native'; */ import { PlainText } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; -import { withTheme, useStyle } from '@wordpress/components'; +import { withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -23,9 +23,9 @@ import styles from './theme.scss'; // Note: styling is applied directly to the (nested) PlainText component. Web-side components // apply it to the container 'div' but we don't have a proper proposal for cascading styling yet. export function CodeEdit( props ) { - const { attributes, setAttributes, style, onFocus, onBlur, theme } = props; - const codeStyle = useStyle( styles.blockCode, styles.blockCodeDark, theme ); - const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark, theme ); + const { attributes, setAttributes, style, onFocus, onBlur, useStyle } = props; + const codeStyle = useStyle( styles.blockCode, styles.blockCodeDark ); + const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark ); return ( <View> diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 2d059b04c5ca41..eb846520ab53f6 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -20,7 +20,6 @@ import { Toolbar, ToolbarButton, withTheme, - useStyle, } from '@wordpress/components'; import { @@ -195,12 +194,11 @@ class ImageEdit extends React.Component { } getIcon( isRetryIcon ) { - const iconStyle = useStyle( styles.icon, styles.iconDark, this.props.theme ); - if ( isRetryIcon ) { return <Icon icon={ SvgIconRetry } { ...styles.iconRetry } />; } + const iconStyle = this.props.useStyle( styles.icon, styles.iconDark ); return <Icon icon={ SvgIcon } { ...iconStyle } />; } diff --git a/packages/block-library/src/missing/edit.native.js b/packages/block-library/src/missing/edit.native.js index c5a604144b7e7b..356aa68f0fb330 100644 --- a/packages/block-library/src/missing/edit.native.js +++ b/packages/block-library/src/missing/edit.native.js @@ -6,7 +6,7 @@ import { View, Text } from 'react-native'; /** * WordPress dependencies */ -import { Icon, withTheme, useStyle } from '@wordpress/components'; +import { Icon, withTheme } from '@wordpress/components'; import { coreBlocks } from '@wordpress/block-library'; import { normalizeIconObject } from '@wordpress/blocks'; import { Component } from '@wordpress/element'; @@ -20,17 +20,17 @@ import styles from './style.scss'; export class UnsupportedBlockEdit extends Component { render() { const { originalName } = this.props.attributes; - const theme = this.props.theme; + const { useStyle, theme } = this.props; const blockType = coreBlocks[ originalName ]; const title = blockType ? blockType.settings.title : __( 'Unsupported' ); - const titleStyle = useStyle( styles.unsupportedBlockMessage, styles.unsupportedBlockMessageDark, theme ); + const titleStyle = useStyle( styles.unsupportedBlockMessage, styles.unsupportedBlockMessageDark ); const icon = blockType ? normalizeIconObject( blockType.settings.icon ) : 'admin-plugins'; - const iconStyle = useStyle( styles.unsupportedBlockIcon, styles.unsupportedBlockIconDark, theme ); + const iconStyle = useStyle( styles.unsupportedBlockIcon, styles.unsupportedBlockIconDark ); const iconClassName = 'unsupported-icon' + '-' + theme; return ( - <View style={ useStyle( styles.unsupportedBlock, styles.unsupportedBlockDark, theme ) }> + <View style={ useStyle( styles.unsupportedBlock, styles.unsupportedBlockDark ) }> <Icon className={ iconClassName } icon={ icon && icon.src ? icon.src : icon } color={ iconStyle.color } /> <Text style={ titleStyle }>{ title }</Text> </View> diff --git a/packages/block-library/src/more/edit.native.js b/packages/block-library/src/more/edit.native.js index e177213a7f1f6f..c266512b287831 100644 --- a/packages/block-library/src/more/edit.native.js +++ b/packages/block-library/src/more/edit.native.js @@ -9,7 +9,7 @@ import Hr from 'react-native-hr'; */ import { __ } from '@wordpress/i18n'; import { Component } from '@wordpress/element'; -import { withTheme, useStyle } from '@wordpress/components'; +import { withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -26,11 +26,13 @@ export class MoreEdit extends Component { } render() { - const { customText } = this.props.attributes; + const { attributes, useStyle } = this.props; + const { customText } = attributes; const { defaultText } = this.state; + const content = customText || defaultText; - const textStyle = useStyle( styles.moreText, styles.moreTextDark, this.props.theme ); - const lineStyle = useStyle( styles.moreLine, styles.moreLineDark, this.props.theme ); + const textStyle = useStyle( styles.moreText, styles.moreTextDark ); + const lineStyle = useStyle( styles.moreLine, styles.moreLineDark ); return ( <View> diff --git a/packages/block-library/src/nextpage/edit.native.js b/packages/block-library/src/nextpage/edit.native.js index be9ad283576401..37f9c235b7ef6f 100644 --- a/packages/block-library/src/nextpage/edit.native.js +++ b/packages/block-library/src/nextpage/edit.native.js @@ -8,19 +8,19 @@ import Hr from 'react-native-hr'; * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; -import { withTheme, useStyle } from '@wordpress/components'; +import { withTheme } from '@wordpress/components'; /** * Internal dependencies */ import styles from './editor.scss'; -export function NextPageEdit( { attributes, isSelected, onFocus, theme } ) { +export function NextPageEdit( { attributes, isSelected, onFocus, useStyle } ) { const { customText = __( 'Page break' ) } = attributes; const accessibilityTitle = attributes.customText || ''; const accessibilityState = isSelected ? [ 'selected' ] : []; - const textStyle = useStyle( styles.nextpageText, styles.nextpageTextDark, theme ); - const lineStyle = useStyle( styles.nextpageLine, styles.nextpageLineDark, theme ); + const textStyle = useStyle( styles.nextpageText, styles.nextpageTextDark ); + const lineStyle = useStyle( styles.nextpageLine, styles.nextpageLineDark ); return ( <View diff --git a/packages/block-library/src/video/edit.native.js b/packages/block-library/src/video/edit.native.js index 4b803ab9d97a95..39fc0e0d2233df 100644 --- a/packages/block-library/src/video/edit.native.js +++ b/packages/block-library/src/video/edit.native.js @@ -21,7 +21,6 @@ import { Toolbar, ToolbarButton, withTheme, - useStyle, } from '@wordpress/components'; import { @@ -150,12 +149,11 @@ class VideoEdit extends React.Component { } getIcon( isRetryIcon, isMediaPlaceholder ) { - const iconStyle = useStyle( style.icon, style.iconDark, this.props.theme ); - if ( isRetryIcon ) { return <Icon icon={ SvgIconRetry } { ...style.icon } />; } + const iconStyle = this.props.useStyle( style.icon, style.iconDark ); return <Icon icon={ SvgIcon } { ...( ! isMediaPlaceholder ? style.iconUploading : iconStyle ) } />; } diff --git a/packages/components/src/mobile/bottom-sheet/cell.native.js b/packages/components/src/mobile/bottom-sheet/cell.native.js index cbc10928478704..6a5fc6ba38947f 100644 --- a/packages/components/src/mobile/bottom-sheet/cell.native.js +++ b/packages/components/src/mobile/bottom-sheet/cell.native.js @@ -16,8 +16,7 @@ import { __, _x, sprintf } from '@wordpress/i18n'; */ import styles from './styles.scss'; import platformStyles from './cellStyles.scss'; -// `useStyle as getStyle`: Hack to avoid lint thinking this is a React Hook -import { withTheme, useStyle as getStyle } from '../dark-mode'; +import { withTheme } from '../dark-mode'; class BottomSheetCell extends Component { constructor( props ) { @@ -50,14 +49,14 @@ class BottomSheetCell extends Component { editable = true, separatorType, style = {}, - theme, + useStyle, ...valueProps } = this.props; const showValue = value !== undefined; const isValueEditable = editable && onChangeValue !== undefined; - const cellLabelStyle = getStyle( styles.cellLabel, styles.cellTextDark, theme ); - const cellLabelCenteredStyle = getStyle( styles.cellLabelCentered, styles.cellTextDark, theme ); + const cellLabelStyle = useStyle( styles.cellLabel, styles.cellTextDark ); + const cellLabelCenteredStyle = useStyle( styles.cellLabelCentered, styles.cellTextDark ); const defaultLabelStyle = showValue || icon !== undefined ? cellLabelStyle : cellLabelCenteredStyle; const drawSeparator = ( separatorType && separatorType !== 'none' ) || separatorStyle === undefined; @@ -81,8 +80,8 @@ class BottomSheetCell extends Component { const separatorStyle = () => { //eslint-disable-next-line @wordpress/no-unused-vars-before-return - const defaultSeparatorStyle = getStyle( styles.separator, styles.separatorDark, theme ); - const cellSeparatorStyle = getStyle( styles.cellSeparator, styles.cellSeparatorDark, theme ); + const defaultSeparatorStyle = this.props.useStyle( styles.separator, styles.separatorDark ); + const cellSeparatorStyle = this.props.useStyle( styles.cellSeparator, styles.cellSeparatorDark ); const leftMarginStyle = { ...cellSeparatorStyle, ...platformStyles.separatorMarginLeft }; switch ( separatorType ) { case 'leftMargin': @@ -98,7 +97,7 @@ class BottomSheetCell extends Component { const getValueComponent = () => { const styleRTL = I18nManager.isRTL && styles.cellValueRTL; - const cellValueStyle = getStyle( styles.cellValue, styles.cellTextDark, theme ); + const cellValueStyle = this.props.useStyle( styles.cellValue, styles.cellTextDark ); const finalStyle = { ...cellValueStyle, ...valueStyle, ...styleRTL }; // To be able to show the `middle` ellipsizeMode on editable cells @@ -151,7 +150,7 @@ class BottomSheetCell extends Component { ); }; - const iconStyle = getStyle( styles.icon, styles.iconDark, theme ); + const iconStyle = useStyle( styles.icon, styles.iconDark ); return ( <TouchableOpacity diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index e3209713e49449..805ddb084c0587 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -19,7 +19,7 @@ import Cell from './cell'; import PickerCell from './picker-cell'; import SwitchCell from './switch-cell'; import KeyboardAvoidingView from './keyboard-avoiding-view'; -import { withTheme, useStyle } from '../dark-mode'; +import { withTheme } from '../dark-mode'; class BottomSheet extends Component { constructor() { @@ -64,7 +64,7 @@ class BottomSheet extends Component { hideHeader, style = {}, contentStyle = {}, - theme, + useStyle, } = this.props; const panResponder = PanResponder.create( { @@ -120,7 +120,7 @@ class BottomSheet extends Component { }, }; - const backgroundStyle = useStyle( styles.background, styles.backgroundDark, theme ); + const backgroundStyle = useStyle( styles.background, styles.backgroundDark ); return ( <Modal diff --git a/packages/components/src/mobile/dark-mode/index.native.js b/packages/components/src/mobile/dark-mode/index.native.js index d2b13020f027f9..44ff4c38a8f644 100644 --- a/packages/components/src/mobile/dark-mode/index.native.js +++ b/packages/components/src/mobile/dark-mode/index.native.js @@ -4,27 +4,18 @@ import { eventEmitter, initialMode } from 'react-native-dark-mode'; import React from 'react'; -// This was failing on CI +// Conditional needed to pass UI Tests on CI if ( eventEmitter.setMaxListeners ) { eventEmitter.setMaxListeners( 150 ); } -export function useStyle( light, dark, theme ) { - const finalDark = { - ...light, - ...dark, - }; - - return theme === 'dark' ? finalDark : light; -} - -// This function takes a component... export function withTheme( WrappedComponent ) { return class extends React.Component { constructor( props ) { super( props ); this.onModeChanged = this.onModeChanged.bind( this ); + this.useStyle = this.useStyle.bind( this ); this.state = { mode: initialMode, @@ -46,8 +37,22 @@ export function withTheme( WrappedComponent ) { } } + useStyle( light, dark ) { + const isDarkMode = this.state.mode === 'dark'; + const finalDark = { + ...light, + ...dark, + }; + + return isDarkMode ? finalDark : light; + } + render() { - return <WrappedComponent theme={ this.state.mode } { ...this.props } />; + return <WrappedComponent + theme={ this.state.mode } + useStyle={ this.useStyle } + { ...this.props } + />; } }; } diff --git a/packages/components/src/mobile/html-text-input/index.native.js b/packages/components/src/mobile/html-text-input/index.native.js index 1408be65c90530..4032fa5a74b0d5 100644 --- a/packages/components/src/mobile/html-text-input/index.native.js +++ b/packages/components/src/mobile/html-text-input/index.native.js @@ -15,7 +15,7 @@ import { withInstanceId, compose } from '@wordpress/compose'; /** * Internal dependencies */ -import { withTheme, useStyle } from '../dark-mode'; +import { withTheme } from '../dark-mode'; import HTMLInputContainer from './container'; import styles from './style.scss'; @@ -61,8 +61,9 @@ export class HTMLTextInput extends Component { } render() { - const htmlStyle = useStyle( styles.htmlView, styles.htmlViewDark, this.props.theme ); - const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark, this.props.theme ); + const { useStyle } = this.props; + const htmlStyle = useStyle( styles.htmlView, styles.htmlViewDark ); + const placeholderStyle = useStyle( styles.placeholder, styles.placeholderDark ); return ( <HTMLInputContainer parentHeight={ this.props.parentHeight }> <TextInput diff --git a/packages/components/src/mobile/html-text-input/test/index.native.js b/packages/components/src/mobile/html-text-input/test/index.native.js index 479846d3f6a960..4a10c0159293df 100644 --- a/packages/components/src/mobile/html-text-input/test/index.native.js +++ b/packages/components/src/mobile/html-text-input/test/index.native.js @@ -33,10 +33,14 @@ const findTitleTextInput = ( wrapper ) => { return findTextInputInWrapper( wrapper, { placeholder } ); }; +const useStyle = () => { + return { color: 'white' }; +}; + describe( 'HTMLTextInput', () => { it( 'HTMLTextInput renders', () => { const wrapper = shallow( - <HTMLTextInput /> + <HTMLTextInput useStyle={ useStyle } /> ); expect( wrapper ).toBeTruthy(); } ); @@ -47,6 +51,7 @@ describe( 'HTMLTextInput', () => { const wrapper = shallow( <HTMLTextInput onChange={ onChange } + useStyle={ useStyle } /> ); @@ -71,6 +76,7 @@ describe( 'HTMLTextInput', () => { <HTMLTextInput onPersist={ onPersist } onChange={ jest.fn() } + useStyle={ useStyle } /> ); @@ -101,6 +107,7 @@ describe( 'HTMLTextInput', () => { const wrapper = shallow( <HTMLTextInput editTitle={ editTitle } + useStyle={ useStyle } /> ); diff --git a/packages/components/src/toolbar/toolbar-container.native.js b/packages/components/src/toolbar/toolbar-container.native.js index f9d019450266c2..c92251c38c19cf 100644 --- a/packages/components/src/toolbar/toolbar-container.native.js +++ b/packages/components/src/toolbar/toolbar-container.native.js @@ -7,11 +7,11 @@ import { View } from 'react-native'; * Internal dependencies */ import styles from './style.scss'; -import { withTheme, useStyle } from '../mobile/dark-mode'; +import { withTheme } from '../mobile/dark-mode'; -const ToolbarContainer = ( props ) => ( - <View style={ [ useStyle( styles.container, styles.containerDark, props.theme ), props.passedStyle ] }> - { props.children } +const ToolbarContainer = ( { useStyle, passedStyle, children } ) => ( + <View style={ [ useStyle( styles.container, styles.containerDark ), passedStyle ] }> + { children } </View> ); diff --git a/packages/edit-post/src/components/header/header-toolbar/index.native.js b/packages/edit-post/src/components/header/header-toolbar/index.native.js index 278b0af30548e1..5da763a5dd8e5d 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.native.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.native.js @@ -15,7 +15,7 @@ import { Inserter, BlockToolbar, } from '@wordpress/block-editor'; -import { Toolbar, ToolbarButton, useStyle, withTheme } from '@wordpress/components'; +import { Toolbar, ToolbarButton, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -30,7 +30,7 @@ function HeaderToolbar( { undo, showInserter, showKeyboardHideButton, - theme, + useStyle, onHideKeyboard, } ) { const scrollViewRef = useRef( null ); @@ -39,7 +39,7 @@ function HeaderToolbar( { }; return ( - <View style={ useStyle( styles.container, styles.containerDark, theme ) }> + <View style={ useStyle( styles.container, styles.containerDark ) }> <ScrollView ref={ scrollViewRef } onContentSizeChange={ scrollToStart } diff --git a/packages/edit-post/src/components/layout/index.native.js b/packages/edit-post/src/components/layout/index.native.js index 5a10ed14cb9a56..36250cf27eb96f 100644 --- a/packages/edit-post/src/components/layout/index.native.js +++ b/packages/edit-post/src/components/layout/index.native.js @@ -11,7 +11,7 @@ import { sendNativeEditorDidLayout } from 'react-native-gutenberg-bridge'; import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView, useStyle, withTheme } from '@wordpress/components'; +import { HTMLTextInput, KeyboardAvoidingView, ReadableContentView, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -99,6 +99,7 @@ class Layout extends Component { render() { const { mode, + useStyle, } = this.props; const isHtmlView = mode === 'text'; @@ -114,8 +115,8 @@ class Layout extends Component { }; return ( - <SafeAreaView style={ useStyle( styles.container, styles.containerDark, this.props.theme ) } onLayout={ this.onRootViewLayout }> - <View style={ useStyle( styles.background, styles.backgroundDark, this.props.theme ) }> + <SafeAreaView style={ useStyle( styles.container, styles.containerDark ) } onLayout={ this.onRootViewLayout }> + <View style={ useStyle( styles.background, styles.backgroundDark ) }> { isHtmlView ? this.renderHTML() : this.renderVisual() } </View> <View style={ { flex: 0, flexBasis: marginBottom, height: marginBottom } } /> diff --git a/packages/edit-post/src/components/visual-editor/index.native.js b/packages/edit-post/src/components/visual-editor/index.native.js index b7c53b12aad545..1d8c7dbc094aff 100644 --- a/packages/edit-post/src/components/visual-editor/index.native.js +++ b/packages/edit-post/src/components/visual-editor/index.native.js @@ -7,7 +7,7 @@ import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; import { BlockList } from '@wordpress/block-editor'; import { PostTitle } from '@wordpress/editor'; -import { ReadableContentView, withTheme, useStyle } from '@wordpress/components'; +import { ReadableContentView, withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -20,9 +20,9 @@ class VisualEditor extends Component { editTitle, setTitleRef, title, - theme, + useStyle, } = this.props; - const blockHolderFocusedStyle = useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark, theme ); + const blockHolderFocusedStyle = useStyle( styles.blockHolderFocused, styles.blockHolderFocusedDark ); return ( <ReadableContentView> <PostTitle diff --git a/packages/rich-text/src/component/index.native.js b/packages/rich-text/src/component/index.native.js index 6191e627d56962..9c3e44440dfd5b 100644 --- a/packages/rich-text/src/component/index.native.js +++ b/packages/rich-text/src/component/index.native.js @@ -20,7 +20,7 @@ import { childrenBlock } from '@wordpress/blocks'; import { decodeEntities } from '@wordpress/html-entities'; import { BACKSPACE } from '@wordpress/keycodes'; import { isURL } from '@wordpress/url'; -import { useStyle, withTheme } from '@wordpress/components'; +import { withTheme } from '@wordpress/components'; /** * Internal dependencies @@ -771,7 +771,7 @@ export class RichText extends Component { style, __unstableIsSelected: isSelected, children, - theme, + useStyle, } = this.props; const record = this.getRecord(); @@ -782,7 +782,7 @@ export class RichText extends Component { minHeight = style.minHeight; } - const placeholderStyle = useStyle( styles.richTextPlaceholder, styles.richTextPlaceholderDark, theme ); + const placeholderStyle = useStyle( styles.richTextPlaceholder, styles.richTextPlaceholderDark ); const { color: defaultPlaceholderTextColor, @@ -792,7 +792,7 @@ export class RichText extends Component { color: defaultColor, textDecorationColor: defaultTextDecorationColor, fontFamily: defaultFontFamily, - } = useStyle( styles.richText, styles.richTextDark, theme ); + } = useStyle( styles.richText, styles.richTextDark ); let selection = null; if ( this.needsSelectionUpdate ) { @@ -821,7 +821,7 @@ export class RichText extends Component { this.firedAfterTextChanged = false; } - const dynamicStyle = useStyle( style, styles.richTextDark, theme ); + const dynamicStyle = useStyle( style, styles.richTextDark ); return ( <View> diff --git a/packages/rich-text/src/component/test/index.native.js b/packages/rich-text/src/component/test/index.native.js index ec0cbb77195244..c4ff0da2b2f415 100644 --- a/packages/rich-text/src/component/test/index.native.js +++ b/packages/rich-text/src/component/test/index.native.js @@ -8,6 +8,10 @@ import { shallow } from 'enzyme'; */ import { RichText } from '../index'; +const useStyle = () => { + return { color: 'white' }; +}; + describe( 'RichText Native', () => { let richText; @@ -40,6 +44,7 @@ describe( 'RichText Native', () => { } } formatTypes={ [] } onSelectionChange={ jest.fn() } + useStyle={ useStyle } /> ); const event = {