From b6dc02b3f04c2fd78d6f9427839717bfc8aa253b Mon Sep 17 00:00:00 2001 From: JuanMa Date: Sat, 2 Dec 2023 14:59:04 +0000 Subject: [PATCH 001/325] Docs: Fundamentals of Block Development - Update registration-of-a-block.md (#56731) Minor fixes --- .../getting-started/fundamentals/registration-of-a-block.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index 7cc8e6bcbe8b06..a330d46e676d5c 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -65,10 +65,10 @@ The function takes two params: - `$settings` (`Object`) – client-side block settings.
-The content of block.json (or any other .json file) can be imported directly in Javascript files when using a build process like the one available with wp-scripts +The content of block.json (or any other .json file) can be imported directly into Javascript files when using a build process like the one available with wp-scripts
-The client-side block settings object passed as a second parameter include two properties that are especially relevant: +The client-side block settings object passed as a second parameter includes two especially relevant properties: - `edit`: The React component that gets used in the editor for our block. - `save`: The function that returns the static HTML markup that gets saved to the Database. @@ -95,4 +95,4 @@ _See the [code above](https://github.com/WordPress/block-development-examples/bl - [`register_block_type` PHP function](https://developer.wordpress.org/reference/functions/register_block_type/) - [`registerBlockType` JS function](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) -- [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion \ No newline at end of file +- [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion From 6b5ffb76d96a609a6020e3c0ee813bb2646db287 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Sat, 2 Dec 2023 17:45:40 +0200 Subject: [PATCH 002/325] Dataviews: Extract to dedicated bundled package (#56721) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Dataviews: Extract to dedicated bundled package * Update packages/dataviews/package.json Co-authored-by: André <583546+oandregal@users.noreply.github.com> * update readme * Update packages/dataviews/README.md * include in dep.extraction plugin bundled packages --------- Co-authored-by: André <583546+oandregal@users.noreply.github.com> --- docs/manifest.json | 6 +++ package-lock.json | 49 ++++++++++++++++++- package.json | 1 + packages/dataviews/.npmrc | 1 + packages/dataviews/CHANGELOG.md | 3 ++ .../src/components => }/dataviews/README.md | 22 ++++++++- packages/dataviews/package.json | 48 ++++++++++++++++++ .../dataviews => dataviews/src}/add-filter.js | 2 +- .../dataviews => dataviews/src}/constants.js | 0 .../dataviews => dataviews/src}/dataviews.js | 0 .../src}/filter-summary.js | 2 +- .../dataviews => dataviews/src}/filters.js | 0 .../dataviews => dataviews/src}/index.js | 0 .../src}/item-actions.js | 2 +- packages/dataviews/src/lock-unlock.js | 10 ++++ .../dataviews => dataviews/src}/pagination.js | 0 .../src}/reset-filters.js | 0 .../dataviews => dataviews/src}/search.js | 2 +- .../dataviews => dataviews/src}/style.scss | 0 .../src/utils/use-debounced-input.js | 18 +++++++ .../src}/view-actions.js | 2 +- .../dataviews => dataviews/src}/view-grid.js | 0 .../dataviews => dataviews/src}/view-list.js | 0 .../dataviews => dataviews/src}/view-table.js | 2 +- .../lib/util.js | 1 + packages/edit-site/package.json | 2 +- .../src/components/page-pages/index.js | 16 +++--- .../page-templates/dataviews-templates.js | 14 +++--- .../sidebar-dataviews/dataview-item.js | 2 +- .../sidebar-dataviews/default-views.js | 6 +-- packages/edit-site/src/style.scss | 2 +- packages/private-apis/src/implementation.js | 1 + tools/webpack/packages.js | 1 + 33 files changed, 182 insertions(+), 33 deletions(-) create mode 100644 packages/dataviews/.npmrc create mode 100644 packages/dataviews/CHANGELOG.md rename packages/{edit-site/src/components => }/dataviews/README.md (86%) create mode 100644 packages/dataviews/package.json rename packages/{edit-site/src/components/dataviews => dataviews/src}/add-filter.js (98%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/constants.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/dataviews.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/filter-summary.js (97%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/filters.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/index.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/item-actions.js (99%) create mode 100644 packages/dataviews/src/lock-unlock.js rename packages/{edit-site/src/components/dataviews => dataviews/src}/pagination.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/reset-filters.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/search.js (93%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/style.scss (100%) create mode 100644 packages/dataviews/src/utils/use-debounced-input.js rename packages/{edit-site/src/components/dataviews => dataviews/src}/view-actions.js (99%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/view-grid.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/view-list.js (100%) rename packages/{edit-site/src/components/dataviews => dataviews/src}/view-table.js (99%) diff --git a/docs/manifest.json b/docs/manifest.json index 3ab4cefb2b533c..b8939951d71837 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -1619,6 +1619,12 @@ "markdown_source": "../packages/data/README.md", "parent": "packages" }, + { + "title": "@wordpress/dataviews", + "slug": "packages-dataviews", + "markdown_source": "../packages/dataviews/README.md", + "parent": "packages" + }, { "title": "@wordpress/date", "slug": "packages-date", diff --git a/package-lock.json b/package-lock.json index 94a60889e63ccf..80a3f384e808ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "@wordpress/customize-widgets": "file:packages/customize-widgets", "@wordpress/data": "file:packages/data", "@wordpress/data-controls": "file:packages/data-controls", + "@wordpress/dataviews": "file:packages/dataviews", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", @@ -18315,6 +18316,10 @@ "resolved": "packages/data-controls", "link": true }, + "node_modules/@wordpress/dataviews": { + "resolved": "packages/dataviews", + "link": true + }, "node_modules/@wordpress/date": { "resolved": "packages/date", "link": true @@ -55057,6 +55062,30 @@ "react": "^18.0.0" } }, + "packages/dataviews": { + "name": "@wordpress/dataviews", + "version": "0.1.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.10.3", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", + "classnames": "^2.3.1", + "remove-accents": "^0.5.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "packages/date": { "name": "@wordpress/date", "version": "4.47.0", @@ -55278,7 +55307,6 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", @@ -55291,6 +55319,7 @@ "@wordpress/core-commands": "file:../core-commands", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", @@ -70371,6 +70400,22 @@ "@wordpress/deprecated": "file:../deprecated" } }, + "@wordpress/dataviews": { + "version": "file:packages/dataviews", + "requires": { + "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.10.3", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", + "classnames": "^2.3.1", + "remove-accents": "^0.5.0" + } + }, "@wordpress/date": { "version": "file:packages/date", "requires": { @@ -70513,7 +70558,6 @@ "version": "file:packages/edit-site", "requires": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", @@ -70526,6 +70570,7 @@ "@wordpress/core-commands": "file:../core-commands", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/package.json b/package.json index bc4f43a47d03f9..e7514660813d9b 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@wordpress/customize-widgets": "file:packages/customize-widgets", "@wordpress/data": "file:packages/data", "@wordpress/data-controls": "file:packages/data-controls", + "@wordpress/dataviews": "file:packages/dataviews", "@wordpress/date": "file:packages/date", "@wordpress/deprecated": "file:packages/deprecated", "@wordpress/dom": "file:packages/dom", diff --git a/packages/dataviews/.npmrc b/packages/dataviews/.npmrc new file mode 100644 index 00000000000000..43c97e719a5a82 --- /dev/null +++ b/packages/dataviews/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md new file mode 100644 index 00000000000000..6ed52df1077824 --- /dev/null +++ b/packages/dataviews/CHANGELOG.md @@ -0,0 +1,3 @@ + + +## Unreleased diff --git a/packages/edit-site/src/components/dataviews/README.md b/packages/dataviews/README.md similarity index 86% rename from packages/edit-site/src/components/dataviews/README.md rename to packages/dataviews/README.md index 31e69a6675c46a..52fff8269d2459 100644 --- a/packages/edit-site/src/components/dataviews/README.md +++ b/packages/dataviews/README.md @@ -1,6 +1,16 @@ -# DataView +# DataViews -This file documents the DataViews UI component, which provides an API to render datasets using different view types (table, grid, etc.). +DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.). + +## Installation + +Install the module + +```bash +npm install @wordpress/dataviews --save +``` + +## Usage ```js

Code is Poetry.

diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json new file mode 100644 index 00000000000000..40a09050b94321 --- /dev/null +++ b/packages/dataviews/package.json @@ -0,0 +1,48 @@ +{ + "name": "@wordpress/dataviews", + "version": "0.1.0", + "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).", + "author": "The WordPress Contributors", + "license": "GPL-2.0-or-later", + "keywords": [ + "wordpress", + "gutenberg", + "dataviews" + ], + "homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/dataviews/README.md", + "repository": { + "type": "git", + "url": "https://github.com/WordPress/gutenberg.git", + "directory": "packages/dataviews" + }, + "bugs": { + "url": "https://github.com/WordPress/gutenberg/issues" + }, + "engines": { + "node": ">=12" + }, + "main": "build/index.js", + "module": "build-module/index.js", + "react-native": "src/index", + "types": "build-types", + "sideEffects": false, + "dependencies": { + "@babel/runtime": "^7.16.0", + "@tanstack/react-table": "^8.10.3", + "@wordpress/a11y": "file:../a11y", + "@wordpress/components": "file:../components", + "@wordpress/compose": "file:../compose", + "@wordpress/element": "file:../element", + "@wordpress/i18n": "file:../i18n", + "@wordpress/icons": "file:../icons", + "@wordpress/private-apis": "file:../private-apis", + "classnames": "^2.3.1", + "remove-accents": "^0.5.0" + }, + "peerDependencies": { + "react": "^18.0.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/edit-site/src/components/dataviews/add-filter.js b/packages/dataviews/src/add-filter.js similarity index 98% rename from packages/edit-site/src/components/dataviews/add-filter.js rename to packages/dataviews/src/add-filter.js index a4b9bf42a4d782..91b35e04aab967 100644 --- a/packages/edit-site/src/components/dataviews/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { unlock } from '../../lock-unlock'; +import { unlock } from './lock-unlock'; import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; const { diff --git a/packages/edit-site/src/components/dataviews/constants.js b/packages/dataviews/src/constants.js similarity index 100% rename from packages/edit-site/src/components/dataviews/constants.js rename to packages/dataviews/src/constants.js diff --git a/packages/edit-site/src/components/dataviews/dataviews.js b/packages/dataviews/src/dataviews.js similarity index 100% rename from packages/edit-site/src/components/dataviews/dataviews.js rename to packages/dataviews/src/dataviews.js diff --git a/packages/edit-site/src/components/dataviews/filter-summary.js b/packages/dataviews/src/filter-summary.js similarity index 97% rename from packages/edit-site/src/components/dataviews/filter-summary.js rename to packages/dataviews/src/filter-summary.js index ae92d0cc462737..64a5b39bf74a6d 100644 --- a/packages/edit-site/src/components/dataviews/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -13,7 +13,7 @@ import { __, sprintf } from '@wordpress/i18n'; * Internal dependencies */ import { OPERATOR_IN } from './constants'; -import { unlock } from '../../lock-unlock'; +import { unlock } from './lock-unlock'; const { DropdownMenuV2: DropdownMenu, diff --git a/packages/edit-site/src/components/dataviews/filters.js b/packages/dataviews/src/filters.js similarity index 100% rename from packages/edit-site/src/components/dataviews/filters.js rename to packages/dataviews/src/filters.js diff --git a/packages/edit-site/src/components/dataviews/index.js b/packages/dataviews/src/index.js similarity index 100% rename from packages/edit-site/src/components/dataviews/index.js rename to packages/dataviews/src/index.js diff --git a/packages/edit-site/src/components/dataviews/item-actions.js b/packages/dataviews/src/item-actions.js similarity index 99% rename from packages/edit-site/src/components/dataviews/item-actions.js rename to packages/dataviews/src/item-actions.js index bec33e915b8a80..267ed3f07e856b 100644 --- a/packages/edit-site/src/components/dataviews/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -14,7 +14,7 @@ import { moreVertical } from '@wordpress/icons'; /** * Internal dependencies */ -import { unlock } from '../../lock-unlock'; +import { unlock } from './lock-unlock'; const { DropdownMenuV2Ariakit: DropdownMenu, diff --git a/packages/dataviews/src/lock-unlock.js b/packages/dataviews/src/lock-unlock.js new file mode 100644 index 00000000000000..18318773cefefe --- /dev/null +++ b/packages/dataviews/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', + '@wordpress/dataviews' + ); diff --git a/packages/edit-site/src/components/dataviews/pagination.js b/packages/dataviews/src/pagination.js similarity index 100% rename from packages/edit-site/src/components/dataviews/pagination.js rename to packages/dataviews/src/pagination.js diff --git a/packages/edit-site/src/components/dataviews/reset-filters.js b/packages/dataviews/src/reset-filters.js similarity index 100% rename from packages/edit-site/src/components/dataviews/reset-filters.js rename to packages/dataviews/src/reset-filters.js diff --git a/packages/edit-site/src/components/dataviews/search.js b/packages/dataviews/src/search.js similarity index 93% rename from packages/edit-site/src/components/dataviews/search.js rename to packages/dataviews/src/search.js index 17a882637a7183..b226ddbffd35e4 100644 --- a/packages/edit-site/src/components/dataviews/search.js +++ b/packages/dataviews/src/search.js @@ -8,7 +8,7 @@ import { SearchControl } from '@wordpress/components'; /** * Internal dependencies */ -import useDebouncedInput from '../../utils/use-debounced-input'; +import useDebouncedInput from './utils/use-debounced-input'; export default function Search( { label, view, onChangeView } ) { const [ search, setSearch, debouncedSearch ] = useDebouncedInput( diff --git a/packages/edit-site/src/components/dataviews/style.scss b/packages/dataviews/src/style.scss similarity index 100% rename from packages/edit-site/src/components/dataviews/style.scss rename to packages/dataviews/src/style.scss diff --git a/packages/dataviews/src/utils/use-debounced-input.js b/packages/dataviews/src/utils/use-debounced-input.js new file mode 100644 index 00000000000000..26cd6c0da0e0a9 --- /dev/null +++ b/packages/dataviews/src/utils/use-debounced-input.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; +import { useDebounce } from '@wordpress/compose'; + +export default function useDebouncedInput( defaultValue = '' ) { + const [ input, setInput ] = useState( defaultValue ); + const [ debouncedInput, setDebouncedState ] = useState( defaultValue ); + + const setDebouncedInput = useDebounce( setDebouncedState, 250 ); + + useEffect( () => { + setDebouncedInput( input ); + }, [ input ] ); + + return [ input, setInput, debouncedInput ]; +} diff --git a/packages/edit-site/src/components/dataviews/view-actions.js b/packages/dataviews/src/view-actions.js similarity index 99% rename from packages/edit-site/src/components/dataviews/view-actions.js rename to packages/dataviews/src/view-actions.js index 28acd2bdb882d6..ff01155727e697 100644 --- a/packages/edit-site/src/components/dataviews/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -12,7 +12,7 @@ import { __ } from '@wordpress/i18n'; /** * Internal dependencies */ -import { unlock } from '../../lock-unlock'; +import { unlock } from './lock-unlock'; import { VIEW_LAYOUTS, LAYOUT_TABLE } from './constants'; const { diff --git a/packages/edit-site/src/components/dataviews/view-grid.js b/packages/dataviews/src/view-grid.js similarity index 100% rename from packages/edit-site/src/components/dataviews/view-grid.js rename to packages/dataviews/src/view-grid.js diff --git a/packages/edit-site/src/components/dataviews/view-list.js b/packages/dataviews/src/view-list.js similarity index 100% rename from packages/edit-site/src/components/dataviews/view-list.js rename to packages/dataviews/src/view-list.js diff --git a/packages/edit-site/src/components/dataviews/view-table.js b/packages/dataviews/src/view-table.js similarity index 99% rename from packages/edit-site/src/components/dataviews/view-table.js rename to packages/dataviews/src/view-table.js index 60b584b7f10261..bf2817293172c4 100644 --- a/packages/edit-site/src/components/dataviews/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -35,7 +35,7 @@ import { useMemo, Children, Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import { unlock } from '../../lock-unlock'; +import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; diff --git a/packages/dependency-extraction-webpack-plugin/lib/util.js b/packages/dependency-extraction-webpack-plugin/lib/util.js index 47dd1a0b7adf4c..bd328430313ce9 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/util.js +++ b/packages/dependency-extraction-webpack-plugin/lib/util.js @@ -4,6 +4,7 @@ const BUNDLED_PACKAGES = [ '@wordpress/interface', '@wordpress/undo-manager', '@wordpress/sync', + '@wordpress/dataviews', ]; /** diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 2ff4f10c084a88..92e9fd8bebe59a 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -27,7 +27,6 @@ "react-native": "src/index", "dependencies": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/api-fetch": "file:../api-fetch", "@wordpress/blob": "file:../blob", @@ -40,6 +39,7 @@ "@wordpress/core-commands": "file:../core-commands", "@wordpress/core-data": "file:../core-data", "@wordpress/data": "file:../data", + "@wordpress/dataviews": "file:../dataviews", "@wordpress/date": "file:../date", "@wordpress/deprecated": "file:../deprecated", "@wordpress/dom": "file:../dom", diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index eac10153e83266..5819b825865ef5 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -12,20 +12,20 @@ import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; +import { + DataViews, + ENUMERATION_TYPE, + VIEW_LAYOUTS, + OPERATOR_IN, + LAYOUT_GRID, + LAYOUT_TABLE, +} from '@wordpress/dataviews'; /** * Internal dependencies */ import Page from '../page'; import Link from '../routes/link'; -import { - DataViews, - VIEW_LAYOUTS, - ENUMERATION_TYPE, - LAYOUT_GRID, - LAYOUT_TABLE, - OPERATOR_IN, -} from '../dataviews'; import { default as DEFAULT_VIEWS } from '../sidebar-dataviews/default-views'; import { trashPostAction, diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 5c24db0c537f78..07f32441da84f3 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -23,6 +23,13 @@ import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; +import { + DataViews, + ENUMERATION_TYPE, + OPERATOR_IN, + LAYOUT_GRID, + LAYOUT_TABLE, +} from '@wordpress/dataviews'; /** * Internal dependencies @@ -31,13 +38,6 @@ import Page from '../page'; import Link from '../routes/link'; import { useAddedBy, AvatarImage } from '../list/added-by'; import { TEMPLATE_POST_TYPE } from '../../utils/constants'; -import { - DataViews, - ENUMERATION_TYPE, - OPERATOR_IN, - LAYOUT_GRID, - LAYOUT_TABLE, -} from '../dataviews'; import { useResetTemplateAction, deleteTemplateAction, diff --git a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js index cd76a923fa6b6c..cbcb4f2f8ed59c 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js +++ b/packages/edit-site/src/components/sidebar-dataviews/dataview-item.js @@ -8,6 +8,7 @@ import classnames from 'classnames'; */ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { __experimentalHStack as HStack } from '@wordpress/components'; +import { VIEW_LAYOUTS } from '@wordpress/dataviews'; /** * Internal dependencies @@ -15,7 +16,6 @@ import { __experimentalHStack as HStack } from '@wordpress/components'; import { useLink } from '../routes/link'; import SidebarNavigationItem from '../sidebar-navigation-item'; import { unlock } from '../../lock-unlock'; -import { VIEW_LAYOUTS } from '../dataviews'; const { useLocation } = unlock( routerPrivateApis ); export default function DataViewItem( { diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index d71265860b29ed..d22786cff0b5c8 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -3,11 +3,7 @@ */ import { __ } from '@wordpress/i18n'; import { trash } from '@wordpress/icons'; - -/** - * Internal dependencies - */ -import { LAYOUT_TABLE, OPERATOR_IN } from '../dataviews'; +import { LAYOUT_TABLE, OPERATOR_IN } from '@wordpress/dataviews'; const DEFAULT_PAGE_BASE = { type: LAYOUT_TABLE, diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 30fbec3a94cc1b..5a93375afec8b0 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -1,10 +1,10 @@ @import "../../interface/src/style.scss"; +@import "../../dataviews/src/style.scss"; @import "./components/add-new-template/style.scss"; @import "./components/block-editor/style.scss"; @import "./components/canvas-loader/style.scss"; @import "./components/code-editor/style.scss"; -@import "./components/dataviews/style.scss"; @import "./components/global-styles/style.scss"; @import "./components/global-styles/screen-revisions/style.scss"; @import "./components/header-edit-mode/style.scss"; diff --git a/packages/private-apis/src/implementation.js b/packages/private-apis/src/implementation.js index 400d84c8cf584d..a7da5bc9726554 100644 --- a/packages/private-apis/src/implementation.js +++ b/packages/private-apis/src/implementation.js @@ -27,6 +27,7 @@ const CORE_MODULES_USING_PRIVATE_APIS = [ '@wordpress/patterns', '@wordpress/reusable-blocks', '@wordpress/router', + '@wordpress/dataviews', ]; /** diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index a76889622b4a2f..86554d5f139098 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -29,6 +29,7 @@ const BUNDLED_PACKAGES = [ '@wordpress/interface', '@wordpress/undo-manager', '@wordpress/sync', + '@wordpress/dataviews', ]; // PHP files in packages that have to be copied during build. From 3251a51a104c7435502e06c6e65982e6f3131143 Mon Sep 17 00:00:00 2001 From: Akira Tachibana Date: Sun, 3 Dec 2023 14:07:09 +0900 Subject: [PATCH 003/325] Docs: Fix {% end %} tab position to show the text (#56735) --- docs/how-to-guides/themes/theme-json.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-to-guides/themes/theme-json.md b/docs/how-to-guides/themes/theme-json.md index bd27abca1494c6..1f7480649f6ab1 100644 --- a/docs/how-to-guides/themes/theme-json.md +++ b/docs/how-to-guides/themes/theme-json.md @@ -103,10 +103,10 @@ body { } ``` -- **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. - {% end %} +- **Custom properties**: there's also a mechanism to create your own CSS Custom Properties. + {% codetabs %} {% Input %} From dcb8a43a6eda293f88efb1138d8dc1636e94ba92 Mon Sep 17 00:00:00 2001 From: Ramon Date: Mon, 4 Dec 2023 15:46:31 +1100 Subject: [PATCH 004/325] Pruning unit tests that have already been migrated to core. (#56492) Keeping tests that cover latest compat changes (6.5) --- ...lobal-styles-revisions-controller-test.php | 121 ++---------------- 1 file changed, 14 insertions(+), 107 deletions(-) diff --git a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php b/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php index 857e8fa297cf17..2ae53f9338389e 100644 --- a/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php +++ b/phpunit/class-gutenberg-rest-global-styles-revisions-controller-test.php @@ -14,26 +14,11 @@ class Gutenberg_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_RES */ protected static $admin_id; - /** - * @var int - */ - protected static $second_admin_id; - - /** - * @var int - */ - protected static $author_id; - /** * @var int */ protected static $global_styles_id; - /** - * @var int - */ - private $total_revisions; - /** * @var array */ @@ -44,47 +29,17 @@ class Gutenberg_REST_Global_Styles_Revisions_Controller_Test extends WP_Test_RES */ private $revision_1_id; - /** - * @var array - */ - private $revision_2; - - /** - * @var int - */ - private $revision_2_id; - - /** - * @var array - */ - private $revision_3; - - /** - * @var int - */ - private $revision_3_id; - /** * Create fake data before our tests run. * * @param WP_UnitTest_Factory $factory Helper that lets us create fake data. */ public static function wpSetupBeforeClass( $factory ) { - self::$admin_id = $factory->user->create( - array( - 'role' => 'administrator', - ) - ); - self::$second_admin_id = $factory->user->create( + self::$admin_id = $factory->user->create( array( 'role' => 'administrator', ) ); - self::$author_id = $factory->user->create( - array( - 'role' => 'author', - ) - ); wp_set_current_user( self::$admin_id ); // This creates the global styles for the current theme. @@ -199,8 +154,6 @@ public static function wpSetupBeforeClass( $factory ) { */ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); - self::delete_user( self::$second_admin_id ); - self::delete_user( self::$author_id ); } /** @@ -209,24 +162,16 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); switch_theme( 'emptytheme' ); - $revisions = wp_get_post_revisions( self::$global_styles_id ); - $this->total_revisions = count( $revisions ); - + $revisions = wp_get_post_revisions( self::$global_styles_id ); $this->revision_1 = array_pop( $revisions ); $this->revision_1_id = $this->revision_1->ID; - $this->revision_2 = array_pop( $revisions ); - $this->revision_2_id = $this->revision_2->ID; - - $this->revision_3 = array_pop( $revisions ); - $this->revision_3_id = $this->revision_3->ID; - /* * For some reason the `rest_api_init` doesn't run early enough to ensure an overwritten `get_item_schema()` * is used. So we manually call it here. * See: https://github.com/WordPress/gutenberg/pull/52370#issuecomment-1643331655. */ - $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_4(); + $global_styles_revisions_controller = new Gutenberg_REST_Global_Styles_Revisions_Controller_6_5(); $global_styles_revisions_controller->register_routes(); } @@ -237,11 +182,6 @@ public function set_up() { */ public function test_register_routes() { $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( - '/wp/v2/global-styles/(?P[\d]+)/revisions', - $routes, - 'Global style revisions based on the given parentID route does not exist.' - ); $this->assertArrayHasKey( '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', $routes, @@ -277,32 +217,6 @@ protected function check_get_revision_response( $response_revision_item, $revisi ); } - /** - * @ticket 58524 - * - * @covers Gutenberg_REST_Global_Styles_Revisions_Controller_6_4::get_items - */ - public function test_get_items() { - wp_set_current_user( self::$admin_id ); - - $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); - $this->assertCount( $this->total_revisions, $data, 'Check that correct number of revisions exists.' ); - - // Reverse chronology. - $this->assertSame( $this->revision_3_id, $data[0]['id'] ); - $this->check_get_revision_response( $data[0], $this->revision_3 ); - - $this->assertSame( $this->revision_2_id, $data[1]['id'] ); - $this->check_get_revision_response( $data[1], $this->revision_2 ); - - $this->assertSame( $this->revision_1_id, $data[2]['id'] ); - $this->check_get_revision_response( $data[2], $this->revision_1 ); - } - /** * @ticket 59810 * @@ -336,26 +250,19 @@ public function test_get_item_invalid_revision_id_should_error() { } /** - * @ticket 58524 - * - * @covers Gutenberg_REST_Global_Styles_Revisions_Controller_6_4::get_item_schema + * @doesNotPerformAssertions */ - public function test_get_item_schema() { - $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $properties = $data['schema']['properties']; + public function test_get_items() { + // Unit tests have been more to WordPress Core for test_get_items(). + // No unique compat unit tests exist. + } - $this->assertCount( 9, $properties, 'Schema properties array has exactly 9 elements.' ); - $this->assertArrayHasKey( 'id', $properties, 'Schema properties array has "id" key.' ); - $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array has "styles" key.' ); - $this->assertArrayHasKey( 'settings', $properties, 'Schema properties array has "settings" key.' ); - $this->assertArrayHasKey( 'parent', $properties, 'Schema properties array has "parent" key.' ); - $this->assertArrayHasKey( 'author', $properties, 'Schema properties array has "author" key.' ); - $this->assertArrayHasKey( 'date', $properties, 'Schema properties array has "date" key.' ); - $this->assertArrayHasKey( 'date_gmt', $properties, 'Schema properties array has "date_gmt" key.' ); - $this->assertArrayHasKey( 'modified', $properties, 'Schema properties array has "modified" key.' ); - $this->assertArrayHasKey( 'modified_gmt', $properties, 'Schema properties array has "modified_gmt" key.' ); + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Unit tests have been more to WordPress Core for test_get_item_schema(). + // No unique compat unit tests exist. } /** From 46850a26337ad44484a38e5df76c720c1243c49f Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:16:30 +0900 Subject: [PATCH 005/325] Fixture Tests: Correctly generate fixture files for form-related blocks (#56719) * Fixture Tests: Correctly generate fixture files for form-related blocks * Fix unit test * Try to fix unit tests --- packages/block-library/src/form-input/save.js | 6 +- phpunit/class-block-fixture-test.php | 8 ++ .../fixtures/blocks/core__form-input.html | 4 +- .../fixtures/blocks/core__form-input.json | 11 ++- .../blocks/core__form-input.parsed.json | 9 +-- .../blocks/core__form-input.serialized.html | 4 +- .../core__form-submission-notification.html | 5 ++ .../core__form-submission-notification.json | 31 ++++++++ ...__form-submission-notification.parsed.json | 35 ++++++++ ...rm-submission-notification.serialized.html | 5 ++ .../blocks/core__form-submit-button.html | 7 ++ .../blocks/core__form-submit-button.json | 26 ++++++ .../core__form-submit-button.parsed.json | 38 +++++++++ .../core__form-submit-button.serialized.html | 7 ++ .../fixtures/blocks/core__form.html | 22 ++++-- .../fixtures/blocks/core__form.json | 79 ++++++++++++------- .../fixtures/blocks/core__form.parsed.json | 62 ++++++++++----- .../blocks/core__form.serialized.html | 32 +++++--- .../full-content/full-content.test.js | 18 +++++ 19 files changed, 324 insertions(+), 85 deletions(-) create mode 100644 test/integration/fixtures/blocks/core__form-submission-notification.html create mode 100644 test/integration/fixtures/blocks/core__form-submission-notification.json create mode 100644 test/integration/fixtures/blocks/core__form-submission-notification.parsed.json create mode 100644 test/integration/fixtures/blocks/core__form-submission-notification.serialized.html create mode 100644 test/integration/fixtures/blocks/core__form-submit-button.html create mode 100644 test/integration/fixtures/blocks/core__form-submit-button.json create mode 100644 test/integration/fixtures/blocks/core__form-submit-button.parsed.json create mode 100644 test/integration/fixtures/blocks/core__form-submit-button.serialized.html diff --git a/packages/block-library/src/form-input/save.js b/packages/block-library/src/form-input/save.js index 0cca31ca423ee6..1404e40634e82e 100644 --- a/packages/block-library/src/form-input/save.js +++ b/packages/block-library/src/form-input/save.js @@ -12,6 +12,7 @@ import { __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, } from '@wordpress/block-editor'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Get the name attribute from a content string. @@ -21,11 +22,8 @@ import { * @return {string} Returns the slug. */ const getNameFromLabel = ( content ) => { - const dummyElement = document.createElement( 'div' ); - dummyElement.innerHTML = content; - // Get the slug. return ( - removeAccents( dummyElement.innerText ) + removeAccents( stripHTML( content ) ) // Convert anything that's not a letter or number to a hyphen. .replace( /[^\p{L}\p{N}]+/gu, '-' ) // Convert to lowercase diff --git a/phpunit/class-block-fixture-test.php b/phpunit/class-block-fixture-test.php index a3e47cc7b28337..adf49445261d15 100644 --- a/phpunit/class-block-fixture-test.php +++ b/phpunit/class-block-fixture-test.php @@ -7,6 +7,12 @@ class Block_Fixture_Test extends WP_UnitTestCase { + public function filter_allowed_html( $tags ) { + $tags['form']['class'] = true; + $tags['form']['enctype'] = true; + return $tags; + } + /** * Tests that running the serialised block content through KSES doesn't cause the * HTML to change. @@ -20,7 +26,9 @@ public function test_kses_doesnt_change_fixtures( $block, $filename ) { $block = preg_replace( "/href=['\"]data:[^'\"]+['\"]/", 'href="https://wordpress.org/foo.jpg"', $block ); $block = preg_replace( '/url\(data:[^)]+\)/', 'url(https://wordpress.org/foo.jpg)', $block ); + add_filter( 'wp_kses_allowed_html', array( $this, 'filter_allowed_html' ) ); $kses_block = wp_kses_post( $block ); + remove_filter( 'wp_kses_allowed_html', array( $this, 'filter_allowed_html' ) ); // KSES adds a space at the end of self-closing tags, add it to the original to match. $block = preg_replace( '|([^ ])/>|', '$1 />', $block ); diff --git a/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html index 718c592641bc32..f30d44a2503d20 100644 --- a/test/integration/fixtures/blocks/core__form-input.html +++ b/test/integration/fixtures/blocks/core__form-input.html @@ -1,3 +1,3 @@ - - + + diff --git a/test/integration/fixtures/blocks/core__form-input.json b/test/integration/fixtures/blocks/core__form-input.json index 68dfb9a36e4e63..fee4df284f1156 100644 --- a/test/integration/fixtures/blocks/core__form-input.json +++ b/test/integration/fixtures/blocks/core__form-input.json @@ -1,11 +1,14 @@ [ { - "name": "core/missing", + "name": "core/form-input", "isValid": true, "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "", - "originalContent": "\n\n" + "type": "text", + "label": "Label", + "inlineLabel": false, + "required": false, + "value": "", + "visibilityPermissions": "all" }, "innerBlocks": [] } diff --git a/test/integration/fixtures/blocks/core__form-input.parsed.json b/test/integration/fixtures/blocks/core__form-input.parsed.json index 73058fc2e17f0b..f92379b595276f 100644 --- a/test/integration/fixtures/blocks/core__form-input.parsed.json +++ b/test/integration/fixtures/blocks/core__form-input.parsed.json @@ -1,14 +1,11 @@ [ { "blockName": "core/form-input", - "attrs": { - "label": "Name", - "required": true - }, + "attrs": {}, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n\n", "innerContent": [ - "\n\n" + "\n\n" ] } ] diff --git a/test/integration/fixtures/blocks/core__form-input.serialized.html b/test/integration/fixtures/blocks/core__form-input.serialized.html index 718c592641bc32..f30d44a2503d20 100644 --- a/test/integration/fixtures/blocks/core__form-input.serialized.html +++ b/test/integration/fixtures/blocks/core__form-input.serialized.html @@ -1,3 +1,3 @@ - - + + diff --git a/test/integration/fixtures/blocks/core__form-submission-notification.html b/test/integration/fixtures/blocks/core__form-submission-notification.html new file mode 100644 index 00000000000000..04eaeef77097a2 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submission-notification.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/test/integration/fixtures/blocks/core__form-submission-notification.json b/test/integration/fixtures/blocks/core__form-submission-notification.json new file mode 100644 index 00000000000000..dac7502e9716ca --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submission-notification.json @@ -0,0 +1,31 @@ +[ + { + "name": "core/form-submission-notification", + "isValid": true, + "attributes": { + "type": "success" + }, + "innerBlocks": [ + { + "name": "core/paragraph", + "isValid": true, + "attributes": { + "content": "Your form has been submitted successfully.", + "dropCap": false, + "backgroundColor": "#00D084", + "textColor": "#000000", + "style": { + "elements": { + "link": { + "color": { + "text": "#000000" + } + } + } + } + }, + "innerBlocks": [] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-submission-notification.parsed.json b/test/integration/fixtures/blocks/core__form-submission-notification.parsed.json new file mode 100644 index 00000000000000..c339aef3b765c5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submission-notification.parsed.json @@ -0,0 +1,35 @@ +[ + { + "blockName": "core/form-submission-notification", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/paragraph", + "attrs": { + "style": { + "elements": { + "link": { + "color": { + "text": "#000000" + } + } + } + }, + "backgroundColor": "#00D084", + "textColor": "#000000" + }, + "innerBlocks": [], + "innerHTML": "\n

Your form has been submitted successfully.

\n", + "innerContent": [ + "\n

Your form has been submitted successfully.

\n" + ] + } + ], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
", + null, + "
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-submission-notification.serialized.html b/test/integration/fixtures/blocks/core__form-submission-notification.serialized.html new file mode 100644 index 00000000000000..696d0ff2118489 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submission-notification.serialized.html @@ -0,0 +1,5 @@ + +
+ +
+ diff --git a/test/integration/fixtures/blocks/core__form-submit-button.html b/test/integration/fixtures/blocks/core__form-submit-button.html new file mode 100644 index 00000000000000..a47c4642327505 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submit-button.html @@ -0,0 +1,7 @@ + +
+
+
+
+
+ diff --git a/test/integration/fixtures/blocks/core__form-submit-button.json b/test/integration/fixtures/blocks/core__form-submit-button.json new file mode 100644 index 00000000000000..eca9cbb3f1a5d8 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submit-button.json @@ -0,0 +1,26 @@ +[ + { + "name": "core/form-submit-button", + "isValid": true, + "attributes": {}, + "innerBlocks": [ + { + "name": "core/buttons", + "isValid": true, + "attributes": {}, + "innerBlocks": [ + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "button", + "type": "submit", + "text": "Submit" + }, + "innerBlocks": [] + } + ] + } + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-submit-button.parsed.json b/test/integration/fixtures/blocks/core__form-submit-button.parsed.json new file mode 100644 index 00000000000000..3c674769ca06d5 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submit-button.parsed.json @@ -0,0 +1,38 @@ +[ + { + "blockName": "core/form-submit-button", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/buttons", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/button", + "attrs": { + "tagName": "button", + "type": "submit" + }, + "innerBlocks": [], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
\n" + ] + } + ], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
", + null, + "
\n" + ] + } + ], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
", + null, + "
\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-submit-button.serialized.html b/test/integration/fixtures/blocks/core__form-submit-button.serialized.html new file mode 100644 index 00000000000000..a47c4642327505 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-submit-button.serialized.html @@ -0,0 +1,7 @@ + +
+
+
+
+
+ diff --git a/test/integration/fixtures/blocks/core__form.html b/test/integration/fixtures/blocks/core__form.html index ab18e0e11c81a5..f443172601a3b5 100644 --- a/test/integration/fixtures/blocks/core__form.html +++ b/test/integration/fixtures/blocks/core__form.html @@ -1,21 +1,27 @@ -
- + + + - + - + - + - -
-
+ +
+
+
+
+
+ + diff --git a/test/integration/fixtures/blocks/core__form.json b/test/integration/fixtures/blocks/core__form.json index 6bad568b12c26d..d1dd3738a5801b 100644 --- a/test/integration/fixtures/blocks/core__form.json +++ b/test/integration/fixtures/blocks/core__form.json @@ -1,62 +1,87 @@ [ { - "name": "core/missing", + "name": "core/form", "isValid": true, "attributes": { - "originalName": "core/form", - "originalUndelimitedContent": "
\n\n\n\n\n
\n
", - "originalContent": "\n
\n\n\n\n\n\n\n\n\n\n\n\n\n\n
\n\n
\n" + "submissionMethod": "email", + "method": "post" }, "innerBlocks": [ { - "name": "core/missing", + "name": "core/form-input", "isValid": true, "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "", - "originalContent": "\n\n" + "type": "text", + "label": "Name", + "inlineLabel": false, + "required": true, + "value": "", + "visibilityPermissions": "all" }, "innerBlocks": [] }, { - "name": "core/missing", + "name": "core/form-input", "isValid": true, "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "", - "originalContent": "\n\n" + "type": "email", + "label": "Email", + "inlineLabel": false, + "required": true, + "value": "", + "visibilityPermissions": "all" }, "innerBlocks": [] }, { - "name": "core/missing", + "name": "core/form-input", "isValid": true, "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "", - "originalContent": "\n\n" + "type": "url", + "label": "Website", + "inlineLabel": false, + "required": false, + "value": "", + "visibilityPermissions": "all" }, "innerBlocks": [] }, { - "name": "core/missing", + "name": "core/form-input", "isValid": true, "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "", - "originalContent": "\n\n" + "type": "textarea", + "label": "Comment", + "inlineLabel": false, + "required": true, + "value": "", + "visibilityPermissions": "all" }, "innerBlocks": [] }, { - "name": "core/missing", + "name": "core/form-submit-button", "isValid": true, - "attributes": { - "originalName": "core/form-input", - "originalUndelimitedContent": "
", - "originalContent": "\n
\n" - }, - "innerBlocks": [] + "attributes": {}, + "innerBlocks": [ + { + "name": "core/buttons", + "isValid": true, + "attributes": {}, + "innerBlocks": [ + { + "name": "core/button", + "isValid": true, + "attributes": { + "tagName": "button", + "type": "submit", + "text": "Submit" + }, + "innerBlocks": [] + } + ] + } + ] } ] } diff --git a/test/integration/fixtures/blocks/core__form.parsed.json b/test/integration/fixtures/blocks/core__form.parsed.json index 379bee84c84e10..fb2daff4a47411 100644 --- a/test/integration/fixtures/blocks/core__form.parsed.json +++ b/test/integration/fixtures/blocks/core__form.parsed.json @@ -10,9 +10,9 @@ "required": true }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n\n", "innerContent": [ - "\n\n" + "\n\n" ] }, { @@ -23,9 +23,9 @@ "required": true }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n\n", "innerContent": [ - "\n\n" + "\n\n" ] }, { @@ -35,9 +35,9 @@ "label": "Website" }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n\n", "innerContent": [ - "\n\n" + "\n\n" ] }, { @@ -48,27 +48,51 @@ "required": true }, "innerBlocks": [], - "innerHTML": "\n\n", + "innerHTML": "\n\n", "innerContent": [ - "\n\n" + "\n\n" ] }, { - "blockName": "core/form-input", - "attrs": { - "type": "submit", - "label": "Submit" - }, - "innerBlocks": [], - "innerHTML": "\n
\n", + "blockName": "core/form-submit-button", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/buttons", + "attrs": {}, + "innerBlocks": [ + { + "blockName": "core/button", + "attrs": { + "tagName": "button", + "type": "submit" + }, + "innerBlocks": [], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
\n" + ] + } + ], + "innerHTML": "\n
\n", + "innerContent": [ + "\n
", + null, + "
\n" + ] + } + ], + "innerHTML": "\n
\n", "innerContent": [ - "\n
\n" + "\n
", + null, + "
\n" ] } ], - "innerHTML": "\n
\n\n\n\n\n\n\n\n
\n", + "innerHTML": "\n
\n\n\n\n\n\n\n\n\n\n
\n", "innerContent": [ - "\n
", + "\n\n", null, "\n\n", null, @@ -78,7 +102,7 @@ null, "\n\n", null, - "
\n" + "\n\n" ] } ] diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html index 585a50868b85e0..9bfff780f50dbf 100644 --- a/test/integration/fixtures/blocks/core__form.serialized.html +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -1,19 +1,25 @@ -
- - + + - - + + + - - + + + - - + + + - -
- -
+ + +
+
+
+
+
+ diff --git a/test/integration/full-content/full-content.test.js b/test/integration/full-content/full-content.test.js index fab2edd98942f8..f825de04771442 100644 --- a/test/integration/full-content/full-content.test.js +++ b/test/integration/full-content/full-content.test.js @@ -35,6 +35,13 @@ import { writeBlockFixtureSerializedHTML, } from '../fixtures'; +/* eslint-disable no-restricted-syntax */ +import * as form from '@wordpress/block-library/src/form'; +import * as formInput from '@wordpress/block-library/src/form-input'; +import * as formSubmitButton from '@wordpress/block-library/src/form-submit-button'; +import * as formSubmissionNotification from '@wordpress/block-library/src/form-submission-notification'; +/* eslint-enable no-restricted-syntax */ + const blockBasenames = getAvailableBlockFixturesBasenames(); /** @@ -64,6 +71,17 @@ describe( 'full post content fixture', () => { ); unstable__bootstrapServerSideBlockDefinitions( blockDefinitions ); registerCoreBlocks(); + + // Form-related blocks will not be registered unless they are opted + // in on the experimental settings page. Therefore, these blocks + // must be explicitly registered. + registerCoreBlocks( [ + form, + formInput, + formSubmitButton, + formSubmissionNotification, + ] ); + if ( process.env.IS_GUTENBERG_PLUGIN ) { __experimentalRegisterExperimentalCoreBlocks( { enableFSEBlocks: true, From 3223102084f9568dc2f748da703a349fd2a4abc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Mon, 4 Dec 2023 09:47:31 +0100 Subject: [PATCH 006/325] DataViews: add support for `NOT IN` operator in filter (#56479) --- packages/dataviews/README.md | 2 + packages/dataviews/src/add-filter.js | 3 +- packages/dataviews/src/constants.js | 1 + packages/dataviews/src/filter-summary.js | 222 +++++++++++++---- packages/dataviews/src/filters.js | 23 +- packages/dataviews/src/index.js | 1 + packages/dataviews/src/view-table.js | 224 +++++++++++++----- .../src/components/page-pages/index.js | 13 +- 8 files changed, 378 insertions(+), 111 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 52fff8269d2459..99b36d8f53c11c 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -185,6 +185,8 @@ Example: - `type`: the type of the field. Used to generate the proper filters. Only `enumeration` available at the moment. - `enableSorting`: whether the data can be sorted by the given field. True by default. - `enableHiding`: whether the field can be hidden. True by default. +- `filterBy`: configuration for the filters. + - `operators`: the list of operators supported by the field. ## Actions diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index 91b35e04aab967..715135a533fb4b 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -36,8 +36,7 @@ export default function AddFilter( { fields, view, onChangeView } ) { name: field.header, elements: field.elements || [], isVisible: view.filters.some( - ( f ) => - f.field === field.id && f.operator === OPERATOR_IN + ( f ) => f.field === field.id ), } ); } diff --git a/packages/dataviews/src/constants.js b/packages/dataviews/src/constants.js index 8394b52bf172f0..c4d1d9242f08a3 100644 --- a/packages/dataviews/src/constants.js +++ b/packages/dataviews/src/constants.js @@ -16,6 +16,7 @@ export const ENUMERATION_TYPE = 'enumeration'; // Filter operators. export const OPERATOR_IN = 'in'; +export const OPERATOR_NOT_IN = 'notIn'; // View layouts. export const LAYOUT_TABLE = 'table'; diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 64a5b39bf74a6d..3c30c6837103a7 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -6,20 +6,72 @@ import { privateApis as componentsPrivateApis, Icon, } from '@wordpress/components'; -import { chevronDown } from '@wordpress/icons'; +import { chevronDown, chevronRightSmall, check } from '@wordpress/icons'; import { __, sprintf } from '@wordpress/i18n'; +import { Children, Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import { OPERATOR_IN } from './constants'; +import { OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; import { unlock } from './lock-unlock'; const { DropdownMenuV2: DropdownMenu, - DropdownMenuCheckboxItemV2: DropdownMenuCheckboxItem, + DropdownMenuGroupV2: DropdownMenuGroup, + DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuSeparatorV2: DropdownMenuSeparator, + DropdownSubMenuV2: DropdownSubMenu, + DropdownSubMenuTriggerV2: DropdownSubMenuTrigger, } = unlock( componentsPrivateApis ); +const FilterText = ( { activeElement, filterInView, filter } ) => { + if ( activeElement === undefined ) { + return filter.name; + } + + if ( + activeElement !== undefined && + filterInView?.operator === OPERATOR_IN + ) { + return sprintf( + /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is Admin". */ + __( '%1$s is %2$s' ), + filter.name, + activeElement.label + ); + } + + if ( + activeElement !== undefined && + filterInView?.operator === OPERATOR_NOT_IN + ) { + return sprintf( + /* translators: 1: Filter name. 2: Filter value. e.g.: "Author is not Admin". */ + __( '%1$s is not %2$s' ), + filter.name, + activeElement.label + ); + } + + return sprintf( + /* translators: 1: Filter name e.g.: "Unknown status for Author". */ + __( 'Unknown status for %1$s' ), + filter.name + ); +}; + +function WithSeparators( { children } ) { + return Children.toArray( children ) + .filter( Boolean ) + .map( ( child, i ) => ( + + { i > 0 && } + { child } + + ) ); +} + export default function FilterSummary( { filter, view, onChangeView } ) { const filterInView = view.filters.find( ( f ) => f.field === filter.field ); const activeElement = filter.elements.find( @@ -31,49 +83,139 @@ export default function FilterSummary( { filter, view, onChangeView } ) { key={ filter.field } trigger={ } > - { filter.elements.map( ( element ) => { - return ( - - onChangeView( ( currentView ) => ( { - ...currentView, - page: 1, - filters: [ - ...view.filters.filter( - ( f ) => f.field !== filter.field - ), - { - field: filter.field, - operator: OPERATOR_IN, - value: - activeElement?.value === - element.value - ? undefined - : element.value, - }, - ], - } ) ) + + + { filter.elements.map( ( element ) => { + return ( + + ) + } + onSelect={ () => + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => + f.field !== filter.field + ), + { + field: filter.field, + operator: + filterInView?.operator || + filter.operators[ 0 ], + value: + activeElement?.value === + element.value + ? undefined + : element.value, + }, + ], + } ) ) + } + > + { element.label } + + ); + } ) } + + { filter.operators.length > 1 && ( + + { filterInView.operator === OPERATOR_IN + ? __( 'Is' ) + : __( 'Is not' ) } + { ' ' } + + } + > + { __( 'Conditions' ) } + } > - { element.label } - - ); - } ) } + + ) + } + onSelect={ () => + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => f.field !== filter.field + ), + { + field: filter.field, + operator: OPERATOR_IN, + value: filterInView?.value, + }, + ], + } ) ) + } + > + { __( 'Is' ) } + + + ) + } + onSelect={ () => + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => f.field !== filter.field + ), + { + field: filter.field, + operator: OPERATOR_NOT_IN, + value: filterInView?.value, + }, + ], + } ) ) + } + > + { __( 'Is not' ) } + + + ) } + ); } diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 0583fd1e45eb60..e2d24e7a848eea 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -4,7 +4,17 @@ import FilterSummary from './filter-summary'; import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; -import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; +import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; + +const operatorsFromField = ( field ) => { + let operators = field.filterBy?.operators; + if ( ! operators || ! Array.isArray( operators ) ) { + operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; + } + return operators.filter( ( operator ) => + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + ); +}; export default function Filters( { fields, view, onChangeView } ) { const filters = []; @@ -13,15 +23,24 @@ export default function Filters( { fields, view, onChangeView } ) { return; } + const operators = operatorsFromField( field ); + if ( operators.length === 0 ) { + return; + } + switch ( field.type ) { case ENUMERATION_TYPE: filters.push( { field: field.id, name: field.header, elements: field.elements || [], + operators, isVisible: view.filters.some( ( f ) => - f.field === field.id && f.operator === OPERATOR_IN + f.field === field.id && + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( + f.operator + ) ), } ); } diff --git a/packages/dataviews/src/index.js b/packages/dataviews/src/index.js index 7f429923ef4b13..01fdaeae0073bf 100644 --- a/packages/dataviews/src/index.js +++ b/packages/dataviews/src/index.js @@ -6,4 +6,5 @@ export { LAYOUT_LIST, ENUMERATION_TYPE, OPERATOR_IN, + OPERATOR_NOT_IN, } from './constants'; diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index bf2817293172c4..8b6422b4be11a7 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -37,7 +37,7 @@ import { useMemo, Children, Fragment } from '@wordpress/element'; */ import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; -import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; +import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; const { DropdownMenuV2: DropdownMenu, @@ -70,15 +70,49 @@ function HeaderMenu( { dataView, header } ) { } const sortedDirection = header.column.getIsSorted(); - let filter; + let filter, filterInView; + const otherFilters = []; if ( header.column.columnDef.type === ENUMERATION_TYPE ) { - filter = { - field: header.column.columnDef.id, - elements: header.column.columnDef.elements || [], - }; + let columnOperators = header.column.columnDef.filterBy?.operators; + if ( ! columnOperators || ! Array.isArray( columnOperators ) ) { + columnOperators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; + } + const operators = columnOperators.filter( ( operator ) => + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + ); + if ( operators.length >= 0 ) { + filter = { + field: header.column.columnDef.id, + operators, + elements: header.column.columnDef.elements || [], + }; + filterInView = { + field: filter.field, + operator: filter.operators[ 0 ], + value: undefined, + }; + } } const isFilterable = !! filter; + if ( isFilterable ) { + const columnFilters = dataView.getState().columnFilters; + columnFilters.forEach( ( columnFilter ) => { + const [ field, operator ] = + Object.keys( columnFilter )[ 0 ].split( ':' ); + const value = Object.values( columnFilter )[ 0 ]; + if ( field === filter.field ) { + filterInView = { + field, + operator, + value, + }; + } else { + otherFilters.push( columnFilter ); + } + } ); + } + return ( } > - { filter.elements.map( ( element ) => { - let isActive = false; - const columnFilters = - dataView.getState().columnFilters; - const columnFilter = columnFilters.find( - ( f ) => - Object.keys( f )[ 0 ].split( - ':' - )[ 0 ] === filter.field - ); - - if ( columnFilter ) { - const value = - Object.values( columnFilter )[ 0 ]; - // Intentionally use loose comparison, so it does type conversion. - // This covers the case where a top-level filter for the same field converts a number into a string. - isActive = element.value == value; // eslint-disable-line eqeqeq - } - - return ( - + + + { filter.elements.map( ( element ) => { + let isActive = false; + if ( filterInView ) { + // Intentionally use loose comparison, so it does type conversion. + // This covers the case where a top-level filter for the same field converts a number into a string. + /* eslint-disable eqeqeq */ + isActive = + element.value == + filterInView.value; + /* eslint-enable eqeqeq */ } - onSelect={ () => { - const otherFilters = - columnFilters?.filter( - ( f ) => { - const [ - field, - operator, - ] = - Object.keys( - f - )[ 0 ].split( ':' ); - return ( - field !== - filter.field || - operator !== - OPERATOR_IN - ); - } - ); - dataView.setColumnFilters( [ - ...otherFilters, - { - [ filter.field + ':in' ]: - isActive - ? undefined - : element.value, - }, - ] ); - } } + return ( + + ) + } + onSelect={ () => { + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.field + + ':' + + filterInView?.operator ]: + isActive + ? undefined + : element.value, + }, + ] ); + } } + > + { element.label } + + ); + } ) } + + { filter.operators.length > 1 && ( + + { filterInView.operator === + OPERATOR_IN + ? __( 'Is' ) + : __( 'Is not' ) } + { ' ' } + + } + > + { __( 'Conditions' ) } + + } > - { element.label } - - ); - } ) } + + ) + } + onSelect={ () => + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.field + + ':' + + OPERATOR_IN ]: + filterInView?.value, + }, + ] ) + } + > + { __( 'Is' ) } + + + ) + } + onSelect={ () => + dataView.setColumnFilters( [ + ...otherFilters, + { + [ filter.field + + ':' + + OPERATOR_NOT_IN ]: + filterInView?.value, + }, + ] ) + } + > + { __( 'Is not' ) } + + + ) } + ) } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 5819b825865ef5..cc56aa15122f2c 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -15,10 +15,11 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { DataViews, ENUMERATION_TYPE, - VIEW_LAYOUTS, - OPERATOR_IN, LAYOUT_GRID, LAYOUT_TABLE, + OPERATOR_IN, + OPERATOR_NOT_IN, + VIEW_LAYOUTS, } from '@wordpress/dataviews'; /** @@ -139,6 +140,11 @@ export default function PagePages() { filter.operator === OPERATOR_IN ) { filters.author = filter.value; + } else if ( + filter.field === 'author' && + filter.operator === OPERATOR_NOT_IN + ) { + filters.author_exclude = filter.value; } } ); // We want to provide a different default item for the status filter @@ -251,6 +257,9 @@ export default function PagePages() { type: ENUMERATION_TYPE, elements: STATUSES, enableSorting: false, + filterBy: { + operators: [ OPERATOR_IN ], + }, }, { header: __( 'Date' ), From e3ef008e62dc52ada2fda9f203bcf12d18fe85de Mon Sep 17 00:00:00 2001 From: Carlos Garcia Date: Mon, 4 Dec 2023 11:56:38 +0100 Subject: [PATCH 007/325] [RNMobile] Fix HTML to blocks conversion when no transformations are available (#56723) * Add native workaround for HTML block in `htmlToBlocks` * Add raw handling tests This file is a clone of the same `blocks-raw-handling.js` file located in `gutenberg/test/integration`. The reason for the separation is that several of the test cases fail in the native version. For now, we are going to skip them, but we'd need to work on them in the future. Once all issues in tests are addressed, we'll remove this file in favor of the original one. * Update blocks raw handling test snapshot with original values * Disable more pasteHandler test cases due to not matching test snapshot * Comment obsolete snapshots of blocks raw handling tests The reason for commenting them instead of removing is that, in the future, we'll restore them once we address the failing test cases. --- .../src/api/raw-handling/html-to-blocks.js | 13 + .../blocks-raw-handling.native.js.snap | 212 +++++++ .../integration/blocks-raw-handling.native.js | 587 ++++++++++++++++++ 3 files changed, 812 insertions(+) create mode 100644 test/native/integration/__snapshots__/blocks-raw-handling.native.js.snap create mode 100644 test/native/integration/blocks-raw-handling.native.js diff --git a/packages/blocks/src/api/raw-handling/html-to-blocks.js b/packages/blocks/src/api/raw-handling/html-to-blocks.js index 18630a9abdce45..1ee2bdc263126f 100644 --- a/packages/blocks/src/api/raw-handling/html-to-blocks.js +++ b/packages/blocks/src/api/raw-handling/html-to-blocks.js @@ -1,7 +1,13 @@ +/** + * WordPress dependencies + */ +import { Platform } from '@wordpress/element'; + /** * Internal dependencies */ import { createBlock, findTransform } from '../factory'; +import parse from '../parser'; import { getBlockAttributes } from '../parser/get-block-attributes'; import { getRawTransforms } from './get-raw-transforms'; @@ -28,6 +34,13 @@ export function htmlToBlocks( html, handler ) { ); if ( ! rawTransform ) { + // Until the HTML block is supported in the native version, we'll parse it + // instead of creating the block to generate it as an unsupported block. + if ( Platform.isNative ) { + return parse( + `${ node.outerHTML }` + ); + } return createBlock( // Should not be hardcoded. 'core/html', diff --git a/test/native/integration/__snapshots__/blocks-raw-handling.native.js.snap b/test/native/integration/__snapshots__/blocks-raw-handling.native.js.snap new file mode 100644 index 00000000000000..75d8caebbe31e0 --- /dev/null +++ b/test/native/integration/__snapshots__/blocks-raw-handling.native.js.snap @@ -0,0 +1,212 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +/* +exports[`Blocks raw handling pasteHandler apple 1`] = `"This is a title


This is a heading


This is a paragraph with a link.


A
Bulleted
Indented
List


One
Two
Three


One
Two
Three
1
2
3
I
II
III


An image:
"`; + +exports[`Blocks raw handling pasteHandler classic 1`] = `"First paragraph

Second paragraph
Third paragraph
Fourth paragraph
Fifth paragraph
Sixth paragraph"`; + +exports[`Blocks raw handling pasteHandler evernote 1`] = `"This is a paragraph.


This is a link.


An
Unordered
Indented
List


One
Two
Indented
Three






One
Two
Three
Four
Five
Six
"`; + +exports[`Blocks raw handling pasteHandler google-docs 1`] = `"This is a title

This is a heading

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

A
Bulleted
Indented
List

One
Two
Three




One
Two
Three
1
2
3
I
II
III





An image:


"`; + +exports[`Blocks raw handling pasteHandler google-docs-list-only 1`] = `"My first list item
A sub list item
A second sub list item
My second list item
My third list item"`; + +exports[`Blocks raw handling pasteHandler google-docs-table 1`] = `"



One
Two
Three
1
2
3
I
II
III"`; + +exports[`Blocks raw handling pasteHandler google-docs-table-with-colspan 1`] = `"

Test colspan

"`; + +exports[`Blocks raw handling pasteHandler google-docs-table-with-comments 1`] = `"



One
Two
Three
1
2
3
I
II
III"`; + +exports[`Blocks raw handling pasteHandler google-docs-table-with-rowspan 1`] = `"

Test rowspan

"`; + +exports[`Blocks raw handling pasteHandler google-docs-with-comments 1`] = `"This is a title

This is a heading

Formatting test: bold, italic, link, strikethrough, superscript, subscript, nested.

A
Bulleted
Indented
List

One
Two
Three




One
Two
Three
1
2
3
I
II
III





An image:



"`; +*/ + +exports[`Blocks raw handling pasteHandler gutenberg 1`] = `"Test"`; + +exports[`Blocks raw handling pasteHandler iframe-embed 1`] = `""`; + +/* +exports[`Blocks raw handling pasteHandler markdown 1`] = `"This is a heading with italic
This is a paragraph with a link, bold, and strikethrough.
Preserve
line breaks please.
Lists
A
Bulleted Indented
List
One
Two
Three
Table
First Header
Second Header
Content from cell 1
Content from cell 2
Content in the first column
Content in the second column



Table with empty cells.
Quote
First
Second
Code
Inline code tags should work.
This is a code block."`; + +exports[`Blocks raw handling pasteHandler ms-word 1`] = `"This is a title
 
This is a subtitle
 
This is a heading level 1
 
This is a heading level 2
 
This is a paragraph with a link.
 
A
Bulleted
Indented
List
 
One
Two
Three
 
One
Two
Three
1
2
3
I
II
III
 
An image:
 

This is an anchor link that leads to the next paragraph.
This is the paragraph with the anchor.
This is an anchor link that leads nowhere.
This is a paragraph with an anchor with no link pointing to it.
This is a reference to a footnote[1].
This is a reference to an endnote[i].


[1] This is a footnote.


[i] This is an endnote."`; + +exports[`Blocks raw handling pasteHandler ms-word-list 1`] = `"This is a headline?
This is a text:
One
Two
Three
Lorem Ipsum.
 "`; + +exports[`Blocks raw handling pasteHandler ms-word-online 1`] = `"This is a heading 
This is a paragraph with a link

Bulleted 
Indented 
List 
 
One 
Two 
Three 

One 
Two 
Three 




II 
III 
 
An image: 
 "`; + +exports[`Blocks raw handling pasteHandler ms-word-styled 1`] = `"
Lorem ipsum dolor sit amet, consectetur adipiscing elit 


Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque aliquet hendrerit auctor. Nam lobortis, est vel lacinia tincidunt, purus tellus vehicula ex, nec pharetra justo dui sed lorem. Nam congue laoreet massa, quis varius est tincidunt ut."`; + +exports[`Blocks raw handling pasteHandler nested-divs 1`] = `"First paragraph

Second paragraph
Third paragraph
Fourth paragraph
Fifth paragraph
Sixth paragraph"`; + +exports[`Blocks raw handling pasteHandler one-image 1`] = `""`; + +exports[`Blocks raw handling pasteHandler plain 1`] = `"test
test

test"`; + +exports[`Blocks raw handling pasteHandler shortcode-matching 1`] = `"[gallery ids="40,41,42"]
[gallery ids="1000"]
[gallery ids="42"]"`; +*/ + +exports[`Blocks raw handling pasteHandler should remove extra blank lines 1`] = ` +" +

1

+ + + +

2

+" +`; + +exports[`Blocks raw handling pasteHandler should strip HTML formatting space from inline text 1`] = `"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent a elit eget tortor molestie egestas. Donec pretium urna vitae mattis imperdiet. Praesent et lorem iaculis, volutpat odio vitae, ornare lacus. Donec ut felis tristique, pharetra erat id, viverra justo. Integer sit amet elementum arcu, eget pharetra felis. In malesuada enim est, sed placerat nulla feugiat at. Vestibulum feugiat vitae elit sit amet tincidunt. Pellentesque finibus sed dolor non facilisis. Curabitur accumsan ante ac hendrerit vestibulum."`; + +exports[`Blocks raw handling pasteHandler should strip some text-level elements 1`] = ` +" +

This is ncorect

+" +`; + +exports[`Blocks raw handling pasteHandler should strip windows data 1`] = ` +" +

Heading Win

+ + + +

Paragraph Win

+" +`; + +/* +exports[`Blocks raw handling pasteHandler slack-paragraphs 1`] = `"test with link
a new line

a new paragraph
another new line

another paragraph"`; + +exports[`Blocks raw handling pasteHandler slack-quote 1`] = `"Test with link."`; + +exports[`Blocks raw handling pasteHandler two-images 1`] = `" "`; + +exports[`Blocks raw handling pasteHandler wordpress 1`] = `"Howdy
This is a paragraph.
More tag

Shortcode
[gallery ids="1"]

test
test"`; +*/ + +exports[`Blocks raw handling should correctly handle quotes with mixed content 1`] = ` +" +
+

chicken

+ + + +

ribs

+
+" +`; + +exports[`rawHandler should convert HTML post to blocks with minimal content changes 1`] = ` +" +

Howdy

+ + + +
+ + + +

This is a paragraph.

+ + + +

Preserve me!

+ + + +

More tag

+ + + + + + + +

Shortcode

+ + + + + + + +
+
Term
+
+ Description. +
+
+ + + +
    +
  1. Item
  2. +
+ + + +
+

Text.

+
+ + + +
+

Heading

+ + + +

Text.

+
+" +`; + +exports[`rawHandler should convert a caption shortcode 1`] = ` +" +
test
+" +`; + +exports[`rawHandler should convert a caption shortcode with caption 1`] = ` +" +
test
+" +`; + +exports[`rawHandler should convert a caption shortcode with link 1`] = ` +" +
Bell on Wharf
Bell on wharf in San Francisco
+" +`; + +exports[`rawHandler should convert a list with attributes 1`] = ` +" +
    +
  1. 1 +
      +
    1. 1
    2. +
    +
  2. +
+" +`; + +exports[`rawHandler should convert to unsupported HTML block when no transformation is available 1`] = ` +" +

Hello world!

+" +`; + +exports[`rawHandler should not strip any text-level elements 1`] = ` +" +

This is ncorect

+" +`; + +exports[`rawHandler should preserve alignment 1`] = ` +" +

center

+" +`; diff --git a/test/native/integration/blocks-raw-handling.native.js b/test/native/integration/blocks-raw-handling.native.js new file mode 100644 index 00000000000000..5f21ca035fbf9d --- /dev/null +++ b/test/native/integration/blocks-raw-handling.native.js @@ -0,0 +1,587 @@ +/** + * External dependencies + */ +import fs from 'fs'; +import path from 'path'; + +/** + * WordPress dependencies + */ +import { + createBlock, + getBlockContent, + pasteHandler, + rawHandler, + registerBlockType, + serialize, +} from '@wordpress/blocks'; +import { registerCoreBlocks } from '@wordpress/block-library'; + +function readFile( filePath ) { + return fs.existsSync( filePath ) + ? fs.readFileSync( filePath, 'utf8' ).trim() + : ''; +} + +// Path to the fixtures provided in `gutenberg/test/integration`. +const fixturesPath = `${ __dirname }/../../integration`; + +// NOTE: This file is a clone of the same `blocks-raw-handling.js` file located in +// `gutenberg/test/integration`. The reason for the separation is that several of +// the test cases fail in the native version. For now, we are going to skip them, but +// we'd need to work on them in the future. +// +// Once all issues in tests are addressed, we'll remove this file in favor of the +// original one. +describe( 'Blocks raw handling', () => { + beforeAll( () => { + // Load all hooks that modify blocks. + require( '../../../packages/editor/src/hooks' ); + registerCoreBlocks(); + registerBlockType( 'test/gallery', { + title: 'Test Gallery', + category: 'text', + attributes: { + ids: { + type: 'array', + default: [], + }, + }, + transforms: { + from: [ + { + type: 'shortcode', + tag: 'gallery', + isMatch( { named: { ids } } ) { + return ids.indexOf( 42 ) > -1; + }, + attributes: { + ids: { + type: 'array', + shortcode: ( { named: { ids } } ) => + ids + .split( ',' ) + .map( ( id ) => parseInt( id, 10 ) ), + }, + }, + priority: 9, + }, + ], + }, + save: () => null, + } ); + + registerBlockType( 'test/non-inline-block', { + title: 'Test Non Inline Block', + category: 'text', + supports: { + pasteTextInline: false, + }, + transforms: { + from: [ + { + type: 'raw', + isMatch: ( node ) => { + return ( + 'words to live by' === node.textContent.trim() + ); + }, + transform: () => { + return createBlock( 'core/embed', { + url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + } ); + }, + }, + ], + }, + save: () => null, + } ); + + registerBlockType( 'test/transform-to-multiple-blocks', { + title: 'Test Transform to Multiple Blocks', + category: 'text', + transforms: { + from: [ + { + type: 'raw', + isMatch: ( node ) => { + return node.textContent + .split( ' ' ) + .every( ( chunk ) => /^P\S+?/.test( chunk ) ); + }, + transform: ( node ) => { + return node.textContent + .split( ' ' ) + .map( ( chunk ) => + createBlock( 'core/paragraph', { + content: chunk.substring( 1 ), + } ) + ); + }, + }, + ], + }, + save: () => null, + } ); + } ); + + it( 'should filter inline content', () => { + const filtered = pasteHandler( { + HTML: '

test

', + mode: 'INLINE', + } ); + + expect( filtered ).toBe( 'test' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should ignore Google Docs UID tag', () => { + const filtered = pasteHandler( { + HTML: 'test', + mode: 'AUTO', + } ); + + expect( filtered ).toBe( 'test' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should ignore Google Docs UID tag in inline mode', () => { + const filtered = pasteHandler( { + HTML: 'test', + mode: 'INLINE', + } ); + + expect( filtered ).toBe( 'test' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should paste special whitespace', () => { + const filtered = pasteHandler( { + HTML: '

', + plainText: ' ', + mode: 'AUTO', + } ); + + expect( console ).toHaveLogged(); + expect( filtered ).toBe( ' ' ); + } ); + + it( 'should paste special whitespace in plain text only', () => { + const filtered = pasteHandler( { + HTML: '', + plainText: ' ', + mode: 'AUTO', + } ); + + expect( console ).toHaveLogged(); + expect( filtered ).toBe( ' ' ); + } ); + + it( 'should parse Markdown', () => { + const filtered = pasteHandler( { + HTML: '* one
* two
* three', + plainText: '* one\n* two\n* three', + mode: 'AUTO', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( filtered ).toMatchInlineSnapshot( ` + "
    +
  • one
  • + + + +
  • two
  • + + + +
  • three
  • +
" + ` ); + expect( console ).toHaveLogged(); + } ); + + it( 'should parse bulleted list', () => { + const filtered = pasteHandler( { + HTML: '• one
• two
• three', + plainText: '• one\n• two\n• three', + mode: 'AUTO', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( filtered ).toMatchInlineSnapshot( ` + "
    +
  • one
  • + + + +
  • two
  • + + + +
  • three
  • +
" + ` ); + expect( console ).toHaveLogged(); + } ); + + it( 'should parse inline Markdown', () => { + const filtered = pasteHandler( { + HTML: 'Some **bold** text.', + plainText: 'Some **bold** text.', + mode: 'AUTO', + } ); + + expect( filtered ).toBe( 'Some bold text.' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should parse HTML in plainText', () => { + const filtered = pasteHandler( { + HTML: '<p>Some <strong>bold</strong> text.</p>', + plainText: '

Some bold text.

', + mode: 'AUTO', + } ); + + expect( filtered ).toBe( 'Some bold text.' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should parse Markdown with HTML', () => { + const filtered = pasteHandler( { + HTML: '', + plainText: '# Some heading\n\nA paragraph.', + mode: 'AUTO', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( filtered ).toBe( + '

Some heading

A paragraph.

' + ); + expect( console ).toHaveLogged(); + } ); + + it.skip( 'should break up forced inline content', () => { + const filtered = pasteHandler( { + HTML: '

test

test

', + mode: 'INLINE', + } ); + + expect( filtered ).toBe( 'test
test' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should normalize decomposed characters', () => { + const filtered = pasteHandler( { + HTML: 'schön', + mode: 'INLINE', + } ); + + expect( filtered ).toBe( 'schön' ); + expect( console ).toHaveLogged(); + } ); + + it.skip( 'should not treat single non-inlineable block as inline text', () => { + const filtered = pasteHandler( { + HTML: '

words to live by

', + plainText: 'words to live by\n', + mode: 'AUTO', + } ); + + expect( filtered ).toHaveLength( 1 ); + expect( filtered[ 0 ].name ).toBe( 'core/embed' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should treat single heading as inline text', () => { + const filtered = pasteHandler( { + HTML: '

FOO

', + plainText: 'FOO\n', + mode: 'AUTO', + } ); + + expect( filtered ).toBe( 'FOO' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should treat single list item as inline text', () => { + const filtered = pasteHandler( { + HTML: '
  • Some bold text.
', + plainText: 'Some bold text.\n', + mode: 'AUTO', + } ); + + expect( filtered ).toBe( 'Some bold text.' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should treat multiple list items as a block', () => { + const filtered = pasteHandler( { + HTML: '
  • One
  • Two
  • Three
', + plainText: 'One\nTwo\nThree\n', + mode: 'AUTO', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( filtered ).toMatchInlineSnapshot( ` + "
    +
  • One
  • + + + +
  • Two
  • + + + +
  • Three
  • +
" + ` ); + expect( console ).toHaveLogged(); + } ); + + it( 'should correctly handle quotes with mixed content', () => { + const filtered = serialize( + pasteHandler( { + HTML: '

chicken

ribs

', + mode: 'AUTO', + } ) + ); + + expect( filtered ).toMatchSnapshot(); + expect( console ).toHaveLogged(); + } ); + + it( 'should paste gutenberg content from plain text', () => { + const block = ''; + expect( + serialize( + pasteHandler( { + plainText: block, + mode: 'AUTO', + } ) + ) + ).toBe( block ); + } ); + + it.skip( 'should handle transforms that return an array of blocks', () => { + const transformed = pasteHandler( { + HTML: '

P1 P2

', + plainText: 'P1 P2\n', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( transformed ).toBe( '

1

2

' ); + expect( console ).toHaveLogged(); + } ); + + it( 'should convert pre', () => { + const transformed = pasteHandler( { + HTML: '
1\n2
', + plainText: '1\n2', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( transformed ).toBe( + '
1\n2
' + ); + expect( console ).toHaveLogged(); + } ); + + it( 'should convert code', () => { + const transformed = pasteHandler( { + HTML: '
1\n2
', + plainText: '1\n2', + } ) + .map( getBlockContent ) + .join( '' ); + + expect( transformed ).toBe( + '
1\n2
' + ); + expect( console ).toHaveLogged(); + } ); + + describe( 'pasteHandler', () => { + // TODO: The cases commented should be eventually addressed and restored. + [ + // 'plain', + // 'classic', + // 'nested-divs', + // 'apple', + // 'google-docs', + // 'google-docs-list-only', + // 'google-docs-table', + // 'google-docs-table-with-colspan', + // 'google-docs-table-with-rowspan', + // 'google-docs-table-with-comments', + // 'google-docs-with-comments', + // 'ms-word', + // 'ms-word-list', + // 'ms-word-styled', + // 'ms-word-online', + // 'evernote', + 'iframe-embed', + // 'one-image', + // 'two-images', + // 'markdown', + // 'wordpress', + 'gutenberg', + // 'shortcode-matching', + // 'slack-quote', + // 'slack-paragraphs', + ].forEach( ( type ) => { + // eslint-disable-next-line jest/valid-title + it( type, () => { + const HTML = readFile( + path.join( + fixturesPath, + `fixtures/documents/${ type }-in.html` + ) + ); + const plainText = readFile( + path.join( + fixturesPath, + `fixtures/documents/${ type }-in.txt` + ) + ); + const output = readFile( + path.join( + fixturesPath, + `fixtures/documents/${ type }-out.html` + ) + ); + + if ( ! ( HTML || plainText ) || ! output ) { + throw new Error( `Expected fixtures for type ${ type }` ); + } + + const converted = pasteHandler( { HTML, plainText } ); + const serialized = + typeof converted === 'string' + ? converted + : serialize( converted ); + + expect( serialized ).toBe( output ); + + const convertedInline = pasteHandler( { + HTML, + plainText, + mode: 'INLINE', + } ); + + expect( convertedInline ).toMatchSnapshot(); + expect( console ).toHaveLogged(); + } ); + } ); + + it( 'should strip some text-level elements', () => { + const HTML = '

This is ncorect

'; + expect( serialize( pasteHandler( { HTML } ) ) ).toMatchSnapshot(); + expect( console ).toHaveLogged(); + } ); + + it( 'should remove extra blank lines', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/google-docs-blank-lines.html' + ) + ); + expect( serialize( pasteHandler( { HTML } ) ) ).toMatchSnapshot(); + expect( console ).toHaveLogged(); + } ); + + it( 'should strip windows data', () => { + const HTML = readFile( + path.join( fixturesPath, 'fixtures/documents/windows.html' ) + ); + expect( serialize( pasteHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should strip HTML formatting space from inline text', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/inline-with-html-formatting-space.html' + ) + ); + expect( pasteHandler( { HTML } ) ).toMatchSnapshot(); + expect( console ).toHaveLogged(); + } ); + } ); +} ); + +describe( 'rawHandler', () => { + it.skip( 'should convert HTML post to blocks with minimal content changes', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/wordpress-convert.html' + ) + ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should convert a caption shortcode', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/shortcode-caption.html' + ) + ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should convert a caption shortcode with link', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/shortcode-caption-with-link.html' + ) + ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should convert a caption shortcode with caption', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/shortcode-caption-with-caption-link.html' + ) + ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should convert a list with attributes', () => { + const HTML = readFile( + path.join( + fixturesPath, + 'fixtures/documents/list-with-attributes.html' + ) + ); + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should not strip any text-level elements', () => { + const HTML = '

This is ncorect

'; + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + it.skip( 'should preserve alignment', () => { + const HTML = '

center

'; + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); + + // This is an extra test added to cover the case fixed in: + // `rnmobile/fix/div-tag-convert-to-blocks`. + it( 'should convert to unsupported HTML block when no transformation is available', () => { + const HTML = '

Hello world!

'; + expect( serialize( rawHandler( { HTML } ) ) ).toMatchSnapshot(); + } ); +} ); From 857b8b895e110cc4b87f7f4c033efec8f314a168 Mon Sep 17 00:00:00 2001 From: Rich Tabor Date: Mon, 4 Dec 2023 06:49:05 -0500 Subject: [PATCH 008/325] Update external images panel in post publish sidebar (#55524) * Update view * Fix test --- .../post-publish-panel/maybe-upload-media.js | 11 +++-------- test/e2e/specs/editor/blocks/image.spec.js | 4 +++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js index 0097b3f0ea7419..f432b0da16c359 100644 --- a/packages/editor/src/components/post-publish-panel/maybe-upload-media.js +++ b/packages/editor/src/components/post-publish-panel/maybe-upload-media.js @@ -10,7 +10,6 @@ import { } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; -import { upload } from '@wordpress/icons'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useState } from '@wordpress/element'; import { isBlobURL } from '@wordpress/blob'; @@ -135,7 +134,7 @@ export default function PostFormatPanel() {

{ __( - 'There are some external images in the post which can be uploaded to the media library. Images coming from different domains may not always display correctly, load slowly for visitors, or be removed unexpectedly.' + 'Upload external images to the Media Library. Images from different domains may load slowly, display incorrectly, or be removed unexpectedly.' ) }

) : ( - ) }
diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index 85590e9ec15964..f1041fa60061a7 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -729,7 +729,9 @@ test.describe( 'Image', () => { await page .getByRole( 'button', { name: 'Publish', exact: true } ) .click(); - await page.getByRole( 'button', { name: 'Upload all' } ).click(); + await page + .getByRole( 'button', { name: 'Upload', exact: true } ) + .click(); await expect( page.locator( '.components-spinner' ) ).toHaveCount( 0 ); From 8c4f4357ce60c97d657a250c2f078465706552fd Mon Sep 17 00:00:00 2001 From: Luis Herranz Date: Mon, 4 Dec 2023 13:30:37 +0100 Subject: [PATCH 009/325] Load the import map polyfill only when there is an import map (#56699) * Load polyfill only when there is an import map * Remove the polyfill to check the performance impact * Revert "Remove the polyfill to check the performance impact" This reverts commit af5eaad64acac068e6436f2367310edba9523145. --- .../interactivity-api/modules.php | 12 ---------- .../modules/class-gutenberg-modules.php | 23 +++++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/lib/experimental/interactivity-api/modules.php b/lib/experimental/interactivity-api/modules.php index 02785a152ca1fa..0695da26f4b1b3 100644 --- a/lib/experimental/interactivity-api/modules.php +++ b/lib/experimental/interactivity-api/modules.php @@ -16,18 +16,6 @@ function gutenberg_register_interactivity_module() { array(), defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : get_bloginfo( 'version' ) ); - - // TODO: Replace with a simpler version that only provides support for import maps. - // TODO: Load only if the browser doesn't support import maps (https://github.com/guybedford/es-module-shims/issues/371). - wp_enqueue_script( - 'es-module-shims', - gutenberg_url( '/build/modules/importmap-polyfill.min.js' ), - array(), - null, - array( - 'strategy' => 'defer', - ) - ); } add_action( 'wp_enqueue_scripts', 'gutenberg_register_interactivity_module' ); diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php index ca74d863043ee6..5f847fa8c897ad 100644 --- a/lib/experimental/modules/class-gutenberg-modules.php +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -114,6 +114,26 @@ public static function print_module_preloads() { } } + /** + * Prints the necessary script to load import map polyfill for browsers that + * do not support import maps. + * + * TODO: Replace the polyfill with a simpler version that only provides + * support for import maps and load it only when the browser doesn't support + * import maps (https://github.com/guybedford/es-module-shims/issues/371). + */ + public static function print_import_map_polyfill() { + $import_map = self::get_import_map(); + if ( ! empty( $import_map['imports'] ) ) { + wp_print_script_tag( + array( + 'src' => gutenberg_url( '/build/modules/importmap-polyfill.min.js' ), + 'defer' => true, + ) + ); + } + } + /** * Gets the module's version. It either returns a timestamp (if SCRIPT_DEBUG * is true), the explicit version of the module if it is set and not false, or @@ -193,3 +213,6 @@ function gutenberg_enqueue_module( $module_identifier ) { // Prints the preloaded modules in the head tag. add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_module_preloads' ) ); + +// Prints the script that loads the import map polyfill in the footer. +add_action( 'wp_footer', array( 'Gutenberg_Modules', 'print_import_map_polyfill' ), 11 ); From 18b5ab3245c5f2848aff73f19cdd5a802862d6ba Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 4 Dec 2023 15:33:14 +0200 Subject: [PATCH 010/325] Performance: measure typing without inspector (#56753) * Performance: measure typing without inspector * Remove min/max --- .../config/performance-reporter.ts | 3 + test/performance/specs/post-editor.spec.js | 116 +++++++++--------- 2 files changed, 62 insertions(+), 57 deletions(-) diff --git a/test/performance/config/performance-reporter.ts b/test/performance/config/performance-reporter.ts index fa7cc90825c220..7b1f171230c59e 100644 --- a/test/performance/config/performance-reporter.ts +++ b/test/performance/config/performance-reporter.ts @@ -26,6 +26,7 @@ export interface WPRawPerformanceResults { firstContentfulPaint: number[]; firstBlock: number[]; type: number[]; + typeWithoutInspector: number[]; typeContainer: number[]; focus: number[]; inserterOpen: number[]; @@ -48,6 +49,7 @@ export interface WPPerformanceResults { type?: number; minType?: number; maxType?: number; + typeWithoutInspector?: number; typeContainer?: number; minTypeContainer?: number; maxTypeContainer?: number; @@ -92,6 +94,7 @@ export function curateResults( type: average( results.type ), minType: minimum( results.type ), maxType: maximum( results.type ), + typeWithoutInspector: average( results.typeWithoutInspector ), typeContainer: average( results.typeContainer ), minTypeContainer: minimum( results.typeContainer ), maxTypeContainer: maximum( results.typeContainer ), diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index d5ff40570afd78..554b0dc71283e6 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -22,6 +22,7 @@ const results = { firstContentfulPaint: [], firstBlock: [], type: [], + typeWithoutInspector: [], typeContainer: [], focus: [], listViewOpen: [], @@ -91,6 +92,40 @@ test.describe( 'Post Editor Performance', () => { } } ); + async function type( target, metrics, key ) { + // The first character typed triggers a longer time (isTyping change). + // It can impact the stability of the metric, so we exclude it. It + // probably deserves a dedicated metric itself, though. + const samples = 10; + const throwaway = 1; + const iterations = samples + throwaway; + + // Start tracing. + await metrics.startTracing(); + + // Type the testing sequence into the empty paragraph. + await target.type( 'x'.repeat( iterations ), { + delay: BROWSER_IDLE_WAIT, + // The extended timeout is needed because the typing is very slow + // and the `delay` value itself does not extend it. + timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. + } ); + + // Stop tracing. + await metrics.stopTracing(); + + // Get the durations. + const [ keyDownEvents, keyPressEvents, keyUpEvents ] = + metrics.getTypingEventDurations(); + + // Save the results. + for ( let i = throwaway; i < iterations; i++ ) { + results[ key ].push( + keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] + ); + } + } + test.describe( 'Typing', () => { let draftId = null; @@ -110,37 +145,34 @@ test.describe( 'Post Editor Performance', () => { name: /Empty block/i, } ); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const iterations = samples + throwaway; + await type( paragraph, metrics, 'type' ); + } ); + } ); - // Start tracing. - await metrics.startTracing(); + test.describe( 'Typing (without inspector)', () => { + let draftId = null; - // Type the testing sequence into the empty paragraph. - await paragraph.type( 'x'.repeat( iterations ), { - delay: BROWSER_IDLE_WAIT, - // The extended timeout is needed because the typing is very slow - // and the `delay` value itself does not extend it. - timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. - } ); + test( 'Setup the test post', async ( { admin, perfUtils, editor } ) => { + await admin.createNewPost(); + await perfUtils.loadBlocksForLargePost(); + await editor.insertBlock( { name: 'core/paragraph' } ); + draftId = await perfUtils.saveDraft(); + } ); - // Stop tracing. - await metrics.stopTracing(); + test( 'Run the test', async ( { admin, perfUtils, metrics, page } ) => { + await admin.editPost( draftId ); + await perfUtils.disableAutosave(); + const toggleButton = page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByRole( 'button', { name: 'Close Settings' } ); + await toggleButton.click(); + const canvas = await perfUtils.getCanvas(); - // Get the durations. - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations(); + const paragraph = canvas.getByRole( 'document', { + name: /Empty block/i, + } ); - // Save the results. - for ( let i = throwaway; i < iterations; i++ ) { - results.type.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + await type( paragraph, metrics, 'typeWithoutInspector' ); } ); } ); @@ -166,37 +198,7 @@ test.describe( 'Post Editor Performance', () => { .first(); await firstParagraph.click(); - // The first character typed triggers a longer time (isTyping change). - // It can impact the stability of the metric, so we exclude it. It - // probably deserves a dedicated metric itself, though. - const samples = 10; - const throwaway = 1; - const iterations = samples + throwaway; - - // Start tracing. - await metrics.startTracing(); - - // Start typing in the middle of the text. - await firstParagraph.type( 'x'.repeat( iterations ), { - delay: BROWSER_IDLE_WAIT, - // The extended timeout is needed because the typing is very slow - // and the `delay` value itself does not extend it. - timeout: iterations * BROWSER_IDLE_WAIT * 2, // 2x the total time to be safe. - } ); - - // Stop tracing. - await metrics.stopTracing(); - - // Get the durations. - const [ keyDownEvents, keyPressEvents, keyUpEvents ] = - metrics.getTypingEventDurations(); - - // Save the results. - for ( let i = throwaway; i < iterations; i++ ) { - results.typeContainer.push( - keyDownEvents[ i ] + keyPressEvents[ i ] + keyUpEvents[ i ] - ); - } + await type( firstParagraph, metrics, 'typeContainer' ); } ); } ); From 088223afd333013e2862b8f8f8f006d2f08c81e3 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Mon, 4 Dec 2023 16:42:59 +0200 Subject: [PATCH 011/325] Move `useDebouncedInput` hook to @wordpress/compose package (#56744) --- .../inserter/media-tab/media-panel.js | 2 +- .../src/components/inserter/menu.js | 2 +- packages/compose/README.md | 12 ++++++++++++ .../src/hooks/use-debounced-input/index.js} | 12 +++++++++++- packages/compose/src/index.js | 1 + packages/compose/src/index.native.js | 1 + packages/dataviews/src/search.js | 6 +----- .../dataviews/src/utils/use-debounced-input.js | 18 ------------------ .../add-custom-template-modal-content.js | 2 +- .../components/page-patterns/patterns-list.js | 7 +++++-- .../edit-site/src/utils/use-debounced-input.js | 18 ------------------ 11 files changed, 34 insertions(+), 47 deletions(-) rename packages/{block-editor/src/components/inserter/hooks/use-debounced-input.js => compose/src/hooks/use-debounced-input/index.js} (59%) delete mode 100644 packages/dataviews/src/utils/use-debounced-input.js delete mode 100644 packages/edit-site/src/utils/use-debounced-input.js diff --git a/packages/block-editor/src/components/inserter/media-tab/media-panel.js b/packages/block-editor/src/components/inserter/media-tab/media-panel.js index 58ae7c49d27628..49601c9c8ad1bc 100644 --- a/packages/block-editor/src/components/inserter/media-tab/media-panel.js +++ b/packages/block-editor/src/components/inserter/media-tab/media-panel.js @@ -5,12 +5,12 @@ import { useRef, useEffect } from '@wordpress/element'; import { Spinner, SearchControl } from '@wordpress/components'; import { focus } from '@wordpress/dom'; import { __ } from '@wordpress/i18n'; +import { useDebouncedInput } from '@wordpress/compose'; /** * Internal dependencies */ import MediaList from './media-list'; -import useDebouncedInput from '../hooks/use-debounced-input'; import { useMediaResults } from './hooks'; import InserterNoResults from '../no-results'; diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index e6c32d1273be34..a6d752848538e7 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -17,6 +17,7 @@ import { import { VisuallyHidden, SearchControl, Popover } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; +import { useDebouncedInput } from '@wordpress/compose'; /** * Internal dependencies @@ -28,7 +29,6 @@ import BlockPatternsTab from './block-patterns-tab'; import { PatternCategoryPreviewPanel } from './block-patterns-tab/pattern-category-preview-panel'; import { MediaTab, MediaCategoryDialog, useMediaCategories } from './media-tab'; import InserterSearchResults from './search-results'; -import useDebouncedInput from './hooks/use-debounced-input'; import useInsertionPoint from './hooks/use-insertion-point'; import InserterTabs from './tabs'; import { store as blockEditorStore } from '../../store'; diff --git a/packages/compose/README.md b/packages/compose/README.md index 5a3ec6437b1fdc..ce393f2b5fd18c 100644 --- a/packages/compose/README.md +++ b/packages/compose/README.md @@ -249,6 +249,18 @@ _Returns_ - `import('../../utils/debounce').DebouncedFunc`: Debounced function. +### useDebouncedInput + +Helper hook for input fields that need to debounce the value before using it. + +_Parameters_ + +- _defaultValue_ `any`: The default value to use. + +_Returns_ + +- `[string, Function, string]`: The input value, the setter and the debounced input value. + ### useDisabled In some circumstances, such as block previews, all focusable DOM elements (input fields, links, buttons, etc.) need to be disabled. This hook adds the behavior to disable nested DOM elements to the returned ref. diff --git a/packages/block-editor/src/components/inserter/hooks/use-debounced-input.js b/packages/compose/src/hooks/use-debounced-input/index.js similarity index 59% rename from packages/block-editor/src/components/inserter/hooks/use-debounced-input.js rename to packages/compose/src/hooks/use-debounced-input/index.js index 26cd6c0da0e0a9..91a01073fcfee8 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-debounced-input.js +++ b/packages/compose/src/hooks/use-debounced-input/index.js @@ -2,8 +2,18 @@ * WordPress dependencies */ import { useEffect, useState } from '@wordpress/element'; -import { useDebounce } from '@wordpress/compose'; +/** + * Internal dependencies + */ +import useDebounce from '../use-debounce'; + +/** + * Helper hook for input fields that need to debounce the value before using it. + * + * @param {any} defaultValue The default value to use. + * @return {[string, Function, string]} The input value, the setter and the debounced input value. + */ export default function useDebouncedInput( defaultValue = '' ) { const [ input, setInput ] = useState( defaultValue ); const [ debouncedInput, setDebouncedState ] = useState( defaultValue ); diff --git a/packages/compose/src/index.js b/packages/compose/src/index.js index 1a667c98cb6905..3d03463f490794 100644 --- a/packages/compose/src/index.js +++ b/packages/compose/src/index.js @@ -39,6 +39,7 @@ export { default as useResizeObserver } from './hooks/use-resize-observer'; export { default as useAsyncList } from './hooks/use-async-list'; export { default as useWarnOnChange } from './hooks/use-warn-on-change'; export { default as useDebounce } from './hooks/use-debounce'; +export { default as useDebouncedInput } from './hooks/use-debounced-input'; export { default as useThrottle } from './hooks/use-throttle'; export { default as useMergeRefs } from './hooks/use-merge-refs'; export { default as useRefEffect } from './hooks/use-ref-effect'; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index 0b9a41679f83a9..a3f959e644f32a 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -33,6 +33,7 @@ export { default as usePreferredColorScheme } from './hooks/use-preferred-color- export { default as usePreferredColorSchemeStyle } from './hooks/use-preferred-color-scheme-style'; export { default as useResizeObserver } from './hooks/use-resize-observer'; export { default as useDebounce } from './hooks/use-debounce'; +export { default as useDebouncedInput } from './hooks/use-debounced-input'; export { default as useThrottle } from './hooks/use-throttle'; export { default as useMergeRefs } from './hooks/use-merge-refs'; export { default as useRefEffect } from './hooks/use-ref-effect'; diff --git a/packages/dataviews/src/search.js b/packages/dataviews/src/search.js index b226ddbffd35e4..2e58b721d6e2eb 100644 --- a/packages/dataviews/src/search.js +++ b/packages/dataviews/src/search.js @@ -4,11 +4,7 @@ import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; import { SearchControl } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import useDebouncedInput from './utils/use-debounced-input'; +import { useDebouncedInput } from '@wordpress/compose'; export default function Search( { label, view, onChangeView } ) { const [ search, setSearch, debouncedSearch ] = useDebouncedInput( diff --git a/packages/dataviews/src/utils/use-debounced-input.js b/packages/dataviews/src/utils/use-debounced-input.js deleted file mode 100644 index 26cd6c0da0e0a9..00000000000000 --- a/packages/dataviews/src/utils/use-debounced-input.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEffect, useState } from '@wordpress/element'; -import { useDebounce } from '@wordpress/compose'; - -export default function useDebouncedInput( defaultValue = '' ) { - const [ input, setInput ] = useState( defaultValue ); - const [ debouncedInput, setDebouncedState ] = useState( defaultValue ); - - const setDebouncedInput = useDebounce( setDebouncedState, 250 ); - - useEffect( () => { - setDebouncedInput( input ); - }, [ input ] ); - - return [ input, setInput, debouncedInput ]; -} diff --git a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js index 643f1e6f818662..44554eac0dcd62 100644 --- a/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js +++ b/packages/edit-site/src/components/add-new-template/add-custom-template-modal-content.js @@ -15,12 +15,12 @@ import { } from '@wordpress/components'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { useDebouncedInput } from '@wordpress/compose'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; -import useDebouncedInput from '../../utils/use-debounced-input'; import { mapToIHasNameAndId } from './utils'; const { diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index eb56fdded90607..6015fbbf4caf34 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -15,7 +15,11 @@ import { import { __, _x, isRTL } from '@wordpress/i18n'; import { chevronLeft, chevronRight } from '@wordpress/icons'; import { privateApis as routerPrivateApis } from '@wordpress/router'; -import { useAsyncList, useViewportMatch } from '@wordpress/compose'; +import { + useAsyncList, + useViewportMatch, + useDebouncedInput, +} from '@wordpress/compose'; /** * Internal dependencies @@ -25,7 +29,6 @@ import Grid from './grid'; import NoPatterns from './no-patterns'; import usePatterns from './use-patterns'; import SidebarButton from '../sidebar-button'; -import useDebouncedInput from '../../utils/use-debounced-input'; import { unlock } from '../../lock-unlock'; import { PATTERN_SYNC_TYPES, PATTERN_TYPES } from '../../utils/constants'; import Pagination from './pagination'; diff --git a/packages/edit-site/src/utils/use-debounced-input.js b/packages/edit-site/src/utils/use-debounced-input.js deleted file mode 100644 index 26cd6c0da0e0a9..00000000000000 --- a/packages/edit-site/src/utils/use-debounced-input.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * WordPress dependencies - */ -import { useEffect, useState } from '@wordpress/element'; -import { useDebounce } from '@wordpress/compose'; - -export default function useDebouncedInput( defaultValue = '' ) { - const [ input, setInput ] = useState( defaultValue ); - const [ debouncedInput, setDebouncedState ] = useState( defaultValue ); - - const setDebouncedInput = useDebounce( setDebouncedState, 250 ); - - useEffect( () => { - setDebouncedInput( input ); - }, [ input ] ); - - return [ input, setInput, debouncedInput ]; -} From 3d389c4a1e5689aa99242e1edac0153798b9d25e Mon Sep 17 00:00:00 2001 From: Alex Stine Date: Mon, 4 Dec 2023 09:03:41 -0600 Subject: [PATCH 012/325] Site editor: Shorter screen reader announcement after changing pages (#56339) * Shorter title announcement. * Reviewer feedback. * Reviewer feedback. * Improve translators comments. --------- Co-authored-by: Andrea Fercia --- packages/edit-site/src/components/editor/index.js | 4 ++-- .../edit-site/src/components/routes/use-title.js | 13 +++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index ce12a2aa5fa6b7..1d3fca36f5f4c0 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -169,8 +169,8 @@ export default function Editor( { listViewToggleElement, isLoading } ) { let title; if ( hasLoadedPost ) { title = sprintf( - // translators: A breadcrumb trail in browser tab. %1$s: title of template being edited, %2$s: type of template (Template or Template Part). - __( '%1$s ‹ %2$s ‹ Editor' ), + // translators: A breadcrumb trail for the Admin document title. %1$s: title of template being edited, %2$s: type of template (Template or Template Part). + __( '%1$s ‹ %2$s' ), getTitle(), POST_TYPE_LABELS[ editedPostType ] ?? POST_TYPE_LABELS[ TEMPLATE_POST_TYPE ] diff --git a/packages/edit-site/src/components/routes/use-title.js b/packages/edit-site/src/components/routes/use-title.js index 6d06c593dd253a..775f3659778748 100644 --- a/packages/edit-site/src/components/routes/use-title.js +++ b/packages/edit-site/src/components/routes/use-title.js @@ -38,8 +38,8 @@ export default function useTitle( title ) { if ( title && siteTitle ) { // @see https://github.com/WordPress/wordpress-develop/blob/94849898192d271d533e09756007e176feb80697/src/wp-admin/admin-header.php#L67-L68 const formattedTitle = sprintf( - /* translators: Admin screen title. 1: Admin screen name, 2: Network or site name. */ - __( '%1$s ‹ %2$s — WordPress' ), + /* translators: Admin document title. 1: Admin screen name, 2: Network or site name. */ + __( '%1$s ‹ %2$s ‹ Editor — WordPress' ), decodeEntities( title ), decodeEntities( siteTitle ) ); @@ -47,14 +47,7 @@ export default function useTitle( title ) { document.title = formattedTitle; // Announce title on route change for screen readers. - speak( - sprintf( - /* translators: The page title that is currently displaying. */ - __( 'Now displaying: %s' ), - document.title - ), - 'assertive' - ); + speak( title, 'assertive' ); } }, [ title, siteTitle, location ] ); } From 66e087114f3452f9ad310c9f31a6325b6faef700 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco Date: Mon, 4 Dec 2023 16:34:55 +0100 Subject: [PATCH 013/325] Mobile - Global Styles - Fix issue with custom color variables not being parsed (#56752) --- .../test/utils.native.js | 22 +++++++++++++++++++ .../global-styles-context/utils.native.js | 14 ++++++++++++ 2 files changed, 36 insertions(+) diff --git a/packages/components/src/mobile/global-styles-context/test/utils.native.js b/packages/components/src/mobile/global-styles-context/test/utils.native.js index c1f968de24e485..6144b9a13ae89e 100644 --- a/packages/components/src/mobile/global-styles-context/test/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/test/utils.native.js @@ -108,6 +108,28 @@ describe( 'parseStylesVariables', () => { expect.objectContaining( PARSED_GLOBAL_STYLES ) ); } ); + + it( 'returns the parsed custom color values correctly', () => { + const defaultStyles = { + ...DEFAULT_GLOBAL_STYLES, + color: { + text: 'var(--wp--custom--color--blue)', + background: 'var(--wp--custom--color--green)', + }, + }; + const customValues = parseStylesVariables( + JSON.stringify( RAW_FEATURES.custom ), + MAPPED_VALUES + ); + const styles = parseStylesVariables( + JSON.stringify( defaultStyles ), + MAPPED_VALUES, + customValues + ); + expect( styles ).toEqual( + expect.objectContaining( PARSED_GLOBAL_STYLES ) + ); + } ); } ); describe( 'getGlobalStyles', () => { diff --git a/packages/components/src/mobile/global-styles-context/utils.native.js b/packages/components/src/mobile/global-styles-context/utils.native.js index f2cbcae9c3f3e7..b56e28da46207c 100644 --- a/packages/components/src/mobile/global-styles-context/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/utils.native.js @@ -248,6 +248,20 @@ export function parseStylesVariables( styles, mappedValues, customValues ) { const customValuesData = customValues ?? JSON.parse( stylesBase ); stylesBase = stylesBase.replace( regex, ( _$1, $2 ) => { const path = $2.split( '--' ); + + // Supports cases for variables like var(--wp--custom--color--background) + if ( path[ 0 ] === 'color' ) { + const colorKey = path[ path.length - 1 ]; + if ( mappedValues?.color ) { + const matchedValue = mappedValues.color?.values?.find( + ( { slug } ) => slug === colorKey + ); + if ( matchedValue ) { + return `${ matchedValue?.color }`; + } + } + } + if ( path.reduce( ( prev, curr ) => prev && prev[ curr ], From 17db200449f0e385b7b41c142936b01822f4e676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Mon, 4 Dec 2023 18:36:50 +0100 Subject: [PATCH 014/325] DataViews: doo not export string constants (#56754) --- packages/dataviews/README.md | 36 ++++++++++++------- packages/dataviews/src/index.js | 10 +----- .../src/components/page-pages/index.js | 18 +++++----- .../page-templates/dataviews-templates.js | 16 ++++----- .../sidebar-dataviews/default-views.js | 6 +++- packages/edit-site/src/utils/constants.js | 8 +++++ 6 files changed, 55 insertions(+), 39 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 99b36d8f53c11c..2ee1d7f42eff5b 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -46,7 +46,7 @@ Example: ```js { - type: LAYOUT_TABLE, + type: 'table', perPage: 5, page: 1, sort: { @@ -55,15 +55,15 @@ Example: }, search: '', filters: [ - { field: 'author', operator: OPERATOR_IN, value: 2 }, - { field: 'status', operator: OPERATOR_IN, value: 'publish,draft' } + { field: 'author', operator: 'in', value: 2 }, + { field: 'status', operator: 'in', value: 'publish,draft' } ], hiddenFields: [ 'date', 'featured-image' ], layout: {}, } ``` -- `type`: view type, one of `table`, `grid`, or `side-by-side`. +- `type`: view type, one of `table`, `grid`, `list`. See "View types". - `perPage`: number of records to show per page. - `page`: the page that is visible. - `sort.field`: field used for sorting the dataset. @@ -71,7 +71,7 @@ Example: - `search`: the text search applied to the dataset. - `filters`: the filters applied to the dataset. Each item describes: - `field`: which field this filter is bound to. - - `operator`: which type of filter it is. Only `in` available at the moment. + - `operator`: which type of filter it is. One of `in`, `notIn`. See "Operator types". - `value`: the actual value selected by the user. - `hiddenFields`: the `id` of the fields that are hidden in the UI. - `layout`: config that is specific to a particular layout type. @@ -87,7 +87,7 @@ The following example shows how a view object is used to query the WordPress RES ```js function MyCustomPageTable() { const [ view, setView ] = useState( { - type: TABLE_LAYOUT, + type: 'table', perPage: 5, page: 1, sort: { @@ -96,8 +96,8 @@ function MyCustomPageTable() { }, search: '', filters: [ - { field: 'author', operator: OPERATOR_IN, value: 2 }, - { field: 'status', operator: OPERATOR_IN, value: 'publish,draft' } + { field: 'author', operator: 'in', value: 2 }, + { field: 'status', operator: 'in', value: 'publish,draft' } ], hiddenFields: [ 'date', 'featured-image' ], layout: {}, @@ -106,10 +106,10 @@ function MyCustomPageTable() { const queryArgs = useMemo( () => { const filters = {}; view.filters.forEach( ( filter ) => { - if ( filter.field === 'status' && filter.operator === OPERATOR_IN ) { + if ( filter.field === 'status' && filter.operator === 'in' ) { filters.status = filter.value; } - if ( filter.field === 'author' && filter.operator === OPERATOR_IN ) { + if ( filter.field === 'author' && filter.operator === 'in' ) { filters.author = filter.value; } } ); @@ -167,7 +167,7 @@ Example: { item.author } ); }, - type: ENUMERATION_TYPE, + type: 'enumeration', elements: [ { value: 1, label: 'Admin' } { value: 2, label: 'User' } @@ -182,7 +182,7 @@ Example: - `getValue`: function that returns the value of the field. - `render`: function that renders the field. - `elements`: the set of valid values for the field's value. -- `type`: the type of the field. Used to generate the proper filters. Only `enumeration` available at the moment. +- `type`: the type of the field. Used to generate the proper filters. Only `enumeration` available at the moment. See "Field types". - `enableSorting`: whether the data can be sorted by the given field. True by default. - `enableHiding`: whether the field can be hidden. True by default. - `filterBy`: configuration for the filters. @@ -202,6 +202,18 @@ Array of operations that can be performed upon each record. Each action is an ob - `RenderModal`: ReactElement, optional. If an action requires that some UI be rendered in a modal, it can provide a component which takes as props the record as `item` and a `closeModal` function. When this prop is provided, the `callback` property is ignored. - `hideModalHeader`: boolean, optional. This property is used in combination with `RenderModal` and controls the visibility of the modal's header. If the action renders a modal and doesn't hide the header, the action's label is going to be used in the modal's header. +## Types + +- Layout types: + - `table`: the view uses a table layout. + - `grid`: the view uses a grid layout. + - `list`: the view uses a list layout. +- Field types: + - `enumeration`: the field value should be taken and can be filtered from a closed list of elements. +- Operator types: + - `in`: operator to be used in filters for fields of type `enumeration`. + - `notIn`: operator to be used in filters for fields of type `enumeration`. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. diff --git a/packages/dataviews/src/index.js b/packages/dataviews/src/index.js index 01fdaeae0073bf..01c67b34c5c99d 100644 --- a/packages/dataviews/src/index.js +++ b/packages/dataviews/src/index.js @@ -1,10 +1,2 @@ export { default as DataViews } from './dataviews'; -export { - VIEW_LAYOUTS, - LAYOUT_GRID, - LAYOUT_TABLE, - LAYOUT_LIST, - ENUMERATION_TYPE, - OPERATOR_IN, - OPERATOR_NOT_IN, -} from './constants'; +export { VIEW_LAYOUTS } from './constants'; diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index cc56aa15122f2c..bac881f2ceb218 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -12,15 +12,7 @@ import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; -import { - DataViews, - ENUMERATION_TYPE, - LAYOUT_GRID, - LAYOUT_TABLE, - OPERATOR_IN, - OPERATOR_NOT_IN, - VIEW_LAYOUTS, -} from '@wordpress/dataviews'; +import { DataViews, VIEW_LAYOUTS } from '@wordpress/dataviews'; /** * Internal dependencies @@ -28,6 +20,14 @@ import { import Page from '../page'; import Link from '../routes/link'; import { default as DEFAULT_VIEWS } from '../sidebar-dataviews/default-views'; +import { + ENUMERATION_TYPE, + LAYOUT_GRID, + LAYOUT_TABLE, + OPERATOR_IN, + OPERATOR_NOT_IN, +} from '../../utils/constants'; + import { trashPostAction, usePermanentlyDeletePostAction, diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 07f32441da84f3..183d85ce40797b 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -23,13 +23,7 @@ import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { - DataViews, - ENUMERATION_TYPE, - OPERATOR_IN, - LAYOUT_GRID, - LAYOUT_TABLE, -} from '@wordpress/dataviews'; +import { DataViews } from '@wordpress/dataviews'; /** * Internal dependencies @@ -37,7 +31,13 @@ import { import Page from '../page'; import Link from '../routes/link'; import { useAddedBy, AvatarImage } from '../list/added-by'; -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { + TEMPLATE_POST_TYPE, + ENUMERATION_TYPE, + OPERATOR_IN, + LAYOUT_GRID, + LAYOUT_TABLE, +} from '../../utils/constants'; import { useResetTemplateAction, deleteTemplateAction, diff --git a/packages/edit-site/src/components/sidebar-dataviews/default-views.js b/packages/edit-site/src/components/sidebar-dataviews/default-views.js index d22786cff0b5c8..11652286e62d8d 100644 --- a/packages/edit-site/src/components/sidebar-dataviews/default-views.js +++ b/packages/edit-site/src/components/sidebar-dataviews/default-views.js @@ -3,7 +3,11 @@ */ import { __ } from '@wordpress/i18n'; import { trash } from '@wordpress/icons'; -import { LAYOUT_TABLE, OPERATOR_IN } from '@wordpress/dataviews'; + +/** + * Internal dependencies + */ +import { LAYOUT_TABLE, OPERATOR_IN } from '../../utils/constants'; const DEFAULT_PAGE_BASE = { type: LAYOUT_TABLE, diff --git a/packages/edit-site/src/utils/constants.js b/packages/edit-site/src/utils/constants.js index 0aae3e681a16e5..f5ca89b9fb62cd 100644 --- a/packages/edit-site/src/utils/constants.js +++ b/packages/edit-site/src/utils/constants.js @@ -44,3 +44,11 @@ export const POST_TYPE_LABELS = { [ PATTERN_TYPES.user ]: __( 'Pattern' ), [ NAVIGATION_POST_TYPE ]: __( 'Navigation' ), }; + +// DataViews constants +export const LAYOUT_GRID = 'grid'; +export const LAYOUT_TABLE = 'table'; +export const LAYOUT_LIST = 'list'; +export const ENUMERATION_TYPE = 'enumeration'; +export const OPERATOR_IN = 'in'; +export const OPERATOR_NOT_IN = 'notIn'; From c1852f305c58e08979cc8d6093d9ecfd57a5585a Mon Sep 17 00:00:00 2001 From: Matias Ventura Date: Mon, 4 Dec 2023 19:15:21 +0100 Subject: [PATCH 015/325] Update preferences organization (#56481) * Preferences reorganization: - Add "Appeareance" and "Accessibility" panels. - Update sections across the panels (blocks->inserter). - Improve copy for clarity and consistency. * Add "top toolbar" setting to the appearance panel. * Adjust copy of block breadcrumbs option * Simplify distraction free setting copy * Update site editor preferences panel to align * Fix e2e test utilities * Fix more tests * make top toolbar and distraction free work in tandem across editors and preference UIs * stub e2e for pref modal * improved the test * updated tests and moved a unit tests to an e2e because it tested what the user sees on screen --------- Co-authored-by: Andrei Draganescu --- .../src/disable-pre-publish-checks.js | 2 +- .../src/enable-pre-publish-checks.js | 6 +- .../components/header/writing-menu/index.js | 34 ++- .../src/components/preferences-modal/index.js | 205 ++++++++++-------- .../preferences-modal/test/index.js | 53 +---- .../header-edit-mode/more-menu/index.js | 40 ++-- .../src/components/preferences-modal/index.js | 92 +++++--- .../components/preferences-modal/README.md | 6 +- test/e2e/specs/editor/various/a11y.spec.js | 11 - .../specs/editor/various/pref-modal.spec.js | 55 +++++ test/e2e/specs/editor/various/preview.spec.js | 4 +- 11 files changed, 265 insertions(+), 243 deletions(-) create mode 100644 test/e2e/specs/editor/various/pref-modal.spec.js diff --git a/packages/e2e-test-utils/src/disable-pre-publish-checks.js b/packages/e2e-test-utils/src/disable-pre-publish-checks.js index 2c12a0aaaa99e4..25660e48c9555d 100644 --- a/packages/e2e-test-utils/src/disable-pre-publish-checks.js +++ b/packages/e2e-test-utils/src/disable-pre-publish-checks.js @@ -10,7 +10,7 @@ import { toggleMoreMenu } from './toggle-more-menu'; export async function disablePrePublishChecks() { await togglePreferencesOption( 'General', - 'Include pre-publish checklist', + 'Enable pre-publish flow', false ); await toggleMoreMenu( 'close' ); diff --git a/packages/e2e-test-utils/src/enable-pre-publish-checks.js b/packages/e2e-test-utils/src/enable-pre-publish-checks.js index a9c8b572302b15..5a65ba6fc1353d 100644 --- a/packages/e2e-test-utils/src/enable-pre-publish-checks.js +++ b/packages/e2e-test-utils/src/enable-pre-publish-checks.js @@ -8,10 +8,6 @@ import { toggleMoreMenu } from './toggle-more-menu'; * Enables Pre-publish checks. */ export async function enablePrePublishChecks() { - await togglePreferencesOption( - 'General', - 'Include pre-publish checklist', - true - ); + await togglePreferencesOption( 'General', 'Enable pre-publish flow', true ); await toggleMoreMenu( 'close' ); } diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index de6acf67c19834..26cc6bc5871650 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; +import { useDispatch, useRegistry } from '@wordpress/data'; import { MenuGroup } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; import { useViewportMatch } from '@wordpress/compose'; @@ -10,7 +10,6 @@ import { PreferenceToggleMenuItem, store as preferencesStore, } from '@wordpress/preferences'; -import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -19,11 +18,6 @@ import { store as postEditorStore } from '../../../store'; function WritingMenu() { const registry = useRegistry(); - const isDistractionFree = useSelect( - ( select ) => - select( blockEditorStore ).getSettings().isDistractionFree, - [] - ); const { setIsInserterOpened, setIsListViewOpened, closeGeneralSidebar } = useDispatch( postEditorStore ); @@ -38,6 +32,10 @@ function WritingMenu() { } ); }; + const turnOffDistractionFree = () => { + setPreference( 'core/edit-post', 'distractionFree', false ); + }; + const isLargeViewport = useViewportMatch( 'medium' ); if ( ! isLargeViewport ) { return null; @@ -47,8 +45,8 @@ function WritingMenu() { + - ); } diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index c08dda81f8e594..833a10fce13c33 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -76,6 +76,10 @@ export default function EditPostPreferencesModal() { closeGeneralSidebar(); }; + const turnOffDistractionFree = () => { + setPreference( 'core/edit-post', 'distractionFree', false ); + }; + const sections = useMemo( () => [ { @@ -86,49 +90,16 @@ export default function EditPostPreferencesModal() { { isLargeViewport && ( ) } - - - - - + - { showBlockBreadcrumbsOption && ( ) } - - ), - }, - { - name: 'blocks', - tabLabel: __( 'Blocks' ), - content: ( - <> - - - - - - - - - ), - }, - { - name: 'panels', - tabLabel: __( 'Panels' ), - content: ( - <> @@ -242,12 +159,108 @@ export default function EditPostPreferencesModal() { /> - + + ), + }, + { + name: 'appearance', + tabLabel: __( 'Appearance' ), + content: ( + + + + + + + ), + }, + { + name: 'accessibility', + tabLabel: __( 'Accessibility' ), + content: ( + <> + + + + + + + + ), + }, + { + name: 'blocks', + tabLabel: __( 'Blocks' ), + content: ( + <> + + + + + + ), }, diff --git a/packages/edit-post/src/components/preferences-modal/test/index.js b/packages/edit-post/src/components/preferences-modal/test/index.js index a0946b478d8f23..01ac1a88fbe7d8 100644 --- a/packages/edit-post/src/components/preferences-modal/test/index.js +++ b/packages/edit-post/src/components/preferences-modal/test/index.js @@ -1,13 +1,12 @@ /** * External dependencies */ -import { render, screen, within } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; /** * WordPress dependencies */ import { useSelect } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -19,56 +18,6 @@ jest.mock( '@wordpress/data/src/components/use-select', () => jest.fn() ); jest.mock( '@wordpress/compose/src/hooks/use-viewport-match', () => jest.fn() ); describe( 'EditPostPreferencesModal', () => { - describe( 'should match snapshot when the modal is active', () => { - afterEach( () => { - useViewportMatch.mockClear(); - } ); - it( 'large viewports', async () => { - useSelect.mockImplementation( () => [ true, true, false ] ); - useViewportMatch.mockImplementation( () => true ); - render( ); - const tabPanel = await screen.findByRole( 'tabpanel', { - name: 'General', - } ); - - expect( - within( tabPanel ).getByLabelText( - 'Include pre-publish checklist' - ) - ).toBeInTheDocument(); - } ); - it( 'small viewports', async () => { - useSelect.mockImplementation( () => [ true, true, false ] ); - useViewportMatch.mockImplementation( () => false ); - render( ); - - // The tabpanel is not rendered in small viewports. - expect( - screen.queryByRole( 'tabpanel', { - name: 'General', - } ) - ).not.toBeInTheDocument(); - - const dialog = screen.getByRole( 'dialog', { - name: 'Preferences', - } ); - - // Checkbox toggle controls are not rendered in small viewports. - expect( - within( dialog ).queryByLabelText( - 'Include pre-publish checklist' - ) - ).not.toBeInTheDocument(); - - // Individual preference nav buttons are rendered in small viewports. - expect( - within( dialog ).getByRole( 'button', { - name: 'General', - } ) - ).toBeInTheDocument(); - } ); - } ); - it( 'should not render when the modal is not active', () => { useSelect.mockImplementation( () => [ false, false, false ] ); render( ); diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js index 2185258ad338a6..f6c47c1eb93bd9 100644 --- a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js +++ b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; -import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; +import { useDispatch, useRegistry } from '@wordpress/data'; import { displayShortcut } from '@wordpress/keycodes'; import { external } from '@wordpress/icons'; import { MenuGroup, MenuItem, VisuallyHidden } from '@wordpress/components'; @@ -36,14 +36,6 @@ import { store as siteEditorStore } from '../../../store'; export default function MoreMenu( { showIconLabels } ) { const registry = useRegistry(); - const isDistractionFree = useSelect( - ( select ) => - select( preferencesStore ).get( - 'core/edit-site', - 'distractionFree' - ), - [] - ); const { setIsInserterOpened, setIsListViewOpened, closeGeneralSidebar } = useDispatch( siteEditorStore ); @@ -59,6 +51,10 @@ export default function MoreMenu( { showIconLabels } ) { } ); }; + const turnOffDistractionFree = () => { + setPreference( 'core/edit-site', 'distractionFree', false ); + }; + return ( <> - + { + setPreference( 'core/edit-site', 'distractionFree', false ); + }; + const sections = useMemo( () => [ { name: 'general', tabLabel: __( 'General' ), content: ( - - - - + + + ), }, + { + name: 'accessibility', + tabLabel: __( 'Accessibility' ), + content: ( + <> + + + + + + + + ), + }, ] ); if ( ! isModalActive ) { return null; diff --git a/packages/interface/src/components/preferences-modal/README.md b/packages/interface/src/components/preferences-modal/README.md index 96ecdf03dcc136..f873ccf297ec12 100644 --- a/packages/interface/src/components/preferences-modal/README.md +++ b/packages/interface/src/components/preferences-modal/README.md @@ -28,11 +28,11 @@ function MyEditorPreferencesModal() { 'Review settings, such as visibility and tags.' ) } label={ __( - 'Include pre-publish checklist' + 'Enable pre-publish flow' ) } /> - ) + ) } { @@ -47,7 +47,7 @@ function MyEditorPreferencesModal() { > // Section content here - ) + ) } ] diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 0a5e421debedb7..05c4ea3b8e97e3 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -148,9 +148,6 @@ test.describe( 'a11y (@firefox, @webkit)', () => { const blocksTab = preferencesModal.locator( 'role=tab[name="Blocks"i]' ); - const panelsTab = preferencesModal.locator( - 'role=tab[name="Panels"i]' - ); // Check initial focus is on the modal dialog container. await expect( preferencesModal ).toBeFocused(); @@ -204,13 +201,5 @@ test.describe( 'a11y (@firefox, @webkit)', () => { await expect( closeButton ).toBeFocused(); await pageUtils.pressKeys( 'Shift+Tab' ); await expect( preferencesModalContent ).not.toBeFocused(); - - // The Panels tab panel content is short and not scrollable. - // Check it's not focusable. - await clickAndFocusTab( panelsTab ); - await pageUtils.pressKeys( 'Shift+Tab' ); - await expect( closeButton ).toBeFocused(); - await pageUtils.pressKeys( 'Shift+Tab' ); - await expect( preferencesModalContent ).not.toBeFocused(); } ); } ); diff --git a/test/e2e/specs/editor/various/pref-modal.spec.js b/test/e2e/specs/editor/various/pref-modal.spec.js new file mode 100644 index 00000000000000..f99c7d32a22a94 --- /dev/null +++ b/test/e2e/specs/editor/various/pref-modal.spec.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Preferences modal', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.describe( 'Preferences modal adaps to viewport', () => { + test( 'Enable pre-publish flow is visible on desktop ', async ( { + page, + } ) => { + await page.click( + 'role=region[name="Editor top bar"i] >> role=button[name="Options"i]' + ); + await page.click( 'role=menuitem[name="Preferences"i]' ); + + const prePublishToggle = page.locator( + 'role=checkbox[name="Enable pre-publish flow"i]' + ); + + await expect( prePublishToggle ).toBeVisible(); + } ); + } ); + test.describe( 'Preferences modal adaps to viewport', () => { + test( 'Enable pre-publish flow is not visible on mobile ', async ( { + page, + } ) => { + await page.setViewportSize( { width: 500, height: 800 } ); + + await page.click( + 'role=region[name="Editor top bar"i] >> role=button[name="Options"i]' + ); + await page.click( 'role=menuitem[name="Preferences"i]' ); + + const generalButton = page.locator( + 'role=button[name="General"i]' + ); + + const generalTabPanel = page.locator( + 'role=tabPanel[name="General"i]' + ); + + const prePublishToggle = page.locator( + 'role=checkbox[name="Enable pre-publish flow"i]' + ); + + await expect( generalButton ).toBeVisible(); + await expect( generalTabPanel ).toBeHidden(); + await expect( prePublishToggle ).toBeHidden(); + } ); + } ); +} ); diff --git a/test/e2e/specs/editor/various/preview.spec.js b/test/e2e/specs/editor/various/preview.spec.js index 8a4ee5a6bd81d2..0666de1405fae1 100644 --- a/test/e2e/specs/editor/various/preview.spec.js +++ b/test/e2e/specs/editor/various/preview.spec.js @@ -335,9 +335,9 @@ class PreviewUtils { ); await this.page.click( 'role=menuitem[name="Preferences"i]' ); - // Navigate to panels section. + // Navigate to general section. await this.page.click( - 'role=dialog[name="Preferences"i] >> role=tab[name="Panels"i]' + 'role=dialog[name="Preferences"i] >> role=tab[name="General"i]' ); // Find custom fields checkbox. From a2b7ac2f1bebbac832d8547294c15dc8e4eba4b6 Mon Sep 17 00:00:00 2001 From: Siobhan Bamber Date: Mon, 4 Dec 2023 19:15:12 +0000 Subject: [PATCH 016/325] [RNMobile] Address `NullPointerException` crash in `AztecHeadingSpan` (#56757) Address rare cases where a null value is passed to a heading block, causing a crash. --- packages/react-native-aztec/android/build.gradle | 2 +- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/react-native-aztec/android/build.gradle b/packages/react-native-aztec/android/build.gradle index 7647548360ca75..18093ff1c2c136 100644 --- a/packages/react-native-aztec/android/build.gradle +++ b/packages/react-native-aztec/android/build.gradle @@ -11,7 +11,7 @@ buildscript { espressoVersion = '3.0.1' // libs - aztecVersion = 'v1.8.0' + aztecVersion = 'v1.9.0' wordpressUtilsVersion = '3.3.0' // main diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index fa8a9af4b89ced..411499dc1dd7cd 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -16,6 +16,7 @@ For each user feature we should also add a importance categorization label to i - [***] Fix issue when backspacing in an empty Paragraph block [#56496] - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] - [**] Fixes a crash on pasting MS Word list markup [#56653] +- [**] Address rare cases where a null value is passed to a heading block, causing a crash [#56757] ## 1.109.0 - [*] Audio block: Improve legibility of audio file details on various background colors [#55627] From c516601bad06a5b8dbe78f84148afb94471ddd4b Mon Sep 17 00:00:00 2001 From: Aurorum <43215253+Aurorum@users.noreply.github.com> Date: Mon, 4 Dec 2023 19:49:37 +0000 Subject: [PATCH 017/325] Social Links Block: Prevent Theme Styles Distorting Size (#56301) * Social Links Block: Prevent Theme Styles Distorting Size * Remove :after selector * Fix #53077 --- .../block-library/src/social-links/style.scss | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/block-library/src/social-links/style.scss b/packages/block-library/src/social-links/style.scss index 23c741e9819f9a..1ad883bbb88840 100644 --- a/packages/block-library/src/social-links/style.scss +++ b/packages/block-library/src/social-links/style.scss @@ -95,14 +95,20 @@ } // This needs specificity because themes usually override it with things like .widget-area a. -.wp-block-social-links .wp-block-social-link .wp-block-social-link-anchor { - &, - &:hover, - &:active, - &:visited, - svg { - color: currentColor; - fill: currentColor; +.wp-block-social-links .wp-block-social-link.wp-social-link { + display: inline-block; + margin: 0; + padding: 0; + + .wp-block-social-link-anchor { + &, + &:hover, + &:active, + &:visited, + svg { + color: currentColor; + fill: currentColor; + } } } From 6189c2f9d2f6fd9eb8c2ad6dea4e11020b641ab3 Mon Sep 17 00:00:00 2001 From: Rich Tabor Date: Mon, 4 Dec 2023 16:34:35 -0500 Subject: [PATCH 018/325] Clean up header toolbar metrics and break points (#56729) * Use medium break point, clean up gaps * post editor * Update gap and margins. * Always use 10 gap * Pinned items to also use 8 * Add check for isLargeViewport on PreviewOptions * Update style.scss * Add back the negative margin right --------- Co-authored-by: jasmussen --- .../src/components/preview-options/index.js | 2 +- .../header/header-toolbar/style.scss | 11 ++- .../src/components/header/style.scss | 6 +- .../document-actions/style.scss | 3 +- .../src/components/header-edit-mode/index.js | 70 ++++++++++--------- .../components/header-edit-mode/style.scss | 33 +++------ .../src/components/pinned-items/style.scss | 2 +- 7 files changed, 56 insertions(+), 71 deletions(-) diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js index 961b23c9065695..91018cc980bb29 100644 --- a/packages/block-editor/src/components/preview-options/index.js +++ b/packages/block-editor/src/components/preview-options/index.js @@ -21,7 +21,7 @@ export default function PreviewOptions( { label, showIconLabels, } ) { - const isMobile = useViewportMatch( 'small', '<' ); + const isMobile = useViewportMatch( 'medium', '<' ); if ( isMobile ) return null; const popoverProps = { diff --git a/packages/edit-post/src/components/header/header-toolbar/style.scss b/packages/edit-post/src/components/header/header-toolbar/style.scss index 029a0bdcfeca1d..717d5cd760db58 100644 --- a/packages/edit-post/src/components/header/header-toolbar/style.scss +++ b/packages/edit-post/src/components/header/header-toolbar/style.scss @@ -82,14 +82,15 @@ .edit-post-header-toolbar__left { display: inline-flex; align-items: center; - padding-left: $grid-unit-10; + padding-left: $grid-unit-20; + gap: $grid-unit-10; // Some plugins add buttons here despite best practices. // Push them a bit rightwards to fit the top toolbar. margin-right: $grid-unit-10; - @include break-small() { - padding-left: $grid-unit-30; + @include break-medium() { + padding-left: $grid-unit-50 * 0.5; } @include break-wide() { @@ -98,8 +99,6 @@ } .edit-post-header-toolbar .edit-post-header-toolbar__left > .edit-post-header-toolbar__inserter-toggle.has-icon { - margin-right: $grid-unit-10; - // Special dimensions for this button. min-width: $button-size-compact; width: $button-size-compact; height: $button-size-compact; @@ -107,7 +106,7 @@ .show-icon-labels & { width: auto; - height: 36px; + height: $button-size-compact; padding: 0 $grid-unit-10; } } diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index 382364b3e35800..6e634427e21986 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -93,11 +93,7 @@ padding-right: $grid-unit-20 - ($grid-unit-15 * 0.5); } - gap: $grid-unit-05; - - @include break-small() { - gap: $grid-unit-10; - } + gap: $grid-unit-10; } .edit-post-header-preview__grouping-external { diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss index 625e33169145f7..dce73f269a705c 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss @@ -93,7 +93,8 @@ color: $gray-800; min-width: $grid-unit-40; display: none; - @include break-small() { + + @include break-medium() { display: initial; } } diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index ecb501f669ee97..110e9ca3b4d842 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -216,41 +216,43 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { variants={ toolbarVariants } transition={ toolbarTransition } > -
- - { ( { onClose } ) => ( - - - { __( 'View site' ) } - - { - /* translators: accessibility text */ - __( '(opens in a new tab)' ) - } - - - + { isLargeViewport && ( +
-
+ > + + { ( { onClose } ) => ( + + + { __( 'View site' ) } + + { + /* translators: accessibility text */ + __( '(opens in a new tab)' ) + } + + + + ) } + +
+ ) } { ! isDistractionFree && ( diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 8df4690c8fca22..0fe335c5292ba2 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -25,7 +25,9 @@ $header-toolbar-min-width: 335px; // is visible on toolbar buttons. height: 100%; // Allow focus ring to be fully visible on furthest right button. - padding-right: 2px; + @include break-medium() { + padding-right: 2px; + } } .edit-site-header-edit-mode__end { @@ -38,7 +40,7 @@ $header-toolbar-min-width: 335px; align-items: center; height: 100%; flex-grow: 1; - margin: 0 $grid-unit-10; + margin: 0 $grid-unit-20; justify-content: center; // Flex items will, by default, refuse to shrink below a minimum // intrinsic width. In order to shrink this flexbox item, and @@ -55,10 +57,11 @@ $header-toolbar-min-width: 335px; .edit-site-header-edit-mode__toolbar { display: flex; align-items: center; - padding-left: $grid-unit-10; + padding-left: $grid-unit-20; + gap: $grid-unit-10; - @include break-small() { - padding-left: $grid-unit-30; + @include break-medium() { + padding-left: $grid-unit-50 * 0.5; } @include break-wide() { @@ -66,12 +69,6 @@ $header-toolbar-min-width: 335px; } .edit-site-header-edit-mode__inserter-toggle { - margin-right: $grid-unit-10; - min-width: $grid-unit-40; - width: $grid-unit-40; - height: $grid-unit-40; - padding: 0; - svg { transition: transform cubic-bezier(0.165, 0.84, 0.44, 1) 0.2s; @include reduce-motion("transition"); @@ -92,17 +89,8 @@ $header-toolbar-min-width: 335px; .edit-site-header-edit-mode__actions { display: inline-flex; align-items: center; - padding-right: $grid-unit-05; - - @include break-small () { - padding-right: $grid-unit-20 - ($grid-unit-15 * 0.5); - } - - gap: $grid-unit-05; - - @include break-small() { - gap: $grid-unit-10; - } + padding-right: $grid-unit-10; + gap: $grid-unit-10; } .edit-site-header-edit-mode__preview-options { @@ -144,7 +132,6 @@ $header-toolbar-min-width: 335px; } .edit-site-header-edit-mode__toolbar > .edit-site-header-edit-mode__inserter-toggle.has-icon { - margin-right: $grid-unit-10; // Special dimensions for this button. min-width: $button-size-compact; width: $button-size-compact; diff --git a/packages/interface/src/components/pinned-items/style.scss b/packages/interface/src/components/pinned-items/style.scss index 693750644c62a1..66062b7fa3dbbf 100644 --- a/packages/interface/src/components/pinned-items/style.scss +++ b/packages/interface/src/components/pinned-items/style.scss @@ -26,7 +26,7 @@ } // Gap between pinned items. - gap: $grid-unit-05; + gap: $grid-unit-10; // Account for larger grid from parent container gap. margin-right: -$grid-unit-05; From 126f7844539c5ca6f2d570d10bee04370d3c782b Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 5 Dec 2023 10:49:05 +0900 Subject: [PATCH 019/325] Command Palette: Use getRevisions instead of deprecated selector (#56738) * Command Palette: Use getRevisions instead of deprecated selector * Changed to faster logic --- .../src/hooks/commands/use-common-commands.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/edit-site/src/hooks/commands/use-common-commands.js b/packages/edit-site/src/hooks/commands/use-common-commands.js index f6da4829bf38d7..fdea50d8afbddb 100644 --- a/packages/edit-site/src/hooks/commands/use-common-commands.js +++ b/packages/edit-site/src/hooks/commands/use-common-commands.js @@ -244,11 +244,16 @@ function useGlobalStylesOpenRevisionsCommands() { const isMobileViewport = useViewportMatch( 'medium', '<' ); const isEditorPage = ! getIsListPage( params, isMobileViewport ); const history = useHistory(); - const hasRevisions = useSelect( - ( select ) => - select( coreStore ).getCurrentThemeGlobalStylesRevisions()?.length, - [] - ); + const hasRevisions = useSelect( ( select ) => { + const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = + select( coreStore ); + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; + return !! globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count; + }, [] ); + const commands = useMemo( () => { if ( ! hasRevisions ) { return []; From 41ddc35ac19f2294b193352e391538f14988e908 Mon Sep 17 00:00:00 2001 From: Ramon Date: Tue, 5 Dec 2023 15:15:16 +1100 Subject: [PATCH 020/325] Site Editor: show back button when editing navigation and template area in-place with no URL params (#56741) * This commit: - sends a default template id to the location state for the back button in focus mode (navigation and template area editing) so the back button works when coming from a template state, but with no URL params - move the back button to the bottom of the canvas container in distraction free mode because the back button is unreachable due to the header * Move into clickhandler * Revert showing the back button under all conditions. --- packages/edit-site/src/components/block-editor/style.scss | 5 +++++ packages/edit-site/src/hooks/navigation-menu-edit.js | 2 +- packages/edit-site/src/hooks/template-part-edit.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index b110b1c274e779..dbc67049fcb869 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -96,6 +96,11 @@ } } +// The toolbar header in distraction mode sits over the back button, which renders it unreachable. +.is-distraction-free .edit-site-visual-editor__back-button { + display: none; +} + .resizable-editor__drag-handle { position: absolute; top: 0; diff --git a/packages/edit-site/src/hooks/navigation-menu-edit.js b/packages/edit-site/src/hooks/navigation-menu-edit.js index 4c04b1b2534026..8b502f075430b0 100644 --- a/packages/edit-site/src/hooks/navigation-menu-edit.js +++ b/packages/edit-site/src/hooks/navigation-menu-edit.js @@ -43,7 +43,7 @@ function NavigationMenuEdit( { attributes } ) { }, { // this applies to Navigation Menus as well. - fromTemplateId: params.postId, + fromTemplateId: params.postId || navigationMenu?.id, } ); diff --git a/packages/edit-site/src/hooks/template-part-edit.js b/packages/edit-site/src/hooks/template-part-edit.js index 0b14bbbbd77121..e60b7945448e7b 100644 --- a/packages/edit-site/src/hooks/template-part-edit.js +++ b/packages/edit-site/src/hooks/template-part-edit.js @@ -43,7 +43,7 @@ function EditTemplatePartMenuItem( { attributes } ) { canvas: 'edit', }, { - fromTemplateId: params.postId, + fromTemplateId: params.postId || templatePart?.id, } ); From a5a4edfc94b9e87bfe4debeaf152854aa7b87db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Chantre?= Date: Tue, 5 Dec 2023 07:48:01 +0100 Subject: [PATCH 021/325] Scripts: Fix CSS imports not minified (#56516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix CSS imports not minified Runs PostCSS on the imported files. Closes #55885. * Update CHANGELOG.md --------- Co-authored-by: Greg Ziółkowski --- packages/scripts/CHANGELOG.md | 4 ++++ packages/scripts/config/webpack.config.js | 1 + 2 files changed, 5 insertions(+) diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index ab55ff9adf5b8e..7ad87c315df80b 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- Fix CSS imports not minified ([#56516](https://github.com/WordPress/gutenberg/pull/56516)). + ## 26.18.0 (2023-11-29) ### Internal diff --git a/packages/scripts/config/webpack.config.js b/packages/scripts/config/webpack.config.js index 05b37945795a7b..1e060d0e142c91 100644 --- a/packages/scripts/config/webpack.config.js +++ b/packages/scripts/config/webpack.config.js @@ -69,6 +69,7 @@ const cssLoaders = [ { loader: require.resolve( 'css-loader' ), options: { + importLoaders: 1, sourceMap: ! isProduction, modules: { auto: true, From 338ae241d6d167b457f364cde737f96264b9ab44 Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Dec 2023 08:10:41 +0100 Subject: [PATCH 022/325] Site Editor: Merge the post only mode and the post editor (#56671) --- .../reference-guides/data/data-core-editor.md | 12 + .../src/components/visual-editor/index.js | 321 ++--------------- .../src/components/visual-editor/style.scss | 15 - packages/edit-post/src/editor.js | 5 +- .../components/block-editor/editor-canvas.js | 59 +++- .../block-editor/site-editor-canvas.js | 50 +-- .../src/components/block-editor/style.scss | 2 +- .../block-editor/use-site-editor-settings.js | 1 + .../sidebar-edit-mode/page-panels/index.js | 64 ++-- .../src/components/editor-canvas/index.js | 334 ++++++++++++++++++ .../editor/src/components/provider/index.js | 89 +---- packages/editor/src/private-apis.js | 2 + packages/editor/src/store/actions.js | 8 +- packages/editor/src/store/index.js | 3 + packages/editor/src/store/private-actions.js | 13 + packages/editor/src/store/reducer.js | 10 + packages/editor/src/store/selectors.js | 11 + .../editor-style-overrides.css | 2 +- test/e2e/specs/site-editor/pages.spec.js | 19 +- 19 files changed, 535 insertions(+), 485 deletions(-) create mode 100644 packages/editor/src/components/editor-canvas/index.js create mode 100644 packages/editor/src/store/private-actions.js diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 0bfa052cf15229..fae5b8a78e2cfc 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -256,6 +256,18 @@ _Returns_ - `string`: Post type. +### getCurrentTemplateId + +Returns the template ID currently being rendered/edited + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `string?`: Template ID. + ### getEditedPostAttribute Returns a single attribute of the post being edited, preferring the unsaved edit if one exists, but falling back to the attribute for the last known saved state of the post. diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 25dcf941970aca..5b9290fff51375 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -6,24 +6,21 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { PostTitle, store as editorStore } from '@wordpress/editor'; import { - BlockList, + store as editorStore, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; +import { BlockTools, - store as blockEditorStore, __unstableUseTypewriter as useTypewriter, - __unstableUseTypingObserver as useTypingObserver, __experimentalUseResizeCanvas as useResizeCanvas, - useSettings, - __experimentalRecursionProvider as RecursionProvider, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useEffect, useRef, useMemo } from '@wordpress/element'; +import { useRef, useMemo, useEffect } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; -import { useSelect } from '@wordpress/data'; +import { useSelect, useDispatch } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; -import { parse, store as blocksStore } from '@wordpress/blocks'; -import { store as coreStore } from '@wordpress/core-data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -31,128 +28,46 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { - LayoutStyle, - useLayoutClasses, - useLayoutStyles, - ExperimentalBlockCanvas: BlockCanvas, -} = unlock( blockEditorPrivateApis ); +const { ExperimentalBlockCanvas: BlockCanvas } = unlock( + blockEditorPrivateApis +); +const { EditorCanvas } = unlock( editorPrivateApis ); const isGutenbergPlugin = process.env.IS_GUTENBERG_PLUGIN ? true : false; -/** - * Given an array of nested blocks, find the first Post Content - * block inside it, recursing through any nesting levels, - * and return its attributes. - * - * @param {Array} blocks A list of blocks. - * - * @return {Object | undefined} The Post Content block. - */ -function getPostContentAttributes( blocks ) { - for ( let i = 0; i < blocks.length; i++ ) { - if ( blocks[ i ].name === 'core/post-content' ) { - return blocks[ i ].attributes; - } - if ( blocks[ i ].innerBlocks.length ) { - const nestedPostContent = getPostContentAttributes( - blocks[ i ].innerBlocks - ); - - if ( nestedPostContent ) { - return nestedPostContent; - } - } - } -} - -function checkForPostContentAtRootLevel( blocks ) { - for ( let i = 0; i < blocks.length; i++ ) { - if ( blocks[ i ].name === 'core/post-content' ) { - return true; - } - } - return false; -} - export default function VisualEditor( { styles } ) { const { deviceType, isWelcomeGuideVisible, isTemplateMode, - postContentAttributes, - editedPostTemplate = {}, - wrapperBlockName, - wrapperUniqueId, isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { const { isFeatureActive, isEditingTemplate, - getEditedPostTemplate, __experimentalGetPreviewDeviceType, } = select( editPostStore ); - const { getCurrentPostId, getCurrentPostType, getEditorSettings } = - select( editorStore ); + const { getEditorSettings } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); const _isTemplateMode = isEditingTemplate(); - const postTypeSlug = getCurrentPostType(); - let _wrapperBlockName; - - if ( postTypeSlug === 'wp_block' ) { - _wrapperBlockName = 'core/block'; - } else if ( ! _isTemplateMode ) { - _wrapperBlockName = 'core/post-content'; - } - const editorSettings = getEditorSettings(); - const supportsTemplateMode = editorSettings.supportsTemplateMode; - const postType = select( coreStore ).getPostType( postTypeSlug ); - const canEditTemplate = select( coreStore ).canUser( - 'create', - 'templates' - ); return { deviceType: __experimentalGetPreviewDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), isTemplateMode: _isTemplateMode, - postContentAttributes: getEditorSettings().postContentAttributes, - // Post template fetch returns a 404 on classic themes, which - // messes with e2e tests, so check it's a block theme first. - editedPostTemplate: - postType?.viewable && supportsTemplateMode && canEditTemplate - ? getEditedPostTemplate() - : undefined, - wrapperBlockName: _wrapperBlockName, - wrapperUniqueId: getCurrentPostId(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, hasV3BlocksOnly: getBlockTypes().every( ( type ) => { return type.apiVersion >= 3; } ), }; }, [] ); - const { isCleanNewPost } = useSelect( editorStore ); const hasMetaBoxes = useSelect( ( select ) => select( editPostStore ).hasMetaBoxes(), [] ); - const { - hasRootPaddingAwareAlignments, - isFocusMode, - themeHasDisabledLayoutStyles, - themeSupportsLayout, - } = useSelect( ( select ) => { - const _settings = select( blockEditorStore ).getSettings(); - return { - themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, - themeSupportsLayout: _settings.supportsLayout, - isFocusMode: _settings.focusMode, - hasRootPaddingAwareAlignments: - _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, - }; - }, [] ); + const { setRenderingMode } = useDispatch( editorStore ); const desktopCanvasStyles = { height: '100%', width: '100%', @@ -171,7 +86,6 @@ export default function VisualEditor( { styles } ) { borderBottom: 0, }; const resizedCanvasStyles = useResizeCanvas( deviceType, isTemplateMode ); - const [ globalLayoutSettings ] = useSettings( 'layout' ); const previewMode = 'is-' + deviceType.toLowerCase() + '-preview'; let animatedStyles = isTemplateMode @@ -192,143 +106,19 @@ export default function VisualEditor( { styles } ) { const ref = useRef(); const contentRef = useMergeRefs( [ ref, useTypewriter() ] ); - // fallbackLayout is used if there is no Post Content, - // and for Post Title. - const fallbackLayout = useMemo( () => { - if ( isTemplateMode ) { - return { type: 'default' }; - } - - if ( themeSupportsLayout ) { - // We need to ensure support for wide and full alignments, - // so we add the constrained type. - return { ...globalLayoutSettings, type: 'constrained' }; - } - // Set default layout for classic themes so all alignments are supported. - return { type: 'default' }; - }, [ isTemplateMode, themeSupportsLayout, globalLayoutSettings ] ); - - const newestPostContentAttributes = useMemo( () => { - if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { - return postContentAttributes; - } - // When in template editing mode, we can access the blocks directly. - if ( editedPostTemplate?.blocks ) { - return getPostContentAttributes( editedPostTemplate?.blocks ); - } - // If there are no blocks, we have to parse the content string. - // Best double-check it's a string otherwise the parse function gets unhappy. - const parseableContent = - typeof editedPostTemplate?.content === 'string' - ? editedPostTemplate?.content - : ''; - - return getPostContentAttributes( parse( parseableContent ) ) || {}; - }, [ - editedPostTemplate?.content, - editedPostTemplate?.blocks, - postContentAttributes, - ] ); - - const hasPostContentAtRootLevel = useMemo( () => { - if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { - return false; - } - // When in template editing mode, we can access the blocks directly. - if ( editedPostTemplate?.blocks ) { - return checkForPostContentAtRootLevel( editedPostTemplate?.blocks ); - } - // If there are no blocks, we have to parse the content string. - // Best double-check it's a string otherwise the parse function gets unhappy. - const parseableContent = - typeof editedPostTemplate?.content === 'string' - ? editedPostTemplate?.content - : ''; - - return ( - checkForPostContentAtRootLevel( parse( parseableContent ) ) || false - ); - }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] ); - - const { layout = {}, align = '' } = newestPostContentAttributes || {}; - - const postContentLayoutClasses = useLayoutClasses( - newestPostContentAttributes, - 'core/post-content' - ); - - const blockListLayoutClass = classnames( - { - 'is-layout-flow': ! themeSupportsLayout, - }, - themeSupportsLayout && postContentLayoutClasses, - align && `align${ align }` - ); - - const postContentLayoutStyles = useLayoutStyles( - newestPostContentAttributes, - 'core/post-content', - '.block-editor-block-list__layout.is-root-container' - ); - - // Update type for blocks using legacy layouts. - const postContentLayout = useMemo( () => { - return layout && - ( layout?.type === 'constrained' || - layout?.inherit || - layout?.contentSize || - layout?.wideSize ) - ? { ...globalLayoutSettings, ...layout, type: 'constrained' } - : { ...globalLayoutSettings, ...layout, type: 'default' }; - }, [ - layout?.type, - layout?.inherit, - layout?.contentSize, - layout?.wideSize, - globalLayoutSettings, - ] ); - - // If there is a Post Content block we use its layout for the block list; - // if not, this must be a classic theme, in which case we use the fallback layout. - const blockListLayout = postContentAttributes - ? postContentLayout - : fallbackLayout; - - const postEditorLayout = - blockListLayout?.type === 'default' && ! hasPostContentAtRootLevel - ? fallbackLayout - : blockListLayout; - - const observeTypingRef = useTypingObserver(); - const titleRef = useRef(); - useEffect( () => { - if ( isWelcomeGuideVisible || ! isCleanNewPost() ) { - return; - } - titleRef?.current?.focus(); - }, [ isWelcomeGuideVisible, isCleanNewPost ] ); - styles = useMemo( () => [ ...styles, { // We should move this in to future to the body. - css: - `.edit-post-visual-editor__post-title-wrapper{margin-top:4rem}` + - ( paddingBottom - ? `body{padding-bottom:${ paddingBottom }}` - : '' ), + css: paddingBottom + ? `body{padding-bottom:${ paddingBottom }}` + : '', }, ], [ styles ] ); - // Add some styles for alignwide/alignfull Post Content and its children. - const alignCSS = `.is-root-container.alignwide { max-width: var(--wp--style--global--wide-size); margin-left: auto; margin-right: auto;} - .is-root-container.alignwide:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: var(--wp--style--global--wide-size);} - .is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;} - .is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`; - const isToBeIframed = ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && ! hasMetaBoxes ) || @@ -336,6 +126,14 @@ export default function VisualEditor( { styles } ) { deviceType === 'Tablet' || deviceType === 'Mobile'; + useEffect( () => { + if ( isTemplateMode ) { + setRenderingMode( 'all' ); + } else { + setRenderingMode( 'post-only' ); + } + }, [ isTemplateMode, setRenderingMode ] ); + return ( - { themeSupportsLayout && - ! themeHasDisabledLayoutStyles && - ! isTemplateMode && ( - <> - - - { align && ( - - ) } - { postContentLayoutStyles && ( - - ) } - - ) } - { ! isTemplateMode && ( -
- -
- ) } - - - + diff --git a/packages/edit-post/src/components/visual-editor/style.scss b/packages/edit-post/src/components/visual-editor/style.scss index 237bbf25f2c79a..46838c97f8799c 100644 --- a/packages/edit-post/src/components/visual-editor/style.scss +++ b/packages/edit-post/src/components/visual-editor/style.scss @@ -38,21 +38,6 @@ // See also https://www.w3.org/TR/CSS22/visudet.html#the-height-property. } -// Ideally this wrapper div is not needed but if we want to match the positioning of blocks -// .block-editor-block-list__layout and block-editor-block-list__block -// We need to have two DOM elements. -.edit-post-visual-editor__post-title-wrapper { - .editor-post-title { - // Center. - margin-left: auto; - margin-right: auto; - } - - // Add extra margin at the top, to push down the Title area in the post editor. - margin-top: 4rem; - margin-bottom: var(--wp--style--block-gap); -} - .edit-post-visual-editor__content-area { width: 100%; height: 100%; diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index d1ef111e7dc4c3..cff867c3f7a2cb 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -36,13 +36,11 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hiddenBlockTypes, blockTypes, keepCaretInsideBlock, - isTemplateMode, template, } = useSelect( ( select ) => { const { isFeatureActive, - isEditingTemplate, getEditedPostTemplate, getHiddenBlockTypes, } = select( editPostStore ); @@ -81,7 +79,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hiddenBlockTypes: getHiddenBlockTypes(), blockTypes: getBlockTypes(), keepCaretInsideBlock: isFeatureActive( 'keepCaretInsideBlock' ), - isTemplateMode: isEditingTemplate(), template: supportsTemplateMode && isViewable && canEditTemplate ? getEditedPostTemplate() @@ -156,7 +153,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { post={ post } initialEdits={ initialEdits } useSubRegistry={ false } - __unstableTemplate={ isTemplateMode ? template : undefined } + __unstableTemplate={ template } { ...props } > diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 235eaf6617aa8f..15d638aa329e12 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -15,16 +15,22 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { useState, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies */ import { unlock } from '../../lock-unlock'; import { store as editSiteStore } from '../../store'; +import { + FOCUSABLE_ENTITIES, + NAVIGATION_POST_TYPE, +} from '../../utils/constants'; const { ExperimentalBlockCanvas: BlockCanvas } = unlock( blockEditorPrivateApis ); +const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); function EditorCanvas( { enableResizing, @@ -33,17 +39,32 @@ function EditorCanvas( { contentRef, ...props } ) { - const { canvasMode, deviceType, isZoomOutMode } = useSelect( - ( select ) => ( { - deviceType: - select( editSiteStore ).__experimentalGetPreviewDeviceType(), - isZoomOutMode: - select( blockEditorStore ).__unstableGetEditorMode() === - 'zoom-out', - canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), - } ), - [] - ); + const { + hasBlocks, + isFocusMode, + templateType, + canvasMode, + deviceType, + isZoomOutMode, + } = useSelect( ( select ) => { + const { getBlockCount, __unstableGetEditorMode } = + select( blockEditorStore ); + const { + getEditedPostType, + __experimentalGetPreviewDeviceType, + getCanvasMode, + } = unlock( select( editSiteStore ) ); + const _templateType = getEditedPostType(); + + return { + templateType: _templateType, + isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), + deviceType: __experimentalGetPreviewDeviceType(), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + canvasMode: getCanvasMode(), + hasBlocks: !! getBlockCount(), + }; + }, [] ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const deviceStyles = useResizeCanvas( deviceType ); const [ isFocused, setIsFocused ] = useState( false ); @@ -70,6 +91,15 @@ function EditorCanvas( { onClick: () => setCanvasMode( 'edit' ), readonly: true, }; + const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; + const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; + // Hide the appender when: + // - In navigation focus mode (should only allow the root Nav block). + // - In view mode (i.e. not editing). + const showBlockAppender = + ( isNavigationFocusMode && hasBlocks ) || canvasMode === 'view' + ? false + : undefined; return ( + { children } ); diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index 0d2d522c8b3e18..bfbb2d3eac43ff 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -7,12 +7,9 @@ import classnames from 'classnames'; */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRef } from '@wordpress/element'; -import { - BlockList, - BlockTools, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { BlockTools, store as blockEditorStore } from '@wordpress/block-editor'; import { useViewportMatch, useResizeObserver } from '@wordpress/compose'; + /** * Internal dependencies */ @@ -29,12 +26,6 @@ import { import { unlock } from '../../lock-unlock'; import PageContentFocusNotifications from '../page-content-focus-notifications'; -const LAYOUT = { - type: 'default', - // At the root level of the site editor, no alignments should be allowed. - alignments: [], -}; - export default function SiteEditorCanvas() { const { clearSelectedBlock } = useDispatch( blockEditorStore ); @@ -56,16 +47,6 @@ export default function SiteEditorCanvas() { const settings = useSiteEditorSettings(); - const { hasBlocks } = useSelect( ( select ) => { - const { getBlockCount } = select( blockEditorStore ); - - const blocks = getBlockCount(); - - return { - hasBlocks: !! blocks, - }; - }, [] ); - const isMobileViewport = useViewportMatch( 'small', '<' ); const enableResizing = isFocusMode && @@ -75,17 +56,7 @@ export default function SiteEditorCanvas() { const contentRef = useRef(); const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; - const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; - - // Hide the appender when: - // - In navigation focus mode (should only allow the root Nav block). - // - In view mode (i.e. not editing). - const showBlockAppender = - ( isNavigationFocusMode && hasBlocks ) || isViewMode - ? false - : undefined; - const forceFullHeight = isNavigationFocusMode; return ( @@ -126,23 +97,6 @@ export default function SiteEditorCanvas() { contentRef={ contentRef } > { resizeObserver } -
diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index dbc67049fcb869..e02240eb880992 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -14,7 +14,7 @@ // Navigation focus mode requires padding around the root Navigation block // for presentational purposes. -.edit-site-block-editor__block-list.is-navigation-block { +.edit-site-editor-canvas__block-list.is-navigation-block { padding: $grid-unit-30; } diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index cb3fb3f1cb3336..3cca41d67985c5 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -138,6 +138,7 @@ export function useSpecificEditorSettings() { return { ...settings, + supportsTemplateMode: true, __experimentalSetIsInserterOpened: setIsInserterOpened, focusMode: canvasMode === 'view' && focusMode ? false : focusMode, isDistractionFree, diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index df59dffe66be69..bbf4b55c052874 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -12,6 +12,7 @@ import { humanTimeDiff } from '@wordpress/date'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -22,28 +23,39 @@ import PageContent from './page-content'; import PageSummary from './page-summary'; export default function PagePanels() { - const { id, type, hasResolved, status, date, password, title, modified } = - useSelect( ( select ) => { - const { getEditedPostContext } = select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const context = getEditedPostContext(); - const queryArgs = [ 'postType', context.postType, context.postId ]; - const page = getEditedEntityRecord( ...queryArgs ); - return { - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - title: page?.title, - id: page?.id, - type: page?.type, - status: page?.status, - date: page?.date, - password: page?.password, - modified: page?.modified, - }; - }, [] ); + const { + id, + type, + hasResolved, + status, + date, + password, + title, + modified, + renderingMode, + } = useSelect( ( select ) => { + const { getEditedPostContext } = select( editSiteStore ); + const { getEditedEntityRecord, hasFinishedResolution } = + select( coreStore ); + const { getRenderingMode } = select( editorStore ); + const context = getEditedPostContext(); + const queryArgs = [ 'postType', context.postType, context.postId ]; + const page = getEditedEntityRecord( ...queryArgs ); + return { + hasResolved: hasFinishedResolution( + 'getEditedEntityRecord', + queryArgs + ), + title: page?.title, + id: page?.id, + type: page?.type, + status: page?.status, + date: page?.date, + password: page?.password, + modified: page?.modified, + renderingMode: getRenderingMode(), + }; + }, [] ); if ( ! hasResolved ) { return null; @@ -77,9 +89,11 @@ export default function PagePanels() { postType={ type } />
- - - + { renderingMode !== 'post-only' && ( + + + + ) } ); } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js new file mode 100644 index 00000000000000..906eb6b272fc78 --- /dev/null +++ b/packages/editor/src/components/editor-canvas/index.js @@ -0,0 +1,334 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + BlockList, + store as blockEditorStore, + __unstableUseTypingObserver as useTypingObserver, + useSettings, + __experimentalRecursionProvider as RecursionProvider, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { useEffect, useRef, useMemo } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { parse } from '@wordpress/blocks'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import PostTitle from '../post-title'; +import { store as editorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const { LayoutStyle, useLayoutClasses, useLayoutStyles } = unlock( + blockEditorPrivateApis +); + +/** + * Given an array of nested blocks, find the first Post Content + * block inside it, recursing through any nesting levels, + * and return its attributes. + * + * @param {Array} blocks A list of blocks. + * + * @return {Object | undefined} The Post Content block. + */ +function getPostContentAttributes( blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/post-content' ) { + return blocks[ i ].attributes; + } + if ( blocks[ i ].innerBlocks.length ) { + const nestedPostContent = getPostContentAttributes( + blocks[ i ].innerBlocks + ); + + if ( nestedPostContent ) { + return nestedPostContent; + } + } + } +} + +function checkForPostContentAtRootLevel( blocks ) { + for ( let i = 0; i < blocks.length; i++ ) { + if ( blocks[ i ].name === 'core/post-content' ) { + return true; + } + } + return false; +} + +export default function EditorCanvas( { + // Ideally as we unify post and site editors, we won't need these props. + autoFocus, + dropZoneElement, + className, + renderAppender, +} ) { + const { + renderingMode, + postContentAttributes, + editedPostTemplate = {}, + wrapperBlockName, + wrapperUniqueId, + } = useSelect( ( select ) => { + const { + getCurrentPostId, + getCurrentPostType, + getCurrentTemplateId, + getEditorSettings, + getRenderingMode, + } = select( editorStore ); + const postTypeSlug = getCurrentPostType(); + const _renderingMode = getRenderingMode(); + let _wrapperBlockName; + + if ( postTypeSlug === 'wp_block' ) { + _wrapperBlockName = 'core/block'; + } else if ( ! _renderingMode === 'post-only' ) { + _wrapperBlockName = 'core/post-content'; + } + + const editorSettings = getEditorSettings(); + const supportsTemplateMode = editorSettings.supportsTemplateMode; + const postType = select( coreStore ).getPostType( postTypeSlug ); + const canEditTemplate = select( coreStore ).canUser( + 'create', + 'templates' + ); + const currentTemplateId = getCurrentTemplateId(); + const template = currentTemplateId + ? select( coreStore ).getEditedEntityRecord( + 'postType', + 'wp_template', + currentTemplateId + ) + : undefined; + + return { + renderingMode: _renderingMode, + postContentAttributes: getEditorSettings().postContentAttributes, + // Post template fetch returns a 404 on classic themes, which + // messes with e2e tests, so check it's a block theme first. + editedPostTemplate: + postType?.viewable && supportsTemplateMode && canEditTemplate + ? template + : undefined, + wrapperBlockName: _wrapperBlockName, + wrapperUniqueId: getCurrentPostId(), + }; + }, [] ); + const { isCleanNewPost } = useSelect( editorStore ); + const { + hasRootPaddingAwareAlignments, + themeHasDisabledLayoutStyles, + themeSupportsLayout, + } = useSelect( ( select ) => { + const _settings = select( blockEditorStore ).getSettings(); + return { + themeHasDisabledLayoutStyles: _settings.disableLayoutStyles, + themeSupportsLayout: _settings.supportsLayout, + hasRootPaddingAwareAlignments: + _settings.__experimentalFeatures?.useRootPaddingAwareAlignments, + }; + }, [] ); + + const [ globalLayoutSettings ] = useSettings( 'layout' ); + + // fallbackLayout is used if there is no Post Content, + // and for Post Title. + const fallbackLayout = useMemo( () => { + if ( renderingMode !== 'post-only' ) { + return { type: 'default' }; + } + + if ( themeSupportsLayout ) { + // We need to ensure support for wide and full alignments, + // so we add the constrained type. + return { ...globalLayoutSettings, type: 'constrained' }; + } + // Set default layout for classic themes so all alignments are supported. + return { type: 'default' }; + }, [ renderingMode, themeSupportsLayout, globalLayoutSettings ] ); + + const newestPostContentAttributes = useMemo( () => { + if ( + ! editedPostTemplate?.content && + ! editedPostTemplate?.blocks && + postContentAttributes + ) { + return postContentAttributes; + } + // When in template editing mode, we can access the blocks directly. + if ( editedPostTemplate?.blocks ) { + return getPostContentAttributes( editedPostTemplate?.blocks ); + } + // If there are no blocks, we have to parse the content string. + // Best double-check it's a string otherwise the parse function gets unhappy. + const parseableContent = + typeof editedPostTemplate?.content === 'string' + ? editedPostTemplate?.content + : ''; + + return getPostContentAttributes( parse( parseableContent ) ) || {}; + }, [ + editedPostTemplate?.content, + editedPostTemplate?.blocks, + postContentAttributes, + ] ); + + const hasPostContentAtRootLevel = useMemo( () => { + if ( ! editedPostTemplate?.content && ! editedPostTemplate?.blocks ) { + return false; + } + // When in template editing mode, we can access the blocks directly. + if ( editedPostTemplate?.blocks ) { + return checkForPostContentAtRootLevel( editedPostTemplate?.blocks ); + } + // If there are no blocks, we have to parse the content string. + // Best double-check it's a string otherwise the parse function gets unhappy. + const parseableContent = + typeof editedPostTemplate?.content === 'string' + ? editedPostTemplate?.content + : ''; + + return ( + checkForPostContentAtRootLevel( parse( parseableContent ) ) || false + ); + }, [ editedPostTemplate?.content, editedPostTemplate?.blocks ] ); + + const { layout = {}, align = '' } = newestPostContentAttributes || {}; + + const postContentLayoutClasses = useLayoutClasses( + newestPostContentAttributes, + 'core/post-content' + ); + + const blockListLayoutClass = classnames( + { + 'is-layout-flow': ! themeSupportsLayout, + }, + themeSupportsLayout && postContentLayoutClasses, + align && `align${ align }` + ); + + const postContentLayoutStyles = useLayoutStyles( + newestPostContentAttributes, + 'core/post-content', + '.block-editor-block-list__layout.is-root-container' + ); + + // Update type for blocks using legacy layouts. + const postContentLayout = useMemo( () => { + return layout && + ( layout?.type === 'constrained' || + layout?.inherit || + layout?.contentSize || + layout?.wideSize ) + ? { ...globalLayoutSettings, ...layout, type: 'constrained' } + : { ...globalLayoutSettings, ...layout, type: 'default' }; + }, [ + layout?.type, + layout?.inherit, + layout?.contentSize, + layout?.wideSize, + globalLayoutSettings, + ] ); + + // If there is a Post Content block we use its layout for the block list; + // if not, this must be a classic theme, in which case we use the fallback layout. + const blockListLayout = postContentAttributes + ? postContentLayout + : fallbackLayout; + + const postEditorLayout = + blockListLayout?.type === 'default' && ! hasPostContentAtRootLevel + ? fallbackLayout + : blockListLayout; + + const observeTypingRef = useTypingObserver(); + const titleRef = useRef(); + useEffect( () => { + if ( ! autoFocus || ! isCleanNewPost() ) { + return; + } + titleRef?.current?.focus(); + }, [ autoFocus, isCleanNewPost ] ); + + // Add some styles for alignwide/alignfull Post Content and its children. + const alignCSS = `.is-root-container.alignwide { max-width: var(--wp--style--global--wide-size); margin-left: auto; margin-right: auto;} + .is-root-container.alignwide:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: var(--wp--style--global--wide-size);} + .is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;} + .is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`; + + return ( + <> + { themeSupportsLayout && + ! themeHasDisabledLayoutStyles && + renderingMode === 'post-only' && ( + <> + + + { align && } + { postContentLayoutStyles && ( + + ) } + + ) } + { renderingMode === 'post-only' && ( +
+ +
+ ) } + + + + + ); +} diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index dda536aec4f733..3bd5860501d4e0 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -23,7 +23,6 @@ import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; import { unlock } from '../../lock-unlock'; import DisableNonPageContentBlocks from './disable-non-page-content-blocks'; -import { PAGE_CONTENT_BLOCK_TYPES } from './constants'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); @@ -60,36 +59,6 @@ function useForceFocusModeForNavigation( navigationBlockClientId ) { ] ); } -/** - * Helper method to extract the post content block types from a template. - * - * @param {Array} blocks Template blocks. - * - * @return {Array} Flattened object. - */ -function extractPageContentBlockTypesFromTemplateBlocks( blocks ) { - const result = []; - for ( let i = 0; i < blocks.length; i++ ) { - // Since the Query Block could contain PAGE_CONTENT_BLOCK_TYPES block types, - // we skip it because we only want to render stand-alone page content blocks in the block list. - if ( blocks[ i ].name === 'core/query' ) { - continue; - } - if ( PAGE_CONTENT_BLOCK_TYPES.includes( blocks[ i ].name ) ) { - result.push( createBlock( blocks[ i ].name ) ); - } - if ( blocks[ i ].innerBlocks.length ) { - result.push( - ...extractPageContentBlockTypesFromTemplateBlocks( - blocks[ i ].innerBlocks - ) - ); - } - } - - return result; -} - /** * Depending on the post, template and template mode, * returns the appropriate blocks and change handlers for the block editor provider. @@ -125,36 +94,6 @@ function useBlockEditorProps( post, template, mode ) { } }, [ post.type, post.id ] ); - const maybePostOnlyBlocks = useMemo( () => { - if ( mode === 'post-only' ) { - const postContentBlocks = - extractPageContentBlockTypesFromTemplateBlocks( - templateBlocks - ); - return [ - createBlock( - 'core/group', - { - layout: { type: 'constrained' }, - style: { - spacing: { - margin: { - top: '4em', // Mimics the post editor. - }, - }, - }, - }, - postContentBlocks.length - ? postContentBlocks - : [ - createBlock( 'core/post-title' ), - createBlock( 'core/post-content' ), - ] - ), - ]; - } - }, [ templateBlocks, mode ] ); - // It is important that we don't create a new instance of blocks on every change // We should only create a new instance if the blocks them selves change, not a dependency of them. const blocks = useMemo( () => { @@ -162,30 +101,19 @@ function useBlockEditorProps( post, template, mode ) { return maybeNavigationBlocks; } - if ( maybePostOnlyBlocks ) { - return maybePostOnlyBlocks; - } - if ( rootLevelPost === 'template' ) { return templateBlocks; } return postBlocks; - }, [ - maybeNavigationBlocks, - maybePostOnlyBlocks, - rootLevelPost, - templateBlocks, - postBlocks, - ] ); + }, [ maybeNavigationBlocks, rootLevelPost, templateBlocks, postBlocks ] ); // Handle fallback to postBlocks outside of the above useMemo, to ensure // that constructed block templates that call `createBlock` are not generated // too frequently. This ensures that clientIds are stable. const disableRootLevelChanges = ( !! template && mode === 'template-locked' ) || - post.type === 'wp_navigation' || - mode === 'post-only'; + post.type === 'wp_navigation'; const navigationBlockClientId = post.type === 'wp_navigation' && blocks && blocks[ 0 ]?.clientId; useForceFocusModeForNavigation( navigationBlockClientId ); @@ -270,7 +198,8 @@ export const ExperimentalEditorProvider = withRegistryProvider( setupEditor, updateEditorSettings, __experimentalTearDownEditor, - } = useDispatch( editorStore ); + setCurrentTemplateId, + } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); // Initialize and tear down the editor. @@ -310,6 +239,10 @@ export const ExperimentalEditorProvider = withRegistryProvider( updateEditorSettings( settings ); }, [ settings, updateEditorSettings ] ); + useEffect( () => { + setCurrentTemplateId( template?.id ); + }, [ template?.id, setCurrentTemplateId ] ); + if ( ! isReady ) { return null; } @@ -332,9 +265,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( > { children } - { [ 'post-only', 'template-locked' ].includes( - mode - ) && } + { mode === 'template-locked' && ( + + ) } diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index a44720eb93ac83..046feee5b9c3f6 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -1,6 +1,7 @@ /** * Internal dependencies */ +import EditorCanvas from './components/editor-canvas'; import { ExperimentalEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; @@ -9,6 +10,7 @@ import PostPanelRow from './components/post-panel-row'; export const privateApis = {}; lock( privateApis, { + EditorCanvas, ExperimentalEditorProvider, EntitiesSavedStatesExtensible, PostPanelRow, diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 0c946d4124f49f..4c1170b064202f 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -560,8 +560,12 @@ export function updateEditorSettings( settings ) { */ export const setRenderingMode = ( mode ) => - ( { dispatch, registry } ) => { - registry.dispatch( blockEditorStore ).clearSelectedBlock(); + ( { dispatch, registry, select } ) => { + if ( select.__unstableIsEditorReady() ) { + // We clear the block selection but we also need to clear the selection from the core store. + registry.dispatch( blockEditorStore ).clearSelectedBlock(); + dispatch.editPost( { selection: undefined }, { undoIgnore: true } ); + } dispatch( { type: 'SET_RENDERING_MODE', diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index baee4d9197d0c2..ebd41354308e7a 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -9,7 +9,9 @@ import { createReduxStore, register } from '@wordpress/data'; import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; +import * as privateActions from './private-actions'; import { STORE_NAME } from './constants'; +import { unlock } from '../lock-unlock'; /** * Post editor data store configuration. @@ -36,3 +38,4 @@ export const store = createReduxStore( STORE_NAME, { } ); register( store ); +unlock( store ).registerPrivateActions( privateActions ); diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js new file mode 100644 index 00000000000000..1af9ff5f6adb92 --- /dev/null +++ b/packages/editor/src/store/private-actions.js @@ -0,0 +1,13 @@ +/** + * Returns an action object used to set which template is currently being used/edited. + * + * @param {string} id Template Id. + * + * @return {Object} Action object. + */ +export function setCurrentTemplateId( id ) { + return { + type: 'SET_CURRENT_TEMPLATE_ID', + id, + }; +} diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 48356fd8e99e3c..7821baf5cdc062 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -90,6 +90,15 @@ export function postId( state = null, action ) { return state; } +export function templateId( state = null, action ) { + switch ( action.type ) { + case 'SET_CURRENT_TEMPLATE_ID': + return action.id; + } + + return state; +} + export function postType( state = null, action ) { switch ( action.type ) { case 'SETUP_EDITOR_STATE': @@ -291,6 +300,7 @@ export function renderingMode( state = 'all', action ) { export default combineReducers( { postId, postType, + templateId, saving, deleting, postLock, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 7aaa2f970a5241..c47a80e96735f5 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -205,6 +205,17 @@ export function getCurrentPostId( state ) { return state.postId; } +/** + * Returns the template ID currently being rendered/edited + * + * @param {Object} state Global application state. + * + * @return {string?} Template ID. + */ +export function getCurrentTemplateId( state ) { + return state.templateId; +} + /** * Returns the number of revisions of the post currently being edited. * diff --git a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css index f8f2e8fe2b4cde..b0ce8f3dc948d5 100644 --- a/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css +++ b/packages/react-native-bridge/common/gutenberg-web-single-block/editor-style-overrides.css @@ -8,7 +8,7 @@ display: none; } -.edit-post-visual-editor__post-title-wrapper { +.editor-editor-canvas__post-title-wrapper { display: none; } diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index af58daeaedbe40..4d40223e0a99c4 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -215,24 +215,13 @@ test.describe( 'Pages', () => { } ) ).toBeHidden(); - // Content blocks are wrapped in a Group block by default. + // Ensure post title component to be visible. await expect( - editor.canvas - .getByRole( 'document', { - name: 'Block: Group', - } ) - .getByRole( 'document', { - name: 'Block: Content', - } ) + editor.canvas.getByRole( 'textbox', { + name: 'Add Title', + } ) ).toBeVisible(); - // Ensure order is preserved between toggling. - await page - .locator( - '[aria-label="Block: Content"] + [aria-label="Block: Title"]' - ) - .isVisible(); - // Remove focus from templateOptionsButton button. await editor.canvas.locator( 'body' ).click(); From 96305e952948653e2921147492556d09ee9d3c17 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 5 Dec 2023 09:18:06 +0200 Subject: [PATCH 023/325] DataViews: Add story (#56761) --- packages/dataviews/src/stories/fixtures.js | 126 ++++++++++++++++ packages/dataviews/src/stories/index.story.js | 137 ++++++++++++++++++ packages/dataviews/src/style.scss | 3 +- packages/dataviews/src/view-grid.js | 7 +- storybook/main.js | 1 + storybook/package-styles/config.js | 7 + .../package-styles/dataviews-ltr.lazy.scss | 1 + .../package-styles/dataviews-rtl.lazy.scss | 1 + 8 files changed, 280 insertions(+), 3 deletions(-) create mode 100644 packages/dataviews/src/stories/fixtures.js create mode 100644 packages/dataviews/src/stories/index.story.js create mode 100644 storybook/package-styles/dataviews-ltr.lazy.scss create mode 100644 storybook/package-styles/dataviews-rtl.lazy.scss diff --git a/packages/dataviews/src/stories/fixtures.js b/packages/dataviews/src/stories/fixtures.js new file mode 100644 index 00000000000000..6b9073e2cc78da --- /dev/null +++ b/packages/dataviews/src/stories/fixtures.js @@ -0,0 +1,126 @@ +/** + * WordPress dependencies + */ +import { trash } from '@wordpress/icons'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; + +/** + * Internal dependencies + */ +import { LAYOUT_TABLE } from '../constants'; + +export const data = [ + { + id: 1, + title: 'Apollo', + description: 'Apollo description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 2, + title: 'Space', + description: 'Space description', + image: 'https://live.staticflickr.com/5678/21911065441_92e2d44708_b.jpg', + }, + { + id: 3, + title: 'NASA', + description: 'NASA photo', + image: 'https://live.staticflickr.com/742/21712365770_8f70a2c91e_b.jpg', + }, + { + id: 4, + title: 'Neptune', + description: 'Neptune description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 5, + title: 'Mercury', + description: 'Mercury description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 6, + title: 'Venus', + description: 'Venus description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 7, + title: 'Earth', + description: 'Earth description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 8, + title: 'Mars', + description: 'Mars description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 9, + title: 'Jupiter', + description: 'Jupiter description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 10, + title: 'Saturn', + description: 'Saturn description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, + { + id: 11, + title: 'Uranus', + description: 'Uranus description', + image: 'https://live.staticflickr.com/5725/21726228300_51333bd62c_b.jpg', + }, +]; + +export const DEFAULT_VIEW = { + type: LAYOUT_TABLE, + search: '', + page: 1, + perPage: 10, + hiddenFields: [ 'image' ], + layout: {}, + filters: [], +}; + +export const actions = [ + { + id: 'delete', + label: 'Delete item', + isPrimary: true, + icon: trash, + hideModalHeader: true, + RenderModal: ( { item, closeModal } ) => { + return ( + + + { `Are you sure you want to delete "${ item.title }"?` } + + + + + + + ); + }, + }, + { + id: 'secondary', + label: 'Secondary action', + callback() {}, + }, +]; diff --git a/packages/dataviews/src/stories/index.story.js b/packages/dataviews/src/stories/index.story.js new file mode 100644 index 00000000000000..e0bea0c92c2b21 --- /dev/null +++ b/packages/dataviews/src/stories/index.story.js @@ -0,0 +1,137 @@ +/** + * WordPress dependencies + */ +import { useState, useMemo, useCallback } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { DataViews, LAYOUT_GRID, LAYOUT_TABLE } from '../index'; + +import { DEFAULT_VIEW, actions, data } from './fixtures'; + +const meta = { + title: 'DataViews (Experimental)/DataViews', + component: DataViews, +}; +export default meta; + +const defaultConfigPerViewType = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: { + mediaField: 'image', + primaryField: 'title', + }, +}; + +function normalizeSearchInput( input = '' ) { + return input.trim().toLowerCase(); +} + +const fields = [ + { + header: 'Image', + id: 'image', + render: ( { item } ) => { + return ( + + ); + }, + width: 50, + enableSorting: false, + }, + { + header: 'Title', + id: 'title', + getValue: ( { item } ) => item.title, + maxWidth: 400, + enableHiding: false, + }, + { + header: 'Description', + id: 'description', + getValue: ( { item } ) => item.description, + maxWidth: 200, + enableSorting: false, + }, +]; + +export const Default = ( props ) => { + const [ view, setView ] = useState( DEFAULT_VIEW ); + const { shownData, paginationInfo } = useMemo( () => { + let filteredData = [ ...data ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredData = filteredData.filter( ( item ) => { + return [ + normalizeSearchInput( item.title ), + normalizeSearchInput( item.description ), + ].some( ( field ) => field.includes( normalizedSearch ) ); + } ); + } + // Handle sorting. + if ( view.sort ) { + const stringSortingFields = [ 'title' ]; + const fieldId = view.sort.field; + if ( stringSortingFields.includes( fieldId ) ) { + const fieldToSort = fields.find( ( field ) => { + return field.id === fieldId; + } ); + filteredData.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + } + // Handle pagination. + const start = ( view.page - 1 ) * view.perPage; + const totalItems = filteredData?.length || 0; + filteredData = filteredData?.slice( start, start + view.perPage ); + return { + shownData: filteredData, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; + }, [ view ] ); + const onChangeView = useCallback( + ( viewUpdater ) => { + let updatedView = + typeof viewUpdater === 'function' + ? viewUpdater( view ) + : viewUpdater; + if ( updatedView.type !== view.type ) { + updatedView = { + ...updatedView, + layout: { + ...defaultConfigPerViewType[ updatedView.type ], + }, + }; + } + + setView( updatedView ); + }, + [ view, setView ] + ); + return ( + + ); +}; +Default.args = { + actions, + getItemId: ( item ) => item.id, + isLoading: false, + supportedLayouts: [ LAYOUT_TABLE, LAYOUT_GRID ], +}; diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index b18ddb4075c72f..5dd89f4c279707 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -3,6 +3,7 @@ height: calc(100% - #{$grid-unit-40} * 2); overflow: auto; padding: $grid-unit-40 $grid-unit-40 0; + box-sizing: border-box; > div { min-height: 100%; @@ -100,7 +101,7 @@ } } - .dataviews-view-grid__title { + .dataviews-view-grid__primary-field { min-height: $grid-unit-30; a { diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 8a39bdd8353d1a..89dd7d393430d9 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { + FlexBlock, __experimentalGrid as Grid, __experimentalHStack as HStack, __experimentalVStack as VStack, @@ -45,10 +46,12 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { { mediaField?.render( { item, view } ) } - { primaryField?.render( { item, view } ) } + + { primaryField?.render( { item, view } ) } + Date: Tue, 5 Dec 2023 08:43:11 +0100 Subject: [PATCH 024/325] Use tooltip for the Timezone only when necessary. (#56214) * Use tooltip for the Timezone only when necessary. * Add changelog entry. * Improve timezone detail check. --- packages/components/CHANGELOG.md | 1 + .../components/src/date-time/time/timezone.tsx | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 3b22c45f919934..f3019c250a6c42 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -22,6 +22,7 @@ ### Bug Fix +- `DateTime`: Make the Timezone indication render a tooltip only when necessary. ([#56214](https://github.com/WordPress/gutenberg/pull/56214)). - `ToolsPanelItem`: Use useLayoutEffect to prevent rendering glitch for last panel item styling. ([#56536](https://github.com/WordPress/gutenberg/pull/56536)). - `FormTokenField`: Fix broken suggestions scrollbar when the `__experimentalExpandOnFocus` prop is defined ([#56426](https://github.com/WordPress/gutenberg/pull/56426)). - `FormTokenField`: `onFocus` prop is now typed as a React `FocusEvent` ([#56426](https://github.com/WordPress/gutenberg/pull/56426)). diff --git a/packages/components/src/date-time/time/timezone.tsx b/packages/components/src/date-time/time/timezone.tsx index 9fac1ec094ed89..9b08eac6307aa7 100644 --- a/packages/components/src/date-time/time/timezone.tsx +++ b/packages/components/src/date-time/time/timezone.tsx @@ -32,12 +32,24 @@ const TimeZone = () => { ? timezone.abbr : `UTC${ offsetSymbol }${ timezone.offset }`; + // Replace underscore with space in strings like `America/Costa_Rica`. + const prettyTimezoneString = timezone.string.replace( '_', ' ' ); + const timezoneDetail = 'UTC' === timezone.string ? __( 'Coordinated Universal Time' ) - : `(${ zoneAbbr }) ${ timezone.string.replace( '_', ' ' ) }`; - - return ( + : `(${ zoneAbbr }) ${ prettyTimezoneString }`; + + // When the prettyTimezoneString is empty, there is no additional timezone + // detail information to show in a Tooltip. + const hasNoAdditionalTimezoneDetail = + prettyTimezoneString.trim().length === 0; + + return hasNoAdditionalTimezoneDetail ? ( + + { zoneAbbr } + + ) : ( { zoneAbbr } From 86bd4514d9bf49b939d3e477378db40586541ae1 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras Date: Tue, 5 Dec 2023 10:14:35 +0200 Subject: [PATCH 025/325] DataViews: Fix dropdown menu actions with modal (#56760) --- packages/dataviews/src/item-actions.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js index 267ed3f07e856b..1b0bd5f213ca8e 100644 --- a/packages/dataviews/src/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -37,7 +37,10 @@ function ButtonTrigger( { action, onClick } ) { function DropdownMenuItemTrigger( { action, onClick } ) { return ( - + { action.label } ); From 6305cf7397327a4377cc09721fbebfc03eeaa66a Mon Sep 17 00:00:00 2001 From: Riad Benguella Date: Tue, 5 Dec 2023 09:23:54 +0100 Subject: [PATCH 026/325] Post Editor: Rely on the editor store for the template mode state (#56716) --- .../data/data-core-edit-post.md | 20 +++---------- .../header/document-actions/index.js | 5 ++-- .../edit-post/src/components/header/index.js | 9 ++++-- .../components/header/mode-switcher/index.js | 3 +- .../edit-post/src/components/layout/index.js | 3 +- .../sidebar/settings-header/index.js | 6 ++-- .../sidebar/settings-sidebar/index.js | 4 ++- .../components/start-page-options/index.js | 6 ++-- .../src/components/visual-editor/index.js | 25 ++++------------- .../src/components/welcome-guide/index.js | 6 ++-- packages/edit-post/src/editor.js | 8 +++++- packages/edit-post/src/index.js | 5 ++-- .../plugins/welcome-guide-menu-item/index.js | 8 ++---- packages/edit-post/src/store/actions.js | 18 ++++++------ packages/edit-post/src/store/reducer.js | 15 ---------- packages/edit-post/src/store/selectors.js | 14 ++++++---- packages/edit-post/src/store/test/actions.js | 28 ------------------- 17 files changed, 66 insertions(+), 117 deletions(-) diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md index 7d6a1deed455b5..e09cf0caaec515 100644 --- a/docs/reference-guides/data/data-core-edit-post.md +++ b/docs/reference-guides/data/data-core-edit-post.md @@ -138,15 +138,9 @@ _Returns_ ### isEditingTemplate -Returns true if the template editing mode is enabled. - -_Parameters_ - -- _state_ `Object`: Global application state. - -_Returns_ +> **Deprecated** -- `boolean`: Whether we're editing the template. +Returns true if the template editing mode is enabled. ### isEditorPanelEnabled @@ -438,15 +432,9 @@ _Parameters_ ### setIsEditingTemplate -Returns an action object used to switch to template editing. - -_Parameters_ - -- _value_ `boolean`: Is editing template. +> **Deprecated** -_Returns_ - -- `Object`: Action object. +Returns an action object used to switch to template editing. ### setIsInserterOpened diff --git a/packages/edit-post/src/components/header/document-actions/index.js b/packages/edit-post/src/components/header/document-actions/index.js index 105b31e3122ac9..5ce58f179f3ab3 100644 --- a/packages/edit-post/src/components/header/document-actions/index.js +++ b/packages/edit-post/src/components/header/document-actions/index.js @@ -13,6 +13,7 @@ import { import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons'; import { store as commandsStore } from '@wordpress/commands'; import { displayShortcut } from '@wordpress/keycodes'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -28,7 +29,7 @@ function DocumentActions() { }; }, [] ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setIsEditingTemplate } = useDispatch( editPostStore ); + const { setRenderingMode } = useDispatch( editorStore ); const { open: openCommandCenter } = useDispatch( commandsStore ); if ( ! template ) { @@ -48,7 +49,7 @@ function DocumentActions() { className="edit-post-document-actions__back" onClick={ () => { clearSelectedBlock(); - setIsEditingTemplate( false ); + setRenderingMode( 'post-only' ); } } icon={ isRTL() ? chevronRightSmall : chevronLeftSmall } > diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index c1c8222394979d..8ac8e47e01dbaa 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -10,7 +10,11 @@ import { privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; -import { PostSavedState, PostPreviewButton } from '@wordpress/editor'; +import { + PostSavedState, + PostPreviewButton, + store as editorStore, +} from '@wordpress/editor'; import { useEffect, useRef, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -73,7 +77,8 @@ function Header( { blockSelectionStart: select( blockEditorStore ).getBlockSelectionStart(), hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), - isEditingTemplate: select( editPostStore ).isEditingTemplate(), + isEditingTemplate: + select( editorStore ).getRenderingMode() !== 'post-only', isPublishSidebarOpened: select( editPostStore ).isPublishSidebarOpened(), hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ), diff --git a/packages/edit-post/src/components/header/mode-switcher/index.js b/packages/edit-post/src/components/header/mode-switcher/index.js index b8d7f912180b60..93c4ead745ffb4 100644 --- a/packages/edit-post/src/components/header/mode-switcher/index.js +++ b/packages/edit-post/src/components/header/mode-switcher/index.js @@ -44,7 +44,8 @@ function ModeSwitcher() { select( editorStore ).getEditorSettings().richEditingEnabled, isCodeEditingEnabled: select( editorStore ).getEditorSettings().codeEditingEnabled, - isEditingTemplate: select( editPostStore ).isEditingTemplate(), + isEditingTemplate: + select( editorStore ).getRenderingMode() !== 'post-only', mode: select( editPostStore ).getEditorMode(), } ), [] diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 9c854dc33636db..bbf33613cb4241 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -165,7 +165,8 @@ function Layout() { const postTypeLabel = getPostTypeLabel(); return { - isTemplateMode: select( editPostStore ).isEditingTemplate(), + isTemplateMode: + select( editorStore ).getRenderingMode() !== 'post-only', hasFixedToolbar: select( editPostStore ).isFeatureActive( 'fixedToolbar' ), sidebarIsOpened: !! ( diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index f4f7a34db0bc3d..fb2bf8f486f51c 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -18,12 +18,12 @@ const SettingsHeader = ( { sidebarName } ) => { const openBlockSettings = () => openGeneralSidebar( 'edit-post/block' ); const { documentLabel, isTemplateMode } = useSelect( ( select ) => { - const postTypeLabel = select( editorStore ).getPostTypeLabel(); + const { getPostTypeLabel, getRenderingMode } = select( editorStore ); return { // translators: Default label for the Document sidebar tab, not selected. - documentLabel: postTypeLabel || _x( 'Document', 'noun' ), - isTemplateMode: select( editPostStore ).isEditingTemplate(), + documentLabel: getPostTypeLabel() || _x( 'Document', 'noun' ), + isTemplateMode: getRenderingMode() !== 'post-only', }; }, [] ); diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 8450fec0225932..42ad255d79e053 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -11,6 +11,7 @@ import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -63,7 +64,8 @@ const SettingsSidebar = () => { return { sidebarName: sidebar, keyboardShortcut: shortcut, - isTemplateMode: select( editPostStore ).isEditingTemplate(), + isTemplateMode: + select( editorStore ).getRenderingMode() !== 'post-only', }; }, [] diff --git a/packages/edit-post/src/components/start-page-options/index.js b/packages/edit-post/src/components/start-page-options/index.js index 77264d27a5e7df..0ef3e166e8ee1a 100644 --- a/packages/edit-post/src/components/start-page-options/index.js +++ b/packages/edit-post/src/components/start-page-options/index.js @@ -90,11 +90,11 @@ function StartPageOptionsModal( { onClose } ) { export default function StartPageOptions() { const [ isClosed, setIsClosed ] = useState( false ); const shouldEnableModal = useSelect( ( select ) => { - const { isCleanNewPost } = select( editorStore ); - const { isEditingTemplate, isFeatureActive } = select( editPostStore ); + const { isCleanNewPost, getRenderingMode } = select( editorStore ); + const { isFeatureActive } = select( editPostStore ); return ( - ! isEditingTemplate() && + getRenderingMode() === 'post-only' && ! isFeatureActive( 'welcomeGuide' ) && isCleanNewPost() ); diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 5b9290fff51375..3c5ba8d0373049 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -16,9 +16,9 @@ import { __experimentalUseResizeCanvas as useResizeCanvas, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useRef, useMemo, useEffect } from '@wordpress/element'; +import { useRef, useMemo } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { useMergeRefs } from '@wordpress/compose'; import { store as blocksStore } from '@wordpress/blocks'; @@ -43,20 +43,16 @@ export default function VisualEditor( { styles } ) { isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { - const { - isFeatureActive, - isEditingTemplate, - __experimentalGetPreviewDeviceType, - } = select( editPostStore ); - const { getEditorSettings } = select( editorStore ); + const { isFeatureActive, __experimentalGetPreviewDeviceType } = + select( editPostStore ); + const { getEditorSettings, getRenderingMode } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); - const _isTemplateMode = isEditingTemplate(); const editorSettings = getEditorSettings(); return { deviceType: __experimentalGetPreviewDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), - isTemplateMode: _isTemplateMode, + isTemplateMode: getRenderingMode() !== 'post-only', isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, hasV3BlocksOnly: getBlockTypes().every( ( type ) => { return type.apiVersion >= 3; @@ -67,7 +63,6 @@ export default function VisualEditor( { styles } ) { ( select ) => select( editPostStore ).hasMetaBoxes(), [] ); - const { setRenderingMode } = useDispatch( editorStore ); const desktopCanvasStyles = { height: '100%', width: '100%', @@ -126,14 +121,6 @@ export default function VisualEditor( { styles } ) { deviceType === 'Tablet' || deviceType === 'Mobile'; - useEffect( () => { - if ( isTemplateMode ) { - setRenderingMode( 'all' ); - } else { - setRenderingMode( 'post-only' ); - } - }, [ isTemplateMode, setRenderingMode ] ); - return ( { - const { isFeatureActive, isEditingTemplate } = select( editPostStore ); - const _isTemplateMode = isEditingTemplate(); + const { isFeatureActive } = select( editPostStore ); + const { getRenderingMode } = select( editorStore ); + const _isTemplateMode = getRenderingMode() !== 'post-only'; const feature = _isTemplateMode ? 'welcomeGuideTemplate' : 'welcomeGuide'; diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index cff867c3f7a2cb..2394ebb3a3a742 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -9,7 +9,7 @@ import { store as editorStore, privateApis as editorPrivateApis, } from '@wordpress/editor'; -import { useMemo } from '@wordpress/element'; +import { useEffect, useMemo } from '@wordpress/element'; import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -142,6 +142,12 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { keepCaretInsideBlock, ] ); + // The default mode of the post editor is "post-only" mode. + const { setRenderingMode } = useDispatch( editorStore ); + useEffect( () => { + setRenderingMode( 'post-only' ); + }, [ setRenderingMode ] ); + if ( ! post ) { return null; } diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index fe967eeeed337f..64ddf1d9fb08ba 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -15,6 +15,7 @@ import { registerLegacyWidgetBlock, registerWidgetGroupBlock, } from '@wordpress/widgets'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -93,7 +94,7 @@ export function initializeEditor( 'removeTemplatePartsFromInserter', ( canInsert, blockType ) => { if ( - ! select( editPostStore ).isEditingTemplate() && + select( editorStore ).getRenderingMode() === 'post-only' && blockType.name === 'core/template-part' ) { return false; @@ -118,7 +119,7 @@ export function initializeEditor( { getBlockParentsByBlockName } ) => { if ( - ! select( editPostStore ).isEditingTemplate() && + select( editorStore ).getRenderingMode() === 'post-only' && blockType.name === 'core/post-content' ) { return ( diff --git a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js index 24eec1114a0a7d..394e148603eb0a 100644 --- a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js +++ b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js @@ -4,15 +4,11 @@ import { useSelect } from '@wordpress/data'; import { PreferenceToggleMenuItem } from '@wordpress/preferences'; import { __ } from '@wordpress/i18n'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../store'; +import { store as editorStore } from '@wordpress/editor'; export default function WelcomeGuideMenuItem() { const isTemplateMode = useSelect( - ( select ) => select( editPostStore ).isEditingTemplate(), + ( select ) => select( editorStore ).getRenderingMode() !== 'post-only', [] ); diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 27e45ab0edf9d6..0380b0f7e98f33 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -513,14 +513,14 @@ export const setIsListViewOpened = /** * Returns an action object used to switch to template editing. * - * @param {boolean} value Is editing template. - * @return {Object} Action object. + * @deprecated */ -export function setIsEditingTemplate( value ) { - return { - type: 'SET_IS_EDITING_TEMPLATE', - value, - }; +export function setIsEditingTemplate() { + deprecated( "dispatch( 'core/edit-post' ).setIsEditingTemplate", { + since: '6.5', + alternative: "dispatch( 'core/editor').setRenderingMode", + } ); + return { type: 'NOTHING' }; } /** @@ -530,8 +530,8 @@ export function setIsEditingTemplate( value ) { */ export const __unstableSwitchToTemplateMode = ( newTemplate = false ) => - ( { registry, select, dispatch } ) => { - dispatch( setIsEditingTemplate( true ) ); + ( { registry, select } ) => { + registry.dispatch( editorStore ).setRenderingMode( 'all' ); const isWelcomeGuideActive = select.isFeatureActive( 'welcomeGuideTemplate' ); diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 622b2e2667f7fc..4748c4fff49723 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -153,20 +153,6 @@ export function listViewPanel( state = false, action ) { return state; } -/** - * Reducer tracking whether template editing is on or off. - * - * @param {boolean} state - * @param {Object} action - */ -function isEditingTemplate( state = false, action ) { - switch ( action.type ) { - case 'SET_IS_EDITING_TEMPLATE': - return action.value; - } - return state; -} - /** * Reducer tracking whether meta boxes are initialized. * @@ -196,5 +182,4 @@ export default combineReducers( { deviceType, blockInserterPanel, listViewPanel, - isEditingTemplate, } ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index 570c03d930a7ec..f7b8f91d380dc0 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -498,13 +498,15 @@ export function isListViewOpened( state ) { /** * Returns true if the template editing mode is enabled. * - * @param {Object} state Global application state. - * - * @return {boolean} Whether we're editing the template. + * @deprecated */ -export function isEditingTemplate( state ) { - return state.isEditingTemplate; -} +export const isEditingTemplate = createRegistrySelector( ( select ) => () => { + deprecated( `select( 'core/edit-post' ).isEditingTemplate`, { + since: '6.5', + alternative: `select( 'core/editor' ).getRenderingMode`, + } ); + return select( editorStore ).getRenderingMode() !== 'post-only'; +} ); /** * Returns true if meta boxes are initialized. diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 76f527935cdf3e..39b889f2fdcbc0 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -146,34 +146,6 @@ describe( 'actions', () => { ).toBe( true ); } ); - describe( '__unstableSwitchToTemplateMode', () => { - it( 'welcome guide is active', () => { - // Activate `welcomeGuideTemplate` feature. - registry - .dispatch( editPostStore ) - .toggleFeature( 'welcomeGuideTemplate' ); - registry.dispatch( editPostStore ).__unstableSwitchToTemplateMode(); - expect( - registry.select( editPostStore ).isEditingTemplate() - ).toBeTruthy(); - const notices = registry.select( noticesStore ).getNotices(); - expect( notices ).toHaveLength( 0 ); - } ); - - it( 'welcome guide is inactive', () => { - expect( - registry.select( editPostStore ).isEditingTemplate() - ).toBeFalsy(); - registry.dispatch( editPostStore ).__unstableSwitchToTemplateMode(); - expect( - registry.select( editPostStore ).isEditingTemplate() - ).toBeTruthy(); - const notices = registry.select( noticesStore ).getNotices(); - expect( notices ).toHaveLength( 1 ); - expect( notices[ 0 ].content ).toMatch( 'template' ); - } ); - } ); - describe( 'hideBlockTypes', () => { it( 'adds the hidden block type to the preferences', () => { registry From 308fc423b26de1415ccf29de0aec8d4b088286e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 5 Dec 2023 09:54:49 +0100 Subject: [PATCH 027/325] DataViews: implement `NOT IN` operator for author filter in templates (#56777) --- .../src/components/page-templates/dataviews-templates.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 183d85ce40797b..0556efa5e63799 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -35,6 +35,7 @@ import { TEMPLATE_POST_TYPE, ENUMERATION_TYPE, OPERATOR_IN, + OPERATOR_NOT_IN, LAYOUT_GRID, LAYOUT_TABLE, } from '../../utils/constants'; @@ -266,6 +267,14 @@ export default function DataviewsTemplates() { filteredTemplates = filteredTemplates.filter( ( item ) => { return item.author_text === filter.value; } ); + } else if ( + filter.field === 'author' && + filter.operator === OPERATOR_NOT_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text !== filter.value; + } ); } } ); } From 19f944db51b5c2f4ef06ba60377e1cb9e3201abd Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 5 Dec 2023 13:18:32 +0200 Subject: [PATCH 028/325] Perf: reopen inspector for remaining tests (#56780) --- test/performance/specs/post-editor.spec.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/performance/specs/post-editor.spec.js b/test/performance/specs/post-editor.spec.js index 554b0dc71283e6..cf2610baa1f9e7 100644 --- a/test/performance/specs/post-editor.spec.js +++ b/test/performance/specs/post-editor.spec.js @@ -159,7 +159,13 @@ test.describe( 'Post Editor Performance', () => { draftId = await perfUtils.saveDraft(); } ); - test( 'Run the test', async ( { admin, perfUtils, metrics, page } ) => { + test( 'Run the test', async ( { + admin, + perfUtils, + metrics, + page, + editor, + } ) => { await admin.editPost( draftId ); await perfUtils.disableAutosave(); const toggleButton = page @@ -173,6 +179,9 @@ test.describe( 'Post Editor Performance', () => { } ); await type( paragraph, metrics, 'typeWithoutInspector' ); + + // Open the inspector again. + await editor.openDocumentSettingsSidebar(); } ); } ); From c362e6276e858ee1d36a6e3fbbe26d67939e722e Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 5 Dec 2023 22:08:06 +0900 Subject: [PATCH 029/325] wp-env: Make env-cwd option work on Windows (#56265) * Gutenberg Project: Make env-cwd option work on Windows * Remove only the first and last space and single quote * Move the logic to trim into the `run()` function * Move the logic to trim into the spawnCommandDirectly() function * Add comment --- packages/env/lib/commands/run.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/env/lib/commands/run.js b/packages/env/lib/commands/run.js index def29b6523139d..734d90603c3919 100644 --- a/packages/env/lib/commands/run.js +++ b/packages/env/lib/commands/run.js @@ -74,7 +74,9 @@ function spawnCommandDirectly( config, container, command, envCwd, spinner ) { container === 'mysql' || container === 'tests-mysql' ? '/' : '/var/www/html', - envCwd + // Remove spaces and single quotes from both ends of the path. + // This is needed because Windows treats single quotes as a literal character. + envCwd.trim().replace( /^'|'$/g, '' ) ); const composeCommand = [ From 88da5577d76b2401504bfefa7365b1d3eed2863c Mon Sep 17 00:00:00 2001 From: Marin Atanasov <8436925+tyxla@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:45:01 +0200 Subject: [PATCH 030/325] RN: Add watch mode for native tests (#56788) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index e7514660813d9b..8f6a3baeeb283f 100644 --- a/package.json +++ b/package.json @@ -322,6 +322,7 @@ "test:e2e:storybook": "playwright test --config test/storybook-playwright/playwright.config.ts", "test:e2e:watch": "npm run test:e2e -- --watch", "test:native": "cross-env NODE_ENV=test jest --config test/native/jest.config.js", + "test:native:watch": "npm run test:native -- --watch", "test:native:clean": "jest --clearCache --config test/native/jest.config.js; rm -rf $TMPDIR/jest_*", "test:native:debug": "cross-env NODE_ENV=test node --inspect-brk node_modules/.bin/jest --runInBand --verbose --config test/native/jest.config.js", "test:native:perf": "cross-env TEST_RUNNER_ARGS='--runInBand --config test/native/jest.config.js --testMatch \"**/performance/*.native.[jt]s?(x)\"' reassure", From 310f7895ab940d9e4a9eedff0babd889525c36b9 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:41:59 +0200 Subject: [PATCH 031/325] Code block: should use RichText (native) (#56724) --- .../block-library/src/code/edit.native.js | 24 +++++++++---------- .../src/code/test/edit.native.js | 4 ++-- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/code/edit.native.js b/packages/block-library/src/code/edit.native.js index 3353dbc3c25a01..d348a6968b40da 100644 --- a/packages/block-library/src/code/edit.native.js +++ b/packages/block-library/src/code/edit.native.js @@ -6,7 +6,7 @@ import { View } from 'react-native'; /** * WordPress dependencies */ -import { PlainText } from '@wordpress/block-editor'; +import { RichText } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; @@ -20,14 +20,11 @@ import { createBlock, getDefaultBlockName } from '@wordpress/blocks'; */ 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, - onFocus, - onBlur, + onRemove, style, insertBlocksAfter, mergeBlocks, @@ -37,30 +34,31 @@ export function CodeEdit( props ) { styles.blockCode, styles.blockCodeDark ), - ...( style?.fontSize && { fontSize: style.fontSize } ), }; + const textStyle = style?.fontSize ? { fontSize: style.fontSize } : {}; + const placeholderStyle = usePreferredColorSchemeStyle( styles.placeholder, styles.placeholderDark ); return ( - - + <RichText + tagName="pre" value={ attributes.content } identifier="content" - style={ codeStyle } - multiline={ true } + style={ textStyle } underlineColorAndroid="transparent" onChange={ ( content ) => setAttributes( { content } ) } onMerge={ mergeBlocks } + onRemove={ onRemove } placeholder={ __( 'Write code…' ) } aria-label={ __( 'Code' ) } - isSelected={ props.isSelected } - onFocus={ onFocus } - onBlur={ onBlur } placeholderTextColor={ placeholderStyle.color } + preserveWhiteSpace + __unstablePastePlainText __unstableOnSplitAtDoubleLineEnd={ () => insertBlocksAfter( createBlock( getDefaultBlockName() ) ) } diff --git a/packages/block-library/src/code/test/edit.native.js b/packages/block-library/src/code/test/edit.native.js index 0f693fa2136ce1..be43e398d03a37 100644 --- a/packages/block-library/src/code/test/edit.native.js +++ b/packages/block-library/src/code/test/edit.native.js @@ -49,7 +49,7 @@ describe( 'Code', () => { const screen = await initializeEditor( { initialHtml, } ); - const { getByDisplayValue } = screen; + const { findByPlaceholderText } = screen; // Get block const codeBlock = await getBlock( screen, 'Code' ); @@ -57,7 +57,7 @@ describe( 'Code', () => { fireEvent.press( codeBlock ); // Get initial text - const codeBlockText = getByDisplayValue( 'Sample text' ); + const codeBlockText = await findByPlaceholderText( 'Write code…' ); expect( codeBlockText ).toBeVisible(); expect( getEditorHtml() ).toMatchSnapshot(); From efb7099762b375ec17d02a47bbea3b8efe82e1d2 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 5 Dec 2023 17:51:13 +0100 Subject: [PATCH 032/325] Start using modules in the interactive create-block template (#56694) * Start using modules * Update changelog --- packages/create-block-interactive-template/CHANGELOG.md | 2 ++ .../block-templates/render.php.mustache | 4 ++++ packages/create-block-interactive-template/index.js | 1 - .../plugin-templates/$slug.php.mustache | 7 +++++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 8fe008fcda17e1..fbe3a8c8c857c8 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) + ## 1.10.0 (2023-11-29) ### Enhancement diff --git a/packages/create-block-interactive-template/block-templates/render.php.mustache b/packages/create-block-interactive-template/block-templates/render.php.mustache index efecd748d19ef8..01cbe6ed83cfb5 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -11,7 +11,11 @@ * @see https://github.com/WordPress/gutenberg/blob/trunk/docs/reference-guides/block-api/block-metadata.md#render */ +// Generate unique id for aria-controls. $unique_id = wp_unique_id( 'p-' ); + +// Enqueue the view file. +gutenberg_enqueue_module( '{{namespace}}-view' ); ?> <div diff --git a/packages/create-block-interactive-template/index.js b/packages/create-block-interactive-template/index.js index 5717b3e709723a..b2682600f7af6d 100644 --- a/packages/create-block-interactive-template/index.js +++ b/packages/create-block-interactive-template/index.js @@ -14,7 +14,6 @@ module.exports = { interactivity: true, }, render: 'file:./render.php', - viewScript: 'file:./view.js', example: {}, }, variants: { diff --git a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache index 52c9c4966646fa..73726b930e4728 100644 --- a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache @@ -43,5 +43,12 @@ if ( ! defined( 'ABSPATH' ) ) { */ function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() { register_block_type( __DIR__ . '/build' ); + + gutenberg_register_module( + '{{namespace}}-view', + plugin_dir_url( __FILE__ ) . 'src/view.js', + array( '@wordpress/interactivity' ), + '{{version}}' + ); } add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init' ); From 62b933e0f0508b8dccd0493427d44ef95145b6f7 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 5 Dec 2023 18:02:18 +0100 Subject: [PATCH 033/325] Interactivity API: new store() API docs (#56764) * New store() API docs * Update interactivity API diagram * Small fixes to the docs * Update interactivity docs assets * Update `data-wp-interactive` in getting started * Add docs for private stores * Slightly improve `wp-class` example * Clarify `store()` returns the same references * Mention how to read props from other ns --------- Co-authored-by: Mario Santos <santosguillamot@gmail.com> Co-authored-by: David Arenas <david.arenas@automattic.com> --- .../interactivity/docs/1-getting-started.md | 2 +- .../interactivity/docs/2-api-reference.md | 464 ++++++++++-------- .../docs/assets/state-directives.png | Bin 339655 -> 338959 bytes .../docs/assets/store-server-client.png | Bin 158950 -> 156828 bytes 4 files changed, 263 insertions(+), 203 deletions(-) diff --git a/packages/interactivity/docs/1-getting-started.md b/packages/interactivity/docs/1-getting-started.md index c4bae5d5fe6702..0b4708b78b3509 100644 --- a/packages/interactivity/docs/1-getting-started.md +++ b/packages/interactivity/docs/1-getting-started.md @@ -83,7 +83,7 @@ To "activate" the Interactivity API in a DOM element (and its children) we add t ```html -<div data-wp-interactive> +<div data-wp-interactive='{ "namespace": "myPlugin" }'> <!-- Interactivity API zone --> </div> ``` \ No newline at end of file diff --git a/packages/interactivity/docs/2-api-reference.md b/packages/interactivity/docs/2-api-reference.md index 828de4379c0269..3c8f179861f525 100644 --- a/packages/interactivity/docs/2-api-reference.md +++ b/packages/interactivity/docs/2-api-reference.md @@ -2,10 +2,10 @@ To add interactivity to blocks using the Interactivity API, developers can use: -- **Directives** - added to the markup to add specific behavior to the DOM elements of block. -- **Store** - that contains the logic and data (state, actions, or effects among others) needed for the behaviour. +- **Directives** - added to the markup to add specific behavior to the DOM elements of the block. +- **Store** - that contains the logic and data (state, actions, or side effects, among others) needed for the behavior. -DOM elements are connected to data stored in the state & context through directives. If data in the state or context change, directives will react to those changes updating the DOM accordingly (see [diagram](https://excalidraw.com/#json=rEg5d71O_jy3NrgYJUIVd,yjOUmMvxzNf6alqFjElvIw)). +DOM elements are connected to data stored in the state and context through directives. If data in the state or context change directives will react to those changes, updating the DOM accordingly (see [diagram](https://excalidraw.com/#json=T4meh6lltJh6TCX51NTIu,DmIhxYSGFTL_ywZFbsmuSw)). ![State & Directives](assets/state-directives.png) @@ -20,7 +20,7 @@ DOM elements are connected to data stored in the state & context through directi - [`wp-style`](#wp-style) ![](https://img.shields.io/badge/ATTRIBUTES-afd2e3.svg) - [`wp-text`](#wp-text) ![](https://img.shields.io/badge/CONTENT-afd2e3.svg) - [`wp-on`](#wp-on) ![](https://img.shields.io/badge/EVENT_HANDLERS-afd2e3.svg) - - [`wp-effect`](#wp-effect) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) + - [`wp-watch`](#wp-watch) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-init`](#wp-init) ![](https://img.shields.io/badge/SIDE_EFFECTS-afd2e3.svg) - [`wp-key`](#wp-key) ![](https://img.shields.io/badge/TEMPLATING-afd2e3.svg) - [Values of directives are references to store properties](#values-of-directives-are-references-to-store-properties) @@ -28,18 +28,14 @@ DOM elements are connected to data stored in the state & context through directi - [Elements of the store](#elements-of-the-store) - [State](#state) - [Actions](#actions) - - [Effects](#effects) - - [Selectors](#selectors) - - [Arguments passed to callbacks](#arguments-passed-to-callbacks) + - [Side Effects](#side-effects) - [Setting the store](#setting-the-store) - [On the client side](#on-the-client-side) - [On the server side](#on-the-server-side) - - ## The directives -Directives are custom attributes that are added to the markup of your block to add behaviour to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks). +Directives are custom attributes that are added to the markup of your block to add behavior to its DOM elements. This can be done in the `render.php` file (for dynamic blocks) or the `save.js` file (for static blocks). Interactivity API directives use the `data-` prefix. @@ -47,40 +43,38 @@ _Example of directives used in the HTML markup_ ```html <div - data-wp-context='{ "myNamespace" : { "isOpen": false } }' - data-wp-effect="effects.myNamespace.logIsOpen" + data-wp-interactive='{ "namespace": "myPlugin" }' + data-wp-context='{ "isOpen": false }' + data-wp-watch="callbacks.logIsOpen" > <button - data-wp-on--click="actions.myNamespace.toggle" - data-wp-bind--aria-expanded="context.myNamespace.isOpen" + data-wp-on--click="actions.toggle" + data-wp-bind--aria-expanded="context.isOpen" aria-controls="p-1" > Toggle </button> - <p id="p-1" data-bind--hidden="!context.myNamespace.isOpen"> + <p id="p-1" data-bind--hidden="!context.isOpen"> This element is now visible! </p> </div> ``` -> **Note** -> The use of Namespaces to define the context, state or any other elements of the store is highly recommended to avoid possible collision with other elements with the same name. In the following examples we have not used namespaces for the sake of simplicity. - Directives can also be injected dynamically using the [HTML Tag Processor](https://make.wordpress.org/core/2023/03/07/introducing-the-html-api-in-wordpress-6-2). ### List of Directives -With directives we can manage directly in the DOM behavior related to things such as side effects, state, event handlers, attributes or content. +With directives, we can directly manage behavior related to things such as side effects, state, event handlers, attributes or content. #### `wp-interactive` -The `wp-interactive` directive "activates" the interactivity for the DOM element and its children through the Interactivity API (directives and store). +The `wp-interactive` directive "activates" the interactivity for the DOM element and its children through the Interactivity API (directives and store). It includes a namespace to reference a specific store. ```html -<!-- Let's make this element and its children interactive --> +<!-- Let's make this element and its children interactive and set the namespace --> <div - data-wp-interactive + data-wp-interactive='{ "namespace": "myPlugin" }' data-wp-context='{ "myColor" : "red", "myBgColor": "yellow" }' > <p>I'm interactive now, <span data-wp-style--background-color="context.myBgColor">>and I can use directives!</span></p> @@ -91,19 +85,19 @@ The `wp-interactive` directive "activates" the interactivity for the DOM element ``` > **Note** -> The use of `wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `wp-interactive` has not been added for the sake of simplicity. - +> The use of `data-wp-interactive` is a requirement for the Interactivity API "engine" to work. In the following examples the `data-wp-interactive` has not been added for the sake of simplicity. Also, the `data-wp-interactive` directive will be injected automatically in the future. #### `wp-context` -It provides **local** state available to a specific HTML node and its children. +It provides a **local** state available to a specific HTML node and its children. -The `wp-context` directive accepts a stringified JSON as value. +The `wp-context` directive accepts a stringified JSON as a value. _Example of `wp-context` directive_ + ```php //render.php -<div data-wp-context='{ {"post": { "id": <?php echo $post->ID; ?> } } ' > +<div data-wp-context='{ "post": { "id": <?php echo $post->ID; ?> } }' > <button data-wp-on--click="actions.logId" > Click Me! </button> @@ -114,10 +108,11 @@ _Example of `wp-context` directive_ <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - logId: ( { context } ) => { - console.log( context.post.id ); + logId: () => { + const { post } = getContext(); + console.log( post.id ); }, }, } ); @@ -126,7 +121,7 @@ store( { </details> <br/> -Different contexts can be defined at different levels and deeper levels will merge their own context with any parent one: +Different contexts can be defined at different levels, and deeper levels will merge their own context with any parent one: ```html <div data-wp-context="{ foo: 'bar' }"> @@ -150,6 +145,7 @@ It allows setting HTML attributes on elements based on a boolean or string value > This directive follows the syntax `data-wp-bind--attribute`. _Example of `wp-bind` directive_ + ```html <li data-wp-context='{ "isMenuOpen": false }'> <button @@ -166,16 +162,18 @@ _Example of `wp-bind` directive_ </div> </li> ``` + <details> <summary><em>See store used with the directive above</em></summary> ```js -store( { - actions: { - toggleMenu: ( { context } ) => { +store( "myPlugin", { + actions: { + toggleMenu: () => { + const context = getContext(); context.isMenuOpen = !context.isMenuOpen; }, - }, + }, } ); ``` @@ -183,15 +181,17 @@ store( { <br/> The `wp-bind` directive is executed: - - when the element is created. - - each time there's a change on any of the properties of the `state` or `context` involved on getting the final value of the directive (inside the callback or the expression passed as reference). + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). When `wp-bind` directive references a callback to get its final value: + - The `wp-bind` directive will be executed each time there's a change on any of the properties of the `state` or `context` used inside this callback. -- The callback receives the attribute name: `attribute`. - The returned value in the callback function is used to change the value of the associated attribute. -The `wp-bind` will do different things over the DOM element is applied depending on its value: +The `wp-bind` will do different things over the DOM element is applied, depending on its value: + - If the value is `true`, the attribute is added: `<div attribute>`. - If the value is `false`, the attribute is removed: `<div>`. - If the value is a string, the attribute is added with its value assigned: `<div attribute="value"`. @@ -204,17 +204,18 @@ It adds or removes a class to an HTML element, depending on a boolean value. > This directive follows the syntax `data-wp-class--classname`. _Example of `wp-class` directive_ -```php + +```html <div> <li - data-wp-context='{ "isSelected": false } ' + data-wp-context='{ "isSelected": false }' data-wp-on--click="actions.toggleSelection" data-wp-class--selected="context.isSelected" > Option 1 </li> <li - data-wp-context='{ "isSelected": false } ' + data-wp-context='{ "isSelected": false }' data-wp-on--click="actions.toggleSelection" data-wp-class--selected="context.isSelected" > @@ -227,9 +228,10 @@ _Example of `wp-class` directive_ <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - toggleSelection: ( { context } ) => { + toggleSelection: () => { + const context = getContext(); context.isSelected = !context.isSelected } } @@ -240,14 +242,14 @@ store( { <br/> The `wp-class` directive is executed: - - when the element is created. - - each time there's a change on any of the properties of the `state` or `context` involved on getting the final value of the directive (inside the callback or the expression passed as reference). + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). When `wp-class` directive references a callback to get its final boolean value, the callback receives the class name: `className`. The boolean value received by the directive is used to toggle (add when `true` or remove when `false`) the associated class name from the `class` attribute. - #### `wp-style` It adds or removes inline style to an HTML element, depending on its value. @@ -255,6 +257,7 @@ It adds or removes inline style to an HTML element, depending on its value. > This directive follows the syntax `data-wp-style--css-property`. _Example of `wp-style` directive_ + ```html <div data-wp-context='{ "color": "red" }' > <button data-wp-on--click="actions.toggleContextColor">Toggle Color Text</button> @@ -267,9 +270,10 @@ _Example of `wp-style` directive_ <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - toggleContextColor: ( { context } ) => { + toggleContextColor: () => { + const context = getContext(); context.color = context.color === 'red' ? 'blue' : 'red'; }, }, @@ -280,14 +284,16 @@ store( { <br/> The `wp-style` directive is executed: - - when the element is created. - - each time there's a change on any of the properties of the `state` or `context` involved on getting the final value of the directive (inside the callback or the expression passed as reference). + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). When `wp-style` directive references a callback to get its final value, the callback receives the class style property: `css-property`. The value received by the directive is used to add or remove the style attribute with the associated CSS property: : - - If the value is `false`, the style attribute is removed: `<div>`. - - If the value is a string, the attribute is added with its value assigned: `<div style="css-property: value;">`. + +- If the value is `false`, the style attribute is removed: `<div>`. +- If the value is a string, the attribute is added with its value assigned: `<div style="css-property: value;">`. #### `wp-text` @@ -306,9 +312,10 @@ It sets the inner text of an HTML element. <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - toggleContextText: ( { context } ) => { + toggleContextText: () => { + const context = getContext(); context.text = context.text === 'Text 1' ? 'Text 2' : 'Text 1'; }, }, @@ -319,8 +326,9 @@ store( { <br/> The `wp-text` directive is executed: - - when the element is created. - - each time there's a change on any of the properties of the `state` or `context` involved on getting the final value of the directive (inside the callback or the expression passed as reference). + +- When the element is created. +- Each time there's a change on any of the properties of the `state` or `context` involved in getting the final value of the directive (inside the callback or the expression passed as reference). The returned value is used to change the inner content of the element: `<div>value</div>`. @@ -331,6 +339,7 @@ It runs code on dispatched DOM events like `click` or `keyup`. > The syntax of this directive is `data-wp-on--[event]` (like `data-wp-on--click` or `data-wp-on--keyup`). _Example of `wp-on` directive_ + ```php <button data-wp-on--click="actions.logTime" > Click Me! @@ -341,9 +350,11 @@ _Example of `wp-on` directive_ <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - logTime: () => console.log( new Date() ), + logTime: ( event ) => { + console.log( new Date() ) + }, }, } ); ``` @@ -353,20 +364,20 @@ store( { The `wp-on` directive is executed each time the associated event is triggered. -The callback passed as reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`) and the returned value by this callback is ignored. - +The callback passed as the reference receives [the event](https://developer.mozilla.org/en-US/docs/Web/API/Event) (`event`), and the returned value by this callback is ignored. -#### `wp-effect` +#### `wp-watch` It runs a callback **when the node is created and runs it again when the state or context changes**. -You can attach several effects to the same DOM element by using the syntax `data-wp-effect--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-effect` directives of that DOM element._ +You can attach several side effects to the same DOM element by using the syntax `data-wp-watch--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-watch` directives of that DOM element._ + +_Example of `wp-watch` directive_ -_Example of `wp-effect` directive_ ```html <div data-wp-context='{ "counter": 0 }' - data-wp-effect="effects.logCounter" + data-wp-watch="callbacks.logCounter" > <p>Counter: <span data-wp-text="context.counter"></span></p> <button data-wp-on--click="actions.increaseCounter">+</button> @@ -378,17 +389,22 @@ _Example of `wp-effect` directive_ <summary><em>See store used with the directive above</em></summary> ```js -store( { +store( "myPlugin", { actions: { - increaseCounter: ({ context }) => { + increaseCounter: () => { + const context = getContext(); context.counter++; }, - decreaseCounter: ({ context }) => { + decreaseCounter: () => { + const context = getContext(); context.counter--; }, - } - effects: { - logCounter: ({ context }) => console.log("Counter is " + context.counter + " at " + new Date() ), + }, + callbacks: { + logCounter: () => { + const { counter } = getContext(); + console.log("Counter is " + counter + " at " + new Date() ); + }, }, } ); ``` @@ -396,17 +412,19 @@ store( { </details> <br/> -The `wp-effect` directive is executed: - - when the element is created. - - each time that any of the properties of the `state` or `context` used inside the callback changes. +The `wp-watch` directive is executed: -The `wp-effect` directive can return a function. If it does, the returned function is used as cleanup logic, i.e., it will run just before the callback runs again, and it will run again when the element is removed from the DOM. +- When the element is created. +- Each time that any of the properties of the `state` or `context` used inside the callback changes. -As a reference, some use cases for this directive may be: -- logging. -- changing the title of the page. -- setting the focus on an element with `.focus()`. -- changing the state or context when certain conditions are met. +The `wp-watch` directive can return a function. If it does, the returned function is used as cleanup logic, i.e., it will run just before the callback runs again, and it will run again when the element is removed from the DOM. + +As a reference, some use cases for this directive may be: + +- Logging. +- Changing the title of the page. +- Setting the focus on an element with `.focus()`. +- Changing the state or context when certain conditions are met. #### `wp-init` @@ -415,17 +433,19 @@ It runs a callback **only when the node is created**. You can attach several `wp-init` to the same DOM element by using the syntax `data-wp-init--[unique-id]`. _The unique id doesn't need to be unique globally, it just needs to be different than the other unique ids of the `wp-init` directives of that DOM element._ _Example of `data-wp-init` directive_ + ```html -<div data-wp-init="effects.logTimeInit"> +<div data-wp-init="callbacks.logTimeInit"> <p>Hi!</> </div> ``` _Example of several `wp-init` directives on the same DOM element_ + ```html <form - data-wp-init--log="effects.logTimeInit" - data-wp-init--focus="effects.focusFirstElement" + data-wp-init--log="callbacks.logTimeInit" + data-wp-init--focus="callbacks.focusFirstElement" > <input type="text"> </form> @@ -435,11 +455,13 @@ _Example of several `wp-init` directives on the same DOM element_ <summary><em>See store used with the directive above</em></summary> ```js -store( { - effects: { +store( "myPlugin", { + callbacks: { logTimeInit: () => console.log( `Init at ` + new Date() ), - focusFirstElement: ( { ref } ) => + focusFirstElement: () => { + const { ref } = getElement(); ref.querySelector( 'input:first-child' ).focus(), + }, }, } ); ``` @@ -447,15 +469,13 @@ store( { </details> <br/> - The `wp-init` can return a function. If it does, the returned function will run when the element is removed from the DOM. #### `wp-key` +The `wp-key` directive assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements. This becomes important if your array elements can move (e.g., due to sorting), get inserted, or get deleted. A well-chosen key value helps the Interactivity API infer what exactly has changed in the array, allowing it to make the correct updates to the DOM. -The `wp-key` directive assigns a unique key to an element to help the Interactivity API identify it when iterating through arrays of elements. This becomes important if your array elements can move (e.g. due to sorting), get inserted, or get deleted. A well-chosen key value helps the Interactivity API infer what exactly has changed in the array, allowing it to make the correct updates to the DOM. - -The key should be a string that uniquely identifies the element among its siblings. Typically it is used on repeated elements like list items. For example: +The key should be a string that uniquely identifies the element among its siblings. Typically, it is used on repeated elements like list items. For example: ```html <ul> @@ -477,47 +497,58 @@ When the list is re-rendered, the Interactivity API will match elements by their ### Values of directives are references to store properties -The value assigned to a directive is a string pointing to a specific state, selector, action, or effect. *Using a Namespace is highly recommended* to define these elements of the store. +The value assigned to a directive is a string pointing to a specific state, action, or side effect. -In the following example we use the namespace `wpmovies` (plugin name is usually a good namespace name) to define the `isPlaying` selector. +In the following example, we use a getter to define the `state.isPlaying` derived value. ```js -store( { - selectors: { - wpmovies: { - isPlaying: ( { state } ) => state.wpmovies.currentVideo !== '', - }, - }, +const { state } = store( "myPlugin", { + state: { + currentVideo: '', + get isPlaying() { + return state.currentVideo !== ''; + } + }, } ); ``` -And then, we use the string value `"selectors.wpmovies.isPlaying"` to assign the result of this selector to `data-bind--hidden`. +And then, we use the string value `"state.isPlaying"` to assign the result of this selector to `data-bind--hidden`. -```php -<div data-bind--hidden="!selectors.wpmovies.isPlaying" ... > +```html +<div data-bind--hidden="!state.isPlaying" ... > <iframe ...></iframe> </div> ``` -These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element (action, effect...) refers to without any additional configuration. +These values assigned to directives are **references** to a particular property in the store. They are wired to the directives automatically so that each directive “knows” what store element refers to, without any additional configuration. +Note that, by default, references point to properties in the current namespace, which is the one specified by the closest ancestor with a `data-wp-interactive` attribute. If you need to access a property from a different namespace, you can explicitly set the namespace where the property we want to access is defined. The syntax is `namespace::reference`, replacing `namespace` with the appropriate value. -## The store +In the example below, we get `state.isPlaying` from `otherPlugin` instead of `myPlugin`: -The store is used to create the logic (actions, effects…) linked to the directives and the data used inside that logic (state, selectors…). +```html +<div data-wp-interactive='{ "namespace": "myPlugin" }'> + <div data-bind--hidden="otherPlugin::!state.isPlaying" ... > + <iframe ...></iframe> + </div> +</div> +``` -**The store is usually created in the `view.js` file of each block**, although it can be initialized from the `render.php` of the block. +## The store -The store contains the reactive state and the actions and effects that modify it. +The store is used to create the logic (actions, side effects…) linked to the directives and the data used inside that logic (state, derived state…). + +**The store is usually created in the `view.js` file of each block**, although the state can be initialized from the `render.php` of the block. ### Elements of the store #### State -Defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data: - - **Global state**: It is defined using the `store()` function, and the data is available to all the HTML nodes of the page. It can be accessed using the `state` property. - - **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `context` property. - +It defines data available to the HTML nodes of the page. It is important to differentiate between two ways to define the data: + +- **Global state**: It is defined using the `store()` function with the `state` property, and the data is available to all the HTML nodes of the page. +- **Context/Local State**: It is defined using the `data-wp-context` directive in an HTML node, and the data is available to that HTML node and its children. It can be accessed using the `getContext` function inside of an action, derived state or side effect. + ```html <div data-wp-context='{ "someText": "Hello World!" }'> @@ -531,13 +562,15 @@ Defines data available to the HTML nodes of the page. It is important to differe ``` ```js -store( { +const { state } = store( "myPlugin", { state: { someText: "Hello Universe!" }, actions: { - someAction: ({ state, context }) => { + someAction: () => { state.someText // Access or modify global state - "Hello Universe!" + + const context = getContext(); context.someText // Access or modify local state (context) - "Hello World!" }, }, @@ -548,96 +581,126 @@ store( { Usually triggered by the `data-wp-on` directive (using event listeners) or other actions. -#### Effects +#### Side Effects -Automatically react to state changes. Usually triggered by `data-wp-effect` or `data-wp-init` directives. +Automatically react to state changes. Usually triggered by `data-wp-watch` or `data-wp-init` directives. -#### Selectors +#### Derived state -Also known as _derived state_, returns a computed version of the state. They can access both `state` and `context`. +They return a computed version of the state. They can access both `state` and `context`. ```js // view.js -store( { - state: { - amount: 34, - defaultCurrency: 'EUR', - currencyExchange: { - USD: 1.1, - GBP: 0.85, - }, - }, - selectors: { - amountInUSD: ( { state } ) => - state.currencyExchange[ 'USD' ] * state.amount, - amountInGBP: ( { state } ) => - state.currencyExchange[ 'GBP' ] * state.amount, - }, +const { state } = store( "myPlugin", { + state: { + amount: 34, + defaultCurrency: 'EUR', + currencyExchange: { + USD: 1.1, + GBP: 0.85, + }, + get amountInUSD() { + return state.currencyExchange[ 'USD' ] * state.amount, + }, + get amountInGBP() { + return state.currencyExchange[ 'GBP' ] * state.amount, + }, + }, +} ); +``` + +### Accessing data in callbacks + + +The **`store`** contains all the store properties, like `state`, `actions` or `callbacks`. They are returned by the `store()` call, so you can access them by destructuring it: + +```js +const { state, actions } = store( "myPlugin", { + // ... } ); ``` -### Arguments passed to callbacks +The `store()` function can be called multiple times and all the store parts will be merged together: + +```js +store( "myPlugin", { + state: { + someValue: 1, + } +} ); + +const { state } = store( "myPlugin", { + actions: { + someAction() { + state.someValue // = 1 + } + } +} ); +``` -When a directive is evaluated, the reference callback receives an object with: +> **Note** +> All `store()` calls with the same namespace return the same references, i.e., the same `state`, `actions`, etc., containing the result of merging all the store parts passed. -- The **`store`** containing all the store properties, like `state`, `selectors`, `actions` or `effects` -- The **context** (an object containing the context defined in all the `wp-context` ancestors). -- The reference to the DOM element on which the directive was defined (a `ref`). -- Other properties relevant to the directive. For example, the `data-wp-on--click` directive also receives the instance of the [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent) triggered by the user. +- To access the context inside an action, derived state, or side effect, you can use the `getContext` function. +- To access the reference, you can use the `getElement` function. -_Example of action making use of all values received when it's triggered_ ```js -// view.js -store( { - state: { - theme: false, - }, - actions: { - toggle: ( { state, context, ref, event, className } ) => { - console.log( state ); - // `{ "theme": false }` - console.log( context ); - // `{ "isOpen": true }` - console.log( ref ); - // The DOM element - console.log( event ); - // The Event object if using the `data-wp-on` - console.log( className ); - // The class name if using the `data-wp-class` - }, - }, +const { state } = store( "myPlugin", { + state: { + get someDerivedValue() { + const context = getContext(); + const { ref } = getElement(); + // ... + } + }, + actions: { + someAction() { + const context = getContext(); + const { ref } = getElement(); + // ... + } + }, + callbacks: { + someEffect() { + const context = getContext(); + const { ref } = getElement(); + // ... + } + } } ); ``` This approach enables some functionalities that make directives flexible and powerful: -- Actions and effects can read and modify the state and the context. +- Actions and side effects can read and modify the state and the context. - Actions and state in blocks can be accessed by other blocks. -- Actions and effects can do anything a regular JavaScript function can do, like access the DOM or make API requests. -- Effects automatically react to state changes. +- Actions and side effects can do anything a regular JavaScript function can do, like access the DOM or make API requests. +- Side effects automatically react to state changes. ### Setting the store #### On the client side -*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, effects or selectors. +*In the `view.js` file of each block* we can define both the state and the elements of the store referencing functions like actions, side effects or derived state. -`store` method used to set the store in javascript can be imported from `@wordpress/interactivity`. +The `store` method used to set the store in javascript can be imported from `@wordpress/interactivity`. ```js // store -import { store } from '@wordpress/interactivity'; +import { store, getContext } from '@wordpress/interactivity'; -store( { +store( "myPlugin", { actions: { - toggle: ( { context } ) => { + toggle: () => { + const context = getContext(); context.isOpen = !context.isOpen; }, }, - effects: { - logIsOpen: ( { context } ) => { + callbacks: { + logIsOpen: () => { + const { isOpen } = getContext(); // Log the value of `isOpen` each time it changes. - console.log( `Is open: ${ context.isOpen }` ); + console.log( `Is open: ${ isOpen }` ); } }, }); @@ -645,69 +708,66 @@ store( { #### On the server side -The store can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1). +> **Note** +> We will rename `wp_store` to `wp_initial_state` in a future version. + +The state can also be initialized on the server using the `wp_store()` function. You would typically do this in the `render.php` file of your block (the `render.php` templates were [introduced](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/) in WordPress 6.1). -The store defined on the server with `wp_store()` gets merged with the stores defined in the view.js files. +The state defined on the server with `wp_store()` gets merged with the stores defined in the view.js files. The `wp_store` function receives an [associative array](https://www.php.net/manual/en/language.types.array.php) as a parameter. - _Example of store initialized from the server with a `state` = `{ someValue: 123 }`_ ```php // render.php wp_store( array( - 'state' => array( - 'myNamespace' => array( - 'someValue' = 123 - ) + 'myPlugin' => array( + 'someValue' = 123 ) ); ``` -Initializing the store in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state: +Initializing the state in the server also allows you to use any WordPress API. For example, you could use the Core Translation API to translate part of your state: ```php // render.php wp_store( array( - "state" => array( - "favoriteMovies" => array( - "1" => array( - "id" => "123-abc", - "movieName" => __("someMovieName", "textdomain") - ), + "favoriteMovies" => array( + "1" => array( + "id" => "123-abc", + "movieName" => __("someMovieName", "textdomain") ), ), ) ); ``` -### Store options - -The `store` function accepts an object as a second argument with the following optional properties: +### Private stores -#### `afterLoad` - -Callback to be executed after the Interactivity API has been set up and the store is ready. It receives the global store as argument. +A given store namespace can be marked as private, thus preventing its content to be accessed from other namespaces. The mechanism to do so is by adding a `lock` option to the `store()` call, like shown in the example below. This way, further executions of `store()` with the same locked namespace will throw an error, meaning that the namespace can only be accessed where its reference was returned from the first `store()` call. This is specially useful for developers that want to hide part of their plugin stores so it doesn't become accessible for extenders. ```js -// view.js -store( - { - state: { - cart: [], - }, - }, - { - afterLoad: async ( { state } ) => { - // Let's consider `clientId` is added - // during server-side rendering. - state.cart = await getCartData( state.clientId ); - }, - } +const { state } = store( + "myPlugin/private", + { state: { messages: [ "private message" ] } }, + { lock: true } ); + +// The following call throws an Error! +store( "myPlugin/private", { /* store part */ } ); ``` +There is also a way to unlock private stores: instead of passing a boolean, you can use a string as the `lock` value. Such a string can then be used in subsequent `store()` calls to the same namespace to unlock its content. Only the code knowing the string lock will be able to unlock the protected store namespaced. This is useful for complex stores defined in multiple JS modules. +```js +const { state } = store( + "myPlugin/private", + { state: { messages: [ "private message" ] } }, + { lock: PRIVATE_LOCK } +); +// The following call works as expected. +store( "myPlugin/private", { /* store part */ }, { lock: PRIVATE_LOCK } ); +``` diff --git a/packages/interactivity/docs/assets/state-directives.png b/packages/interactivity/docs/assets/state-directives.png index feb93a2d1f8956ed5d16b59f02855bddccf027c5..a2422d1a2a049ec307bdddc8d8f46f5bb8436214 100644 GIT binary patch literal 338959 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%0D`r8+fVsO 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 literal 339655 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%0Dkb?>#G;e79ODD-(^m8{_XRA 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 HhIju5@06o+ diff --git a/packages/interactivity/docs/assets/store-server-client.png b/packages/interactivity/docs/assets/store-server-client.png index 089268cdc7d9c7d9619404716ef059f981a2375b..37818e37faa3dcb0de649e593e6e8dad363e6180 100644 GIT binary patch literal 156828 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 literal 158950 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%0D1L*sH>E0hzB+%3g04?g0L<mCb)?7Q9Ofn z3s}8~y8}COEr*YU`}}}ELF4{5=q@z+s@~a%z(@W7Hk-4jI<m2CIly8X{_7HvnRXUV zMtzR|P4n^hM&+^xEjHMU!e93oVy7hx<Per^iad>9>p2UpXD=<R(&?eF_Mw1vL>ozq zFRHpbrY4cqck6$gfPd`QFCz@*weMmbE!I2=seBO9mh-dE&Cl;}*F8IJ^qOlrXL4s> zp5w^QNBo^!^pD~wSCGqqXR+V)>V!fRDnsDJm%@%6*8(L|$5!2>#&-LhF9)M>#}qGH z_&i-Pn$=%fX2waLN_lK<=i8tPxA|G8@(br0y$1)abf$%$H(z|!pmvK4Zx0d}tUTMZ zTq*FMR@H=lx+yqN8%b?>?}VFkx+BH*eNs4Mt@1szId@a=AN05HbL`5uO#kq)i4lJt zEDxOkPb($>m$vuLk=tX^4O^|A2)^lYtk~x9dH!{ywrgA1n<ow7T^H4_6>g^Jp1poW zxsh?E^X7s>d&PK3gN#k+XiwSZ%G=_bS*_seV*JnnHH_OuseSUp=8t{XHPZ*KQ^->p zSy+s;UAg==M0qH0llADk2u57s`WIuP8Lfk-y3>KCdM$t=X~medun9XqfQi1|gmn`; z5C2wC!ISbik=YX|xbq408Y(4g!!{rE%bT$5tQUJx(lnRjONM^|GBdBzC7wB!+BW=g zs>h||mI9%3i~b6jv}9(EzJixsVcN%mwI38vq$i#WGlfg7*j4!U%c!^5J#2}?q6o=( z>6KZ1p3BTCsm0O*jp2#Aw0vThHbz+o_aO;3iP>2FT=Feez_T*0k4LGWq{en>^LAiD z`ojo`|NaELObIiv`r6)Q?kedkf5Zf0`ax$_e^cv+viN5*_$Pf_{#fD>y{rERUTeXd From 5644375d669ffc0d9ec7df5c2d32deae56d18aab Mon Sep 17 00:00:00 2001 From: Jeff Ong <jonger4@gmail.com> Date: Tue, 5 Dec 2023 12:08:48 -0500 Subject: [PATCH 034/325] Check response.errors.length. (#56762) --- .../src/components/global-styles/font-library-modal/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/context.js b/packages/edit-site/src/components/global-styles/font-library-modal/context.js index 33a5b0910f0526..58b8621adcf0c3 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/context.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/context.js @@ -229,7 +229,7 @@ function FontLibraryProvider( { children } ) { // Uninstall the font (remove the font files from the server and the post from the database). const response = await fetchUninstallFonts( [ font ] ); // Deactivate the font family (remove the font family from the global styles). - if ( ! response.errors ) { + if ( 0 === response.errors.length ) { deactivateFontFamily( font ); // Save the global styles to the database. await saveSpecifiedEntityEdits( From bc4e98c6b27becf9a96cccebb8e4caf820d74730 Mon Sep 17 00:00:00 2001 From: David Arenas <david.arenas@automattic.com> Date: Tue, 5 Dec 2023 18:16:15 +0100 Subject: [PATCH 035/325] Interactivity API: update TS/JSDocs after migrating to the new `store()` API (#56748) * Update tsdocs for `store()` * Fix TS errors in hooks.tsx * Minor changes to context types * Rename DirectiveCallback params to args * Update directive() tsdocs * Add tsdocs for `getContext` and `getElement` * Add jsdocs for `navigate` and `prefetch` * Fix previousScope ref and state * Remove unnecessary `!` operator * Add removed comments for `DirectiveArgs` * Fix example format in `directive()` --- packages/interactivity/src/hooks.tsx | 225 ++++++++++++++++++--------- packages/interactivity/src/router.js | 35 ++++- packages/interactivity/src/store.ts | 85 ++++++---- 3 files changed, 236 insertions(+), 109 deletions(-) diff --git a/packages/interactivity/src/hooks.tsx b/packages/interactivity/src/hooks.tsx index f782d998498621..14f0dc6683b460 100644 --- a/packages/interactivity/src/hooks.tsx +++ b/packages/interactivity/src/hooks.tsx @@ -1,44 +1,93 @@ -// @ts-nocheck - /** * External dependencies */ import { h, options, createContext, cloneElement } from 'preact'; import { useRef, useCallback, useContext } from 'preact/hooks'; import { deepSignal } from 'deepsignal'; +import type { VNode, Context, RefObject } from 'preact'; + /** * Internal dependencies */ import { stores } from './store'; +interface DirectiveEntry { + value: string | Object; + namespace: string; + suffix: string; +} -/** @typedef {import('preact').VNode} VNode */ -/** @typedef {typeof context} Context */ -/** @typedef {ReturnType<typeof getEvaluate>} Evaluate */ +type DirectiveEntries = Record< string, DirectiveEntry[] >; -/** - * @typedef {Object} DirectiveCallbackParams Callback parameters. - * @property {Object} directives Object map with the defined directives of the element being evaluated. - * @property {Object} props Props present in the current element. - * @property {VNode} element Virtual node representing the original element. - * @property {Context} context The inherited context. - * @property {Evaluate} evaluate Function that resolves a given path to a value either in the store or the context. - */ +interface DirectiveArgs { + /** + * Object map with the defined directives of the element being evaluated. + */ + directives: DirectiveEntries; + /** + * Props present in the current element. + */ + props: Object; + /** + * Virtual node representing the element. + */ + element: VNode; + /** + * The inherited context. + */ + context: Context< any >; + /** + * Function that resolves a given path to a value either in the store or the + * context. + */ + evaluate: Evaluate; +} -/** - * @callback DirectiveCallback Callback that runs the directive logic. - * @param {DirectiveCallbackParams} params Callback parameters. - */ +interface DirectiveCallback { + ( args: DirectiveArgs ): VNode | void; +} -/** - * @typedef DirectiveOptions Options object. - * @property {number} [priority=10] Value that specifies the priority to - * evaluate directives of this type. Lower - * numbers correspond with earlier execution. - * Default is `10`. - */ +interface DirectiveOptions { + /** + * Value that specifies the priority to evaluate directives of this type. + * Lower numbers correspond with earlier execution. + * + * @default 10 + */ + priority?: number; +} + +interface Scope { + evaluate: Evaluate; + context: Context< any >; + ref: RefObject< HTMLElement >; + state: any; + props: any; +} + +interface Evaluate { + ( entry: DirectiveEntry, ...args: any[] ): any; +} + +interface GetEvaluate { + ( args: { scope: Scope } ): Evaluate; +} + +type PriorityLevel = string[]; + +interface GetPriorityLevels { + ( directives: DirectiveEntries ): PriorityLevel[]; +} + +interface DirectivesProps { + directives: DirectiveEntries; + priorityLevels: PriorityLevel[]; + element: VNode; + originalProps: any; + previousScope?: Scope; +} // Main context. -const context = createContext( {} ); +const context = createContext< any >( {} ); // Wrap the element props to prevent modifications. const immutableMap = new WeakMap(); @@ -65,12 +114,28 @@ const deepImmutable = < T extends Object = {} >( target: T ): T => { // Store stacks for the current scope and the default namespaces and export APIs // to interact with them. -const scopeStack: any[] = []; +const scopeStack: Scope[] = []; const namespaceStack: string[] = []; +/** + * Retrieves the context inherited by the element evaluating a function from the + * store. The returned value depends on the element and the namespace where the + * function calling `getContext()` exists. + * + * @param namespace Store namespace. By default, the namespace where the calling + * function exists is used. + * @return The context content. + */ export const getContext = < T extends object >( namespace?: string ): T => getScope()?.context[ namespace || namespaceStack.slice( -1 )[ 0 ] ]; +/** + * Retrieves a representation of the element where a function from the store + * is being evalutated. Such representation is read-only, and contains a + * reference to the DOM element, its props and a local reactive state. + * + * @return Element representation. + */ export const getElement = () => { if ( ! getScope() ) { throw Error( @@ -87,7 +152,7 @@ export const getElement = () => { export const getScope = () => scopeStack.slice( -1 )[ 0 ]; -export const setScope = ( scope ) => { +export const setScope = ( scope: Scope ) => { scopeStack.push( scope ); }; export const resetScope = () => { @@ -102,8 +167,8 @@ export const resetNamespace = () => { }; // WordPress Directives. -const directiveCallbacks = {}; -const directivePriorities = {}; +const directiveCallbacks: Record< string, DirectiveCallback > = {}; +const directivePriorities: Record< string, number > = {}; /** * Register a new directive type in the Interactivity API runtime. @@ -112,34 +177,37 @@ const directivePriorities = {}; * ```js * directive( * 'alert', // Name without the `data-wp-` prefix. - * ( { directives: { alert }, element, evaluate }) => { - * element.props.onclick = () => { - * alert( evaluate( alert.default ) ); - * } + * ( { directives: { alert }, element, evaluate } ) => { + * const defaultEntry = alert.find( entry => entry.suffix === 'default' ); + * element.props.onclick = () => { alert( evaluate( defaultEntry ) ); } * } * ) * ``` * * The previous code registers a custom directive type for displaying an alert * message whenever an element using it is clicked. The message text is obtained - * from the store using `evaluate`. + * from the store under the inherited namespace, using `evaluate`. * * When the HTML is processed by the Interactivity API, any element containing * the `data-wp-alert` directive will have the `onclick` event handler, e.g., * * ```html - * <button data-wp-alert="state.messages.alert">Click me!</button> + * <div data-wp-interactive='{ "namespace": "messages" }'> + * <button data-wp-alert="state.alert">Click me!</button> + * </div> * ``` - * Note that, in the previous example, you access `alert.default` in order to - * retrieve the `state.messages.alert` value passed to the directive. You can - * also define custom names by appending `--` to the directive attribute, - * followed by a suffix, like in the following HTML snippet: + * Note that, in the previous example, the directive callback gets the path + * value (`state.alert`) from the directive entry with suffix `default`. A + * custom suffix can also be specified by appending `--` to the directive + * attribute, followed by the suffix, like in the following HTML snippet: * * ```html - * <button - * data-wp-color--text="state.theme.text" - * data-wp-color--background="state.theme.background" - * >Click me!</button> + * <div data-wp-interactive='{ "namespace": "myblock" }'> + * <button + * data-wp-color--text="state.text" + * data-wp-color--background="state.background" + * >Click me!</button> + * </div> * ``` * * This could be an hypothetical implementation of the custom directive used in @@ -149,28 +217,36 @@ const directivePriorities = {}; * ```js * directive( * 'color', // Name without prefix and suffix. - * ( { directives: { color }, ref, evaluate }) => { - * if ( color.text ) { - * ref.style.setProperty( - * 'color', - * evaluate( color.text ) - * ); - * } - * if ( color.background ) { - * ref.style.setProperty( - * 'background-color', - * evaluate( color.background ) - * ); - * } + * ( { directives: { color }, ref, evaluate } ) => + * colors.forEach( ( color ) => { + * if ( color.suffix = 'text' ) { + * ref.style.setProperty( + * 'color', + * evaluate( color.text ) + * ); + * } + * if ( color.suffix = 'background' ) { + * ref.style.setProperty( + * 'background-color', + * evaluate( color.background ) + * ); + * } + * } ); * } * ) * ``` * - * @param {string} name Directive name, without the `data-wp-` prefix. - * @param {DirectiveCallback} callback Function that runs the directive logic. - * @param {DirectiveOptions=} options Options object. + * @param name Directive name, without the `data-wp-` prefix. + * @param callback Function that runs the directive logic. + * @param options Options object. + * @param options.priority Option to control the directive execution order. The + * lesser, the highest priority. Default is `10`. */ -export const directive = ( name, callback, { priority = 10 } = {} ) => { +export const directive = ( + name: string, + callback: DirectiveCallback, + { priority = 10 }: DirectiveOptions = {} +) => { directiveCallbacks[ name ] = callback; directivePriorities[ name ] = priority; }; @@ -186,10 +262,13 @@ const resolve = ( path, namespace ) => { }; // Generate the evaluate function. -const getEvaluate = - ( { scope } = {} ) => +const getEvaluate: GetEvaluate = + ( { scope } ) => ( entry, ...args ) => { let { value: path, namespace } = entry; + if ( typeof path !== 'string' ) { + throw new Error( 'The `value` prop should be a string path' ); + } // If path starts with !, remove it and save a flag. const hasNegationOperator = path[ 0 ] === '!' && !! ( path = path.slice( 1 ) ); @@ -202,8 +281,10 @@ const getEvaluate = // Separate directives by priority. The resulting array contains objects // of directives grouped by same priority, and sorted in ascending order. -const getPriorityLevels = ( directives ) => { - const byPriority = Object.keys( directives ).reduce( ( obj, name ) => { +const getPriorityLevels: GetPriorityLevels = ( directives ) => { + const byPriority = Object.keys( directives ).reduce< + Record< number, string[] > + >( ( obj, name ) => { if ( directiveCallbacks[ name ] ) { const priority = directivePriorities[ name ]; ( obj[ priority ] = obj[ priority ] || [] ).push( name ); @@ -212,7 +293,7 @@ const getPriorityLevels = ( directives ) => { }, {} ); return Object.entries( byPriority ) - .sort( ( [ p1 ], [ p2 ] ) => p1 - p2 ) + .sort( ( [ p1 ], [ p2 ] ) => parseInt( p1 ) - parseInt( p2 ) ) .map( ( [ , arr ] ) => arr ); }; @@ -222,17 +303,17 @@ const Directives = ( { priorityLevels: [ currentPriorityLevel, ...nextPriorityLevels ], element, originalProps, - previousScope = {}, -} ) => { + previousScope, +}: DirectivesProps ) => { // Initialize the scope of this element. These scopes are different per each // level because each level has a different context, but they share the same // element ref, state and props. - const scope = useRef( {} ).current; + const scope = useRef< Scope >( {} as Scope ).current; scope.evaluate = useCallback( getEvaluate( { scope } ), [] ); scope.context = useContext( context ); /* eslint-disable react-hooks/rules-of-hooks */ - scope.ref = previousScope.ref || useRef( null ); - scope.state = previousScope.state || useRef( deepSignal( {} ) ).current; + scope.ref = previousScope?.ref || useRef( null ); + scope.state = previousScope?.state || useRef( deepSignal( {} ) ).current; /* eslint-enable react-hooks/rules-of-hooks */ // Create a fresh copy of the vnode element and add the props to the scope. @@ -276,7 +357,7 @@ const Directives = ( { // Preact Options Hook called each time a vnode is created. const old = options.vnode; -options.vnode = ( vnode ) => { +options.vnode = ( vnode: VNode< any > ) => { if ( vnode.props.__directives ) { const props = vnode.props; const directives = props.__directives; @@ -292,7 +373,7 @@ options.vnode = ( vnode ) => { priorityLevels, originalProps: props, type: vnode.type, - element: h( vnode.type, props ), + element: h( vnode.type as any, props ), top: true, }; vnode.type = Directives; diff --git a/packages/interactivity/src/router.js b/packages/interactivity/src/router.js index 68d1bc677addf3..1082d43ff3a6a6 100644 --- a/packages/interactivity/src/router.js +++ b/packages/interactivity/src/router.js @@ -59,8 +59,18 @@ const regionsToVdom = ( dom ) => { return { regions, title }; }; -// Prefetch a page. We store the promise to avoid triggering a second fetch for -// a page if a fetching has already started. +/** + * Prefetchs the page with the passed URL. + * + * The function normalizes the URL and stores internally the fetch promise, to + * avoid triggering a second fetch for an ongoing request. + * + * @param {string} url The page URL. + * @param {Object} [options] Options object. + * @param {boolean} [options.force] Force fetching the URL again. + * @param {string} [options.html] HTML string to be used instead of fetching + * the requested URL. + */ export const prefetch = ( url, options = {} ) => { url = cleanUrl( url ); if ( options.force || ! pages.has( url ) ) { @@ -84,7 +94,26 @@ const renderRegions = ( page ) => { // Variable to store the current navigation. let navigatingTo = ''; -// Navigate to a new page. +/** + * Navigates to the specified page. + * + * This function normalizes the passed href, fetchs the page HTML if needed, and + * updates any interactive regions whose contents have changed. It also creates + * a new entry in the browser session history. + * + * @param {string} href The page href. + * @param {Object} [options] Options object. + * @param {boolean} [options.force] If true, it forces re-fetching the URL. + * @param {string} [options.html] HTML string to be used instead of fetching + * the requested URL. + * @param {boolean} [options.replace] If true, it replaces the current entry in + * the browser session history. + * @param {number} [options.timeout] Time until the navigation is aborted, in + * milliseconds. Default is 10000. + * + * @return {Promise} Promise that resolves once the navigation is completed or + * aborted. + */ export const navigate = async ( href, options = {} ) => { const url = cleanUrl( href ); navigatingTo = href; diff --git a/packages/interactivity/src/store.ts b/packages/interactivity/src/store.ts index 1e9ab7e1a8f46b..8463d1a0a51323 100644 --- a/packages/interactivity/src/store.ts +++ b/packages/interactivity/src/store.ts @@ -164,71 +164,88 @@ const handlers = { return result; }, }; +interface StoreOptions { + /** + * Property to block/unblock private store namespaces. + * + * If the passed value is `true`, it blocks the given namespace, making it + * accessible only trough the returned variables of the `store()` call. In + * the case a lock string is passed, it also blocks the namespace, but can + * be unblocked for other `store()` calls using the same lock string. + * + * @example + * ``` + * // The store can only be accessed where the `state` const can. + * const { state } = store( 'myblock/private', { ... }, { lock: true } ); + * ``` + * + * @example + * ``` + * // Other modules knowing `SECRET_LOCK_STRING` can access the namespace. + * const { state } = store( + * 'myblock/private', + * { ... }, + * { lock: 'SECRET_LOCK_STRING' } + * ); + * ``` + */ + lock?: boolean | string; +} -/** - * @typedef StoreProps Properties object passed to `store`. - * @property {Object} state State to be added to the global store. All the - * properties included here become reactive. - */ - -/** - * @typedef StoreOptions Options object. - */ +const universalUnlock = + 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; /** - * Extends the Interactivity API global store with the passed properties. + * Extends the Interactivity API global store adding the passed properties to + * the given namespace. It also returns stable references to the namespace + * content. * - * These props typically consist of `state`, which is reactive, and other - * properties like `selectors`, `actions`, `effects`, etc. which can store - * callbacks and derived state. These props can then be referenced by any - * directive to make the HTML interactive. + * These props typically consist of `state`, which is the reactive part of the + * store ― which means that any directive referencing a state property will be + * re-rendered anytime it changes ― and function properties like `actions` and + * `callbacks`, mostly used for event handlers. These props can then be + * referenced by any directive to make the HTML interactive. * * @example * ```js - * store({ + * const { state } = store( 'counter', { * state: { - * counter: { value: 0 }, + * value: 0, + * get double() { return state.value * 2; }, * }, * actions: { - * counter: { - * increment: ({ state }) => { - * state.counter.value += 1; - * }, + * increment() { + * state.value += 1; * }, * }, - * }); + * } ); * ``` * * The code from the example above allows blocks to subscribe and interact with * the store by using directives in the HTML, e.g.: * * ```html - * <div data-wp-interactive> + * <div data-wp-interactive='{ "namespace": "counter" }'> * <button - * data-wp-text="state.counter.value" - * data-wp-on--click="actions.counter.increment" + * data-wp-text="state.double" + * data-wp-on--click="actions.increment" * > * 0 * </button> * </div> * ``` + * @param namespace The store namespace to interact with. + * @param storePart Properties to add to the store namespace. + * @param options Options for the given namespace. * - * @param {StoreProps} properties Properties to be added to the global store. - * @param {StoreOptions} [options] Options passed to the `store` call. + * @return A reference to the namespace content. */ - -interface StoreOptions { - lock?: boolean | string; -} - -const universalUnlock = - 'I acknowledge that using a private store means my plugin will inevitably break on the next store release.'; - export function store< S extends object = {} >( namespace: string, storePart?: S, options?: StoreOptions ): S; + export function store< T extends object >( namespace: string, storePart?: T, From a0943f2fba4a5df0e5ee051bfc567b7f6f7f1942 Mon Sep 17 00:00:00 2001 From: Florent Hernandez <lithrel@randomdomainname.net> Date: Tue, 5 Dec 2023 18:29:49 +0100 Subject: [PATCH 036/325] wp-env: Migrate to Compose V2 (#51339) Update docker-compose package to 0.24.1 Use v2 as dockerCompose command Fixes https://github.com/WordPress/gutenberg/issues/51249 --- package-lock.json | 54 +++++++++++++++++++--------- packages/env/CHANGELOG.md | 4 +++ packages/env/lib/cli.js | 4 +-- packages/env/lib/commands/clean.js | 2 +- packages/env/lib/commands/destroy.js | 2 +- packages/env/lib/commands/logs.js | 2 +- packages/env/lib/commands/run.js | 3 +- packages/env/lib/commands/start.js | 2 +- packages/env/lib/commands/stop.js | 2 +- packages/env/lib/test/cli.js | 2 +- packages/env/lib/wordpress.js | 2 +- packages/env/package.json | 2 +- test/unit/jest.config.js | 4 +++ 13 files changed, 57 insertions(+), 28 deletions(-) diff --git a/package-lock.json b/package-lock.json index 80a3f384e808ed..dfd755f2884cdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25688,15 +25688,6 @@ "node": ">=6" } }, - "node_modules/docker-compose": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.22.2.tgz", - "integrity": "sha512-iXWb5+LiYmylIMFXvGTYsjI1F+Xyx78Jm/uj1dxwwZLbWkUdH6yOXY5Nr3RjbYX15EgbGJCq78d29CmWQQQMPg==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", @@ -55484,7 +55475,7 @@ "dependencies": { "chalk": "^4.0.0", "copy-dir": "^1.3.0", - "docker-compose": "^0.22.2", + "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", "inquirer": "^7.1.0", @@ -55513,6 +55504,18 @@ "node": ">=12" } }, + "packages/env/node_modules/docker-compose": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", + "integrity": "sha512-2/WLvA7UZ6A2LDLQrYW0idKipmNBWhtfvrn2yzjC5PnHDzuFVj1zAZN6MJxVMKP0zZH8uzAK6OwVZYHGuyCmTw==", + "dev": true, + "dependencies": { + "yaml": "^2.2.2" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "packages/env/node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -55565,6 +55568,15 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "packages/env/node_modules/yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, "packages/env/node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -70702,7 +70714,7 @@ "requires": { "chalk": "^4.0.0", "copy-dir": "^1.3.0", - "docker-compose": "^0.22.2", + "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", "inquirer": "^7.1.0", @@ -70725,6 +70737,14 @@ "wrap-ansi": "^7.0.0" } }, + "docker-compose": { + "version": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.24.2.tgz", + "integrity": "sha512-2/WLvA7UZ6A2LDLQrYW0idKipmNBWhtfvrn2yzjC5PnHDzuFVj1zAZN6MJxVMKP0zZH8uzAK6OwVZYHGuyCmTw==", + "dev": true, + "requires": { + "yaml": "^2.2.2" + } + }, "emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -70765,6 +70785,12 @@ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true }, + "yaml": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz", + "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==", + "dev": true + }, "yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -76981,12 +77007,6 @@ "@leichtgewicht/ip-codec": "^2.0.1" } }, - "docker-compose": { - "version": "0.22.2", - "resolved": "https://registry.npmjs.org/docker-compose/-/docker-compose-0.22.2.tgz", - "integrity": "sha512-iXWb5+LiYmylIMFXvGTYsjI1F+Xyx78Jm/uj1dxwwZLbWkUdH6yOXY5Nr3RjbYX15EgbGJCq78d29CmWQQQMPg==", - "dev": true - }, "doctrine": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index aeae4f73e766fe..8b39bea46f785e 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Change + +- Update Docker usage to `docker compose` V2 following [deprecation](https://docs.docker.com/compose/migrate/) of `docker-compose` V1. + ## 8.13.0 (2023-11-29) ## 8.12.0 (2023-11-16) diff --git a/packages/env/lib/cli.js b/packages/env/lib/cli.js index 1788315b60b9db..896df6cd59fed0 100644 --- a/packages/env/lib/cli.js +++ b/packages/env/lib/cli.js @@ -58,10 +58,10 @@ const withSpinner = 'err' in error && 'out' in error ) { - // Error is a docker-compose error. That means something docker-related failed. + // Error is a docker compose error. That means something docker-related failed. // https://github.com/PDMLab/docker-compose/blob/HEAD/src/index.ts spinner.fail( - 'Error while running docker-compose command.' + 'Error while running docker compose command.' ); if ( error.out ) { process.stdout.write( error.out ); diff --git a/packages/env/lib/commands/clean.js b/packages/env/lib/commands/clean.js index e3977b3b63b8c0..587080eee99db1 100644 --- a/packages/env/lib/commands/clean.js +++ b/packages/env/lib/commands/clean.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); /** * Internal dependencies diff --git a/packages/env/lib/commands/destroy.js b/packages/env/lib/commands/destroy.js index fbbff0c8a28982..20f76250271a9e 100644 --- a/packages/env/lib/commands/destroy.js +++ b/packages/env/lib/commands/destroy.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); const util = require( 'util' ); const fs = require( 'fs' ).promises; const path = require( 'path' ); diff --git a/packages/env/lib/commands/logs.js b/packages/env/lib/commands/logs.js index 3a749b20b3dab6..b581835b2f9945 100644 --- a/packages/env/lib/commands/logs.js +++ b/packages/env/lib/commands/logs.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); /** * Internal dependencies diff --git a/packages/env/lib/commands/run.js b/packages/env/lib/commands/run.js index 734d90603c3919..88dc99374afbeb 100644 --- a/packages/env/lib/commands/run.js +++ b/packages/env/lib/commands/run.js @@ -80,6 +80,7 @@ function spawnCommandDirectly( config, container, command, envCwd, spinner ) { ); const composeCommand = [ + 'compose', '-f', config.dockerComposeConfigPath, 'exec', @@ -100,7 +101,7 @@ function spawnCommandDirectly( config, container, command, envCwd, spinner ) { // cannot use it to spawn an interactive command. Thus, we run docker- // compose on the CLI directly. const childProc = spawn( - 'docker-compose', + 'docker', composeCommand, { stdio: 'inherit' }, spinner diff --git a/packages/env/lib/commands/start.js b/packages/env/lib/commands/start.js index 2765e9c4e31984..4203ac74632287 100644 --- a/packages/env/lib/commands/start.js +++ b/packages/env/lib/commands/start.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); const util = require( 'util' ); const path = require( 'path' ); const fs = require( 'fs' ).promises; diff --git a/packages/env/lib/commands/stop.js b/packages/env/lib/commands/stop.js index 3700c3f2aa5815..5393ef8c6a000f 100644 --- a/packages/env/lib/commands/stop.js +++ b/packages/env/lib/commands/stop.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); /** * Internal dependencies diff --git a/packages/env/lib/test/cli.js b/packages/env/lib/test/cli.js index ba850e3259f4c8..542aea598a42fa 100644 --- a/packages/env/lib/test/cli.js +++ b/packages/env/lib/test/cli.js @@ -138,7 +138,7 @@ describe( 'env cli', () => { await env.start.mock.results[ 0 ].value.catch( () => {} ); expect( spinner.fail ).toHaveBeenCalledWith( - 'Error while running docker-compose command.' + 'Error while running docker compose command.' ); expect( process.stderr.write ).toHaveBeenCalledWith( 'failure error' ); expect( process.exit ).toHaveBeenCalledWith( 1 ); diff --git a/packages/env/lib/wordpress.js b/packages/env/lib/wordpress.js index e8c20aa70f2158..423547fad688b5 100644 --- a/packages/env/lib/wordpress.js +++ b/packages/env/lib/wordpress.js @@ -2,7 +2,7 @@ /** * External dependencies */ -const dockerCompose = require( 'docker-compose' ); +const { v2: dockerCompose } = require( 'docker-compose' ); const util = require( 'util' ); const fs = require( 'fs' ).promises; const path = require( 'path' ); diff --git a/packages/env/package.json b/packages/env/package.json index 94ee81a31d59b6..cb362b6c9f3d1a 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -34,7 +34,7 @@ "dependencies": { "chalk": "^4.0.0", "copy-dir": "^1.3.0", - "docker-compose": "^0.22.2", + "docker-compose": "^0.24.3", "extract-zip": "^1.6.7", "got": "^11.8.5", "inquirer": "^7.1.0", diff --git a/test/unit/jest.config.js b/test/unit/jest.config.js index 9d19a5c9feb2f9..38459b631fea4b 100644 --- a/test/unit/jest.config.js +++ b/test/unit/jest.config.js @@ -39,6 +39,10 @@ module.exports = { transform: { '^.+\\.[jt]sx?$': '<rootDir>/test/unit/scripts/babel-transformer.js', }, + transformIgnorePatterns: [ + '/node_modules/(?!(docker-compose|yaml)/)', + '\\.pnp\\.[^\\/]+$', + ], snapshotSerializers: [ '@emotion/jest/serializer', 'snapshot-diff/serializer', From 6ab61f62bda24440f2c7e32deb02aace3dee8a33 Mon Sep 17 00:00:00 2001 From: Nikita <nkdevinfo@gmail.com> Date: Tue, 5 Dec 2023 21:09:07 +0300 Subject: [PATCH 037/325] Fix: PHP 8.1 deprecated warning strpos() (#56171) * Update layout.php * Update layout.php --- lib/block-supports/layout.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/block-supports/layout.php b/lib/block-supports/layout.php index d35c963d0bed48..db02364b707901 100644 --- a/lib/block-supports/layout.php +++ b/lib/block-supports/layout.php @@ -819,7 +819,8 @@ function gutenberg_render_layout_support_flag( $block_content, $block ) { break; } - if ( false !== strpos( $processor->get_attribute( 'class' ), $inner_block_wrapper_classes ) ) { + $class_attribute = $processor->get_attribute( 'class' ); + if ( is_string( $class_attribute ) && false !== strpos( $class_attribute, $inner_block_wrapper_classes ) ) { break; } } while ( $processor->next_tag() ); From 14648d5498ed775bce98a57dfb7a1fb1dc0c1576 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 5 Dec 2023 20:18:11 +0200 Subject: [PATCH 038/325] Block editor: hooks: subscribe only to relevant attributes (#56783) --- packages/block-editor/src/hooks/background.js | 48 ++++++++++--------- packages/block-editor/src/hooks/border.js | 24 ++++++---- packages/block-editor/src/hooks/color.js | 39 ++++++++------- packages/block-editor/src/hooks/dimensions.js | 35 ++++++++------ packages/block-editor/src/hooks/padding.js | 4 +- packages/block-editor/src/hooks/style.js | 33 +++++++++++-- packages/block-editor/src/hooks/typography.js | 25 ++++++---- 7 files changed, 130 insertions(+), 78 deletions(-) diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index b0f93fa8b2e060..342db267ff3315 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -24,6 +24,7 @@ import { Platform, useCallback, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { getFilename } from '@wordpress/url'; +import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -41,13 +42,13 @@ export const IMAGE_BACKGROUND_TYPE = 'image'; * Checks if there is a current value in the background image block support * attributes. * - * @param {Object} props Block props. + * @param {Object} style Style attribute. * @return {boolean} Whether or not the block has a background image value set. */ -export function hasBackgroundImageValue( props ) { +export function hasBackgroundImageValue( style ) { const hasValue = - !! props.attributes.style?.background?.backgroundImage?.id || - !! props.attributes.style?.background?.backgroundImage?.url; + !! style?.background?.backgroundImage?.id || + !! style?.background?.backgroundImage?.url; return hasValue; } @@ -82,13 +83,10 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { * Resets the background image block support attributes. This can be used when disabling * the background image controls for a block via a `ToolsPanel`. * - * @param {Object} props Block props. - * @param {Object} props.attributes Block's attributes. - * @param {Object} props.setAttributes Function to set block's attributes. + * @param {Object} style Style attribute. + * @param {Function} setAttributes Function to set block's attributes. */ -export function resetBackgroundImage( { attributes = {}, setAttributes } ) { - const { style = {} } = attributes; - +export function resetBackgroundImage( style = {}, setAttributes ) { setAttributes( { style: cleanEmptyObject( { ...style, @@ -145,11 +143,13 @@ function InspectorImagePreview( { label, filename, url: imgUrl } ) { ); } -function BackgroundImagePanelItem( props ) { - const { attributes, clientId, setAttributes } = props; - - const { id, title, url } = - attributes.style?.background?.backgroundImage || {}; +function BackgroundImagePanelItem( { clientId, setAttributes } ) { + const style = useSelect( + ( select ) => + select( blockEditorStore ).getBlockAttributes( clientId )?.style, + [ clientId ] + ); + const { id, title, url } = style?.background?.backgroundImage || {}; const replaceContainerRef = useRef(); @@ -167,9 +167,9 @@ function BackgroundImagePanelItem( props ) { const onSelectMedia = ( media ) => { if ( ! media || ! media.url ) { const newStyle = { - ...attributes.style, + ...style, background: { - ...attributes.style?.background, + ...style?.background, backgroundImage: undefined, }, }; @@ -201,9 +201,9 @@ function BackgroundImagePanelItem( props ) { } const newStyle = { - ...attributes.style, + ...style, background: { - ...attributes.style?.background, + ...style?.background, backgroundImage: { url: media.url, id: media.id, @@ -244,14 +244,14 @@ function BackgroundImagePanelItem( props ) { }; }, [] ); - const hasValue = hasBackgroundImageValue( props ); + const hasValue = hasBackgroundImageValue( style ); return ( <ToolsPanelItem className="single-column" hasValue={ () => hasValue } label={ __( 'Background image' ) } - onDeselect={ () => resetBackgroundImage( props ) } + onDeselect={ () => resetBackgroundImage( style, setAttributes ) } isShownByDefault={ true } resetAllFilter={ resetAllFilter } panelId={ clientId } @@ -286,7 +286,7 @@ function BackgroundImagePanelItem( props ) { // closed and focus is redirected to the dropdown toggle button. toggleButton?.focus(); toggleButton?.click(); - resetBackgroundImage( props ); + resetBackgroundImage( style, setAttributes ); } } > { __( 'Reset ' ) } @@ -302,7 +302,7 @@ function BackgroundImagePanelItem( props ) { ); } -export function BackgroundImagePanel( props ) { +function BackgroundImagePanelPure( props ) { const [ backgroundImage ] = useSettings( 'background.backgroundImage' ); if ( ! backgroundImage || @@ -317,3 +317,5 @@ export function BackgroundImagePanel( props ) { </InspectorControls> ); } + +export const BackgroundImagePanel = pure( BackgroundImagePanelPure ); diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index c2d30d5501576b..29e4afd2f018ca 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -8,9 +8,10 @@ import classnames from 'classnames'; */ import { getBlockSupport } from '@wordpress/blocks'; import { __experimentalHasSplitBorders as hasSplitBorders } from '@wordpress/components'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { Platform, useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -27,6 +28,7 @@ import { useHasBorderPanel, BorderPanel as StylesBorderPanel, } from '../components/global-styles'; +import { store as blockEditorStore } from '../store'; export const BORDER_SUPPORT_KEY = '__experimentalBorder'; @@ -135,16 +137,18 @@ function BordersInspectorControl( { children, resetAllFilter } ) { ); } -export function BorderPanel( props ) { - const { clientId, name, attributes, setAttributes } = props; +function BorderPanelPure( { clientId, name, setAttributes } ) { const settings = useBlockSettings( name ); const isEnabled = useHasBorderPanel( settings ); + function selector( select ) { + const { style, borderColor } = + select( blockEditorStore ).getBlockAttributes( clientId ) || {}; + return { style, borderColor }; + } + const { style, borderColor } = useSelect( selector, [ clientId ] ); const value = useMemo( () => { - return attributesToStyle( { - style: attributes.style, - borderColor: attributes.borderColor, - } ); - }, [ attributes.style, attributes.borderColor ] ); + return attributesToStyle( { style, borderColor } ); + }, [ style, borderColor ] ); const onChange = ( newStyle ) => { setAttributes( styleToAttributes( newStyle ) ); @@ -154,7 +158,7 @@ export function BorderPanel( props ) { return null; } - const defaultControls = getBlockSupport( props.name, [ + const defaultControls = getBlockSupport( name, [ BORDER_SUPPORT_KEY, '__experimentalDefaultControls', ] ); @@ -171,6 +175,8 @@ export function BorderPanel( props ) { ); } +export const BorderPanel = pure( BorderPanelPure ); + /** * Determine whether there is block support for border properties. * diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 19fe4b0ea5ecd4..94bcc599dd6371 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -9,7 +9,8 @@ import classnames from 'classnames'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, Platform, useCallback } from '@wordpress/element'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -32,6 +33,7 @@ import { default as StylesColorPanel, } from '../components/global-styles/color-panel'; import BlockColorContrastChecker from './contrast-checker'; +import { store as blockEditorStore } from '../store'; export const COLOR_SUPPORT_KEY = 'color'; @@ -289,23 +291,26 @@ function ColorInspectorControl( { children, resetAllFilter } ) { ); } -export function ColorEdit( props ) { - const { clientId, name, attributes, setAttributes } = props; +function ColorEditPure( { clientId, name, setAttributes } ) { const settings = useBlockSettings( name ); const isEnabled = useHasColorPanel( settings ); + function selector( select ) { + const { style, textColor, backgroundColor, gradient } = + select( blockEditorStore ).getBlockAttributes( clientId ) || {}; + return { style, textColor, backgroundColor, gradient }; + } + const { style, textColor, backgroundColor, gradient } = useSelect( + selector, + [ clientId ] + ); const value = useMemo( () => { return attributesToStyle( { - style: attributes.style, - textColor: attributes.textColor, - backgroundColor: attributes.backgroundColor, - gradient: attributes.gradient, + style, + textColor, + backgroundColor, + gradient, } ); - }, [ - attributes.style, - attributes.textColor, - attributes.backgroundColor, - attributes.gradient, - ] ); + }, [ style, textColor, backgroundColor, gradient ] ); const onChange = ( newStyle ) => { setAttributes( styleToAttributes( newStyle ) ); @@ -315,7 +320,7 @@ export function ColorEdit( props ) { return null; } - const defaultControls = getBlockSupport( props.name, [ + const defaultControls = getBlockSupport( name, [ COLOR_SUPPORT_KEY, '__experimentalDefaultControls', ] ); @@ -328,7 +333,7 @@ export function ColorEdit( props ) { // Deactivating it requires `enableContrastChecker` to have // an explicit value of `false`. false !== - getBlockSupport( props.name, [ + getBlockSupport( name, [ COLOR_SUPPORT_KEY, 'enableContrastChecker', ] ); @@ -343,7 +348,7 @@ export function ColorEdit( props ) { defaultControls={ defaultControls } enableContrastChecker={ false !== - getBlockSupport( props.name, [ + getBlockSupport( name, [ COLOR_SUPPORT_KEY, 'enableContrastChecker', ] ) @@ -356,6 +361,8 @@ export function ColorEdit( props ) { ); } +export const ColorEdit = pure( ColorEditPure ); + /** * This adds inline styles for color palette colors. * Ideally, this is not needed and themes should load their palettes on the editor. diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 084763f0c21b16..4e2b17f363bddf 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -2,9 +2,10 @@ * WordPress dependencies */ import { useState, useEffect, useCallback } from '@wordpress/element'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { getBlockSupport } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; +import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -65,17 +66,19 @@ function DimensionsInspectorControl( { children, resetAllFilter } ) { ); } -export function DimensionsPanel( props ) { - const { - clientId, - name, - attributes, - setAttributes, - __unstableParentLayout, - } = props; +function DimensionsPanelPure( { + clientId, + name, + setAttributes, + __unstableParentLayout, +} ) { const settings = useBlockSettings( name, __unstableParentLayout ); const isEnabled = useHasDimensionsPanel( settings ); - const value = attributes.style; + const value = useSelect( + ( select ) => + select( blockEditorStore ).getBlockAttributes( clientId )?.style, + [ clientId ] + ); const [ visualizedProperty, setVisualizedProperty ] = useVisualizer(); const onChange = ( newStyle ) => { setAttributes( { @@ -87,11 +90,11 @@ export function DimensionsPanel( props ) { return null; } - const defaultDimensionsControls = getBlockSupport( props.name, [ + const defaultDimensionsControls = getBlockSupport( name, [ DIMENSIONS_SUPPORT_KEY, '__experimentalDefaultControls', ] ); - const defaultSpacingControls = getBlockSupport( props.name, [ + const defaultSpacingControls = getBlockSupport( name, [ SPACING_SUPPORT_KEY, '__experimentalDefaultControls', ] ); @@ -114,19 +117,23 @@ export function DimensionsPanel( props ) { { !! settings?.spacing?.padding && ( <PaddingVisualizer forceShow={ visualizedProperty === 'padding' } - { ...props } + clientId={ clientId } + value={ value } /> ) } { !! settings?.spacing?.margin && ( <MarginVisualizer forceShow={ visualizedProperty === 'margin' } - { ...props } + clientId={ clientId } + value={ value } /> ) } </> ); } +export const DimensionsPanel = pure( DimensionsPanelPure ); + /** * @deprecated */ diff --git a/packages/block-editor/src/hooks/padding.js b/packages/block-editor/src/hooks/padding.js index b6e4e50e30f9cf..ca4436153d122c 100644 --- a/packages/block-editor/src/hooks/padding.js +++ b/packages/block-editor/src/hooks/padding.js @@ -16,11 +16,11 @@ function getComputedCSS( element, property ) { .getPropertyValue( property ); } -export function PaddingVisualizer( { clientId, attributes, forceShow } ) { +export function PaddingVisualizer( { clientId, value, forceShow } ) { const blockElement = useBlockElement( clientId ); const [ style, setStyle ] = useState(); - const padding = attributes?.style?.spacing?.padding; + const padding = value?.spacing?.padding; useEffect( () => { if ( diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index d74e10b0208f1c..8b3a475e1babe5 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -361,16 +361,39 @@ export const withBlockStyleControls = createHigherOrderComponent( const shouldDisplayControls = useDisplayBlockControls(); const blockEditingMode = useBlockEditingMode(); + const { clientId, name, setAttributes, __unstableParentLayout } = props; return ( <> { shouldDisplayControls && blockEditingMode === 'default' && ( <> - <ColorEdit { ...props } /> - <BackgroundImagePanel { ...props } /> - <TypographyPanel { ...props } /> - <BorderPanel { ...props } /> - <DimensionsPanel { ...props } /> + <ColorEdit + clientId={ clientId } + name={ name } + setAttributes={ setAttributes } + /> + <BackgroundImagePanel + clientId={ clientId } + name={ name } + setAttributes={ setAttributes } + /> + <TypographyPanel + clientId={ clientId } + name={ name } + setAttributes={ setAttributes } + __unstableParentLayout={ __unstableParentLayout } + /> + <BorderPanel + clientId={ clientId } + name={ name } + setAttributes={ setAttributes } + /> + <DimensionsPanel + clientId={ clientId } + name={ name } + setAttributes={ setAttributes } + __unstableParentLayout={ __unstableParentLayout } + /> </> ) } <BlockEdit key="edit" { ...props } /> diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index c7d1a6ba3b1443..7d0e5e1c318d56 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -3,6 +3,8 @@ */ import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { useMemo, useCallback } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; +import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -17,6 +19,7 @@ import { LINE_HEIGHT_SUPPORT_KEY } from './line-height'; import { FONT_FAMILY_SUPPORT_KEY } from './font-family'; import { FONT_SIZE_SUPPORT_KEY } from './font-size'; import { cleanEmptyObject, useBlockSettings } from './utils'; +import { store as blockEditorStore } from '../store'; function omit( object, keys ) { return Object.fromEntries( @@ -106,22 +109,24 @@ function TypographyInspectorControl( { children, resetAllFilter } ) { ); } -export function TypographyPanel( { +function TypographyPanelPure( { clientId, name, - attributes, setAttributes, __unstableParentLayout, } ) { + function selector( select ) { + const { style, fontFamily, fontSize } = + select( blockEditorStore ).getBlockAttributes( clientId ) || {}; + return { style, fontFamily, fontSize }; + } + const { style, fontFamily, fontSize } = useSelect( selector, [ clientId ] ); const settings = useBlockSettings( name, __unstableParentLayout ); const isEnabled = useHasTypographyPanel( settings ); - const value = useMemo( () => { - return attributesToStyle( { - style: attributes.style, - fontFamily: attributes.fontFamily, - fontSize: attributes.fontSize, - } ); - }, [ attributes.style, attributes.fontSize, attributes.fontFamily ] ); + const value = useMemo( + () => attributesToStyle( { style, fontFamily, fontSize } ), + [ style, fontSize, fontFamily ] + ); const onChange = ( newStyle ) => { setAttributes( styleToAttributes( newStyle ) ); @@ -148,6 +153,8 @@ export function TypographyPanel( { ); } +export const TypographyPanel = pure( TypographyPanelPure ); + export const hasTypographySupport = ( blockName ) => { return TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => hasBlockSupport( blockName, key ) From b6216dec5ce6e37df1114e1d2bf4098d92bd4313 Mon Sep 17 00:00:00 2001 From: Carlos Garcia <fluiddot@gmail.com> Date: Tue, 5 Dec 2023 19:47:28 +0100 Subject: [PATCH 039/325] Mobile Release v1.109.2 (#56782) * Release script: Update react-native-editor version to 1.109.0 * Release script: Update CHANGELOG for version 1.109.0 * Release script: Update podfile * Update `react-native-editor` changelog * Update `react-native-editor` changelog * Mobile - Fix issue when backspacing in an empty Paragraph block (#56496) * Bring changes from #55134 to the mobile code * Mobile - RichText - Force focus when the block is selected but the textinput is not, for cases when merging blocks. * Update Buttons integration test due to a change in the logic of the app where deleting the only button available does not remove the block * Mobile - Heading block - Adds integration test for merging a Heading block with an empty Paragraph block * Mobile - Paragraph block - Adds integration test to check that backspacing in an empty Paragraph block merges succesfully with the previous block and keeps the focus on the TextInput * Mobile - RichText - Set selection values to be the last character position when merging and adds some comments to explain what is doing * Mobile - Paragraph block test - Use focusTextInput to check the TextInput is in focused instead of checking for the fomatting toolbar button * Rename shouldFocusTextInputAfterUpdate to shouldFocusTextInputAfterMerge * Update CHANGELOG * Release script: Update react-native-editor version to 1.109.1 * Release script: Update CHANGELOG for version 1.109.1 * Release script: Update podfile * [RNMobile] Fixes a crash on pasting MS Word list markup (#56653) * Add polyfill for Element.prototype.remove * Enable unit tests of `raw-handling` API filter `ms-list-converter` * Update `react-native-editor` changelog * [RNMobile] Fix issue related to receiving undefined ref in text color format (#56686) * Fix issue related to receiving undefined ref in text color format In rare cases, `TextColorEdit` might receive the `RichText` ref as undefined. This ref is used to get the background color of the text and use it in the toolbar button. * Update `react-native-editor` changelog * Add test to cover undefined `contentRef` case * Correct typo in `changelog` * [RNMobile] Fix HTML to blocks conversion when no transformations are available (#56723) * Add native workaround for HTML block in `htmlToBlocks` * Add raw handling tests This file is a clone of the same `blocks-raw-handling.js` file located in `gutenberg/test/integration`. The reason for the separation is that several of the test cases fail in the native version. For now, we are going to skip them, but we'd need to work on them in the future. Once all issues in tests are addressed, we'll remove this file in favor of the original one. * Update blocks raw handling test snapshot with original values * Disable more pasteHandler test cases due to not matching test snapshot * Comment obsolete snapshots of blocks raw handling tests The reason for commenting them instead of removing is that, in the future, we'll restore them once we address the failing test cases. * RichText (native): remove HTML check in getFormatColors (#56684) * Mobile - Global Styles - Fix issue with custom color variables not being parsed (#56752) * [RNMobile] Address `NullPointerException` crash in `AztecHeadingSpan` (#56757) Address rare cases where a null value is passed to a heading block, causing a crash. * Release script: Update react-native-editor version to 1.109.2 * Release script: Update CHANGELOG for version 1.109.2 * Release script: Update podfile --------- Co-authored-by: Gerardo Pacheco <gerardo.pacheco@automattic.com> Co-authored-by: Ella <4710635+ellatrix@users.noreply.github.com> Co-authored-by: Siobhan Bamber <siobhan@automattic.com> --- package-lock.json | 6 +++--- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 9 +++++++-- packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfd755f2884cdc..e4b4aeb47f6bad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56165,7 +56165,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.109.1", + "version": "1.109.2", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56178,7 +56178,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.109.1", + "version": "1.109.2", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56189,7 +56189,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.109.1", + "version": "1.109.2", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index e814be90943a74..fbf34269306eea 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.109.1", + "version": "1.109.2", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index ca7cfc3de79bcf..6b9bdb782d66d1 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.109.1", + "version": "1.109.2", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 411499dc1dd7cd..ea7b841879c4fb 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,11 +12,16 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] [internal] Move InserterButton from components package to block-editor package [#56494] -## 1.109.1 -- [***] Fix issue when backspacing in an empty Paragraph block [#56496] +## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] - [**] Fixes a crash on pasting MS Word list markup [#56653] - [**] Address rare cases where a null value is passed to a heading block, causing a crash [#56757] +- [**] Fixes a crash related to HTML to blocks conversion when no transformations are available [#56723] +- [**] Fixes a crash related to undefined attributes in `getFormatColors` function of `RichText` component [#56684] +- [**] Fixes an issue with custom color variables not being parsed when using global styles [#56752] + +## 1.109.1 +- [***] Fix issue when backspacing in an empty Paragraph block [#56496] ## 1.109.0 - [*] Audio block: Improve legibility of audio file details on various background colors [#55627] diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index dd3021ab8a6dba..51b5554191ea8d 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.109.1): + - Gutenberg (1.109.2): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.109.1): + - RNTAztecView (1.109.2): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: ce2b737d183d0179cb86596412bad21d48eafdcb + Gutenberg: 2da422f5cdffef9f66fc57f87ddba4dbda5ceb9d hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: 8d9b3bd517873101ab1ea89948b45c601bcedea0 + RNTAztecView: dc2635b4d33818f4c113717ff67071c1e367ed8c SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index aa41fc9ffa1af1..bcc15b44b4ca1b 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.109.1", + "version": "1.109.2", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From d703927ac98b3887ef9dcf0ce959d114f0443404 Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Tue, 5 Dec 2023 16:55:13 -0500 Subject: [PATCH 040/325] Update formatting and fix grammar in the Block Editor Handbook readme (#56798) * Update formatting and grammar. * Fix formatting. --- docs/README.md | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/docs/README.md b/docs/README.md index b94a8d78d41a75..222b54209c7d62 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,8 +1,8 @@ # Block Editor Handbook -Hi! 👋 Welcome to the Block Editor Handbook. +👋 Welcome to the Block Editor Handbook. -The [**Block editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content, and is designed to create rich and flexible layouts for websites and digital products. +The [**Block Editor**](https://wordpress.org/gutenberg/) is a modern and up-to-date paradigm for WordPress site building and publishing. It uses a modular system of **Blocks** to compose and format content and is designed to create rich and flexible layouts for websites and digital products. The editor consists of several primary elements, as shown in the following figure: @@ -12,54 +12,44 @@ The elements highlighted in the figure are: 1. **Inserter**: A panel for inserting blocks into the content canvas 2. **Content canvas**: The content editor, which holds content created with blocks -3. **Settings sidebar**: A sidebar panel for configuring a block’s settings (among other things) +3. **Settings Sidebar**: A sidebar panel for configuring a block’s settings (among other things) -Through the Block editor, you create content modularly using Blocks. There are a number of [core blocks](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/) ready to be used, and you can also [create your own custom block](https://developer.wordpress.org/block-editor/getting-started/create-block/). +Through the Block editor, you create content modularly using Blocks. There are many [core blocks](https://developer.wordpress.org/block-editor/reference-guides/core-blocks/) ready to be used, and you can also [create your own custom block](https://developer.wordpress.org/block-editor/getting-started/create-block/). -A [Block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media element, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content that is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing). +A [Block](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#blocks) is a discrete element such as a Paragraph, Heading, Media, or Embed. Each block is treated as a separate element with individual editing and format controls. When all these components are pieced together, they make up the content that is then [stored in the WordPress database](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/#serialization-and-parsing). -The Block Editor is the result of the [work done on the **Gutenberg project**](https://developer.wordpress.org/block-editor/getting-started/faq/#what-is-gutenberg) which is aimed to revolutionize the WordPress editing experience. +The Block Editor is the result of the work done on the [**Gutenberg project**](https://developer.wordpress.org/block-editor/getting-started/faq/#what-is-gutenberg), which aims to revolutionize the WordPress editing experience. -Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended in a multitude of different ways. +Besides offering an [enhanced editing experience](https://wordpress.org/gutenberg/) through visual content creation tools, the Block Editor is also a powerful developer platform with a [rich feature set of APIs](https://developer.wordpress.org/block-editor/reference-guides/) that allow it to be manipulated and extended many different ways. ## Navigating this handbook This handbook is focused on block development and is divided into five sections, each serving a different purpose. -**[Getting Started](https://developer.wordpress.org/block-editor/getting-started/)** +- [**Getting Started**](https://developer.wordpress.org/block-editor/getting-started/) - For those just starting out with block development, this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/). Its [Glossary of terms](https://developer.wordpress.org/block-editor/getting-started/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/getting-started/faq/) should answer any outstanding questions you may have. -For those just starting out with block development this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/). Its [Glossary of terms](https://developer.wordpress.org/block-editor/getting-started/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/getting-started/faq/) should answer any outstanding questions you may have. +- [**How-to Guides**](https://developer.wordpress.org/block-editor/how-to-guides/) - Here, you can build on what you learned in the Getting Started section and learn how to solve particular problems you might encounter. You can also get tutorials and example code that you can reuse for projects such as [building a full-featured block](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/) or [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/). In addition, you can learn [How to use JavaScript with the Block Editor](https://developer.wordpress.org/block-editor/how-to-guides/javascript/). -**[How-to Guides](https://developer.wordpress.org/block-editor/how-to-guides/)** -Here you can build on what you learned in the Getting Started section and learn how to solve particular problems that you might encounter. You can also get tutorials, and example code that you can reuse, for projects such as [building a full-featured block](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/) or [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/). In addition you can learn [How to use JavaScript with the Block Editor](https://developer.wordpress.org/block-editor/how-to-guides/javascript/). +- [**Reference Guides**](https://developer.wordpress.org/block-editor/reference-guides/) - This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ -**[Reference Guides](https://developer.wordpress.org/block-editor/reference-guides/)** +- [**Explanations**](https://developer.wordpress.org/block-editor/explanations/) - This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. -This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API that you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ - - -**[Explanations](https://developer.wordpress.org/block-editor/explanations/)** - -This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. - -**[Contributor Guide](https://developer.wordpress.org/block-editor/contributors/)** - -Gutenberg is open source software and anyone is welcome to contribute to the project. This section details how to contribute and can help you choose in which way you want to contribute, whether that be with [code](https://developer.wordpress.org/block-editor/contributors/code/), with [design](https://developer.wordpress.org/block-editor/contributors/design/), with [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. +- [**Contributor Guide**](https://developer.wordpress.org/block-editor/contributors/) - Gutenberg is open source software, and anyone is welcome to contribute to the project. This section details how to contribute and can help you choose in which way you want to contribute, whether with [code](https://developer.wordpress.org/block-editor/contributors/code/), [design](https://developer.wordpress.org/block-editor/contributors/design/), [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. ## Further resources -This handbook should be considered the canonical resource for all things related to block development. However there are other resources that can help you. +This handbook should be considered the canonical resource for all things related to block development. However, there are other resources that can help you. - [**WordPress Developer Blog**](https://developer.wordpress.org/news/) - An ever-growing resource of technical articles covering specific topics related to block development and a wide variety of use cases. The blog is also an excellent way to [keep up with the latest developments in WordPress](https://developer.wordpress.org/news/tag/roundup/). - [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) -- [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [block-editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. +- [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [Block Editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. - [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository is another useful reference._ -- [**End User Documentation**](https://wordpress.org/documentation/) - Documentation site targeted to the end user (not developers) where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). +- [**End User Documentation**](https://wordpress.org/documentation/) - This documentation site is targeted to the end user (not developers), where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). ## Are you in the right place? From fc522eef4a64470dc74e556d0cd423289eaf9969 Mon Sep 17 00:00:00 2001 From: tellthemachines <tellthemachines@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:06:01 +1100 Subject: [PATCH 041/325] Match the front end layout classname in the editor. (#56774) --- packages/block-editor/src/hooks/layout.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 9b35c3dcd6076b..171290e180ee95 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -359,8 +359,9 @@ function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { : layout || defaultBlockLayout || {}; const layoutClasses = useLayoutClasses( attributes, name ); + const selectorPrefix = `wp-container-${ kebabCase( name ) }-layout-`; // Higher specificity to override defaults from theme.json. - const selector = `.wp-container-${ id }.wp-container-${ id }`; + const selector = `.${ selectorPrefix }${ id }.${ selectorPrefix }${ id }`; const [ blockGapSupport ] = useSettings( 'spacing.blockGap' ); const hasBlockGapSupport = blockGapSupport !== null; @@ -378,7 +379,7 @@ function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { // Attach a `wp-container-` id-based class name as well as a layout class name such as `is-layout-flex`. const layoutClassNames = classnames( { - [ `wp-container-${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. + [ `${ selectorPrefix }${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. }, layoutClasses ); From 0e98444b80a1f1ef96a45f48ee85df2664f22833 Mon Sep 17 00:00:00 2001 From: tellthemachines <tellthemachines@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:14:23 +1100 Subject: [PATCH 042/325] Fix sticky position in classic themes with appearance tools support (#56743) * Fix sticky position in classic themes with appearance tools support * Remove editor setting to detect appearance tools support * Delete unused file --- packages/block-editor/src/components/block-list/block.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index a95075c6f9b42c..ff5915981acdf9 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -161,6 +161,10 @@ function BlockListBlock( { !! wrapperProps[ 'data-align' ] && ! themeSupportsLayout; + // Support for sticky position in classic themes with alignment wrappers. + + const isSticky = className?.includes( 'is-position-sticky' ); + // For aligned blocks, provide a wrapper element so the block can be // positioned relative to the block column. // This is only kept for classic themes that don't support layout @@ -172,7 +176,7 @@ function BlockListBlock( { if ( isAligned ) { blockEdit = ( <div - className="wp-block" + className={ classnames( 'wp-block', isSticky && className ) } data-align={ wrapperProps[ 'data-align' ] } > { blockEdit } @@ -221,7 +225,7 @@ function BlockListBlock( { isTemporarilyEditingAsBlocks, }, dataAlign && themeSupportsLayout && `align${ dataAlign }`, - className + ! ( dataAlign && isSticky ) && className ), wrapperProps: restWrapperProps, isAligned, From 62ecf4a4bdb4aceee01031ca2e08adeb3a250bf4 Mon Sep 17 00:00:00 2001 From: Matias Benedetto <matias.benedetto@gmail.com> Date: Tue, 5 Dec 2023 16:17:25 -0600 Subject: [PATCH 043/325] Font Library: add font family and font face preview keys to schema (#56793) * Add preview key to theme.json schema * add preview key to php data sanitization schema * update description text Co-authored-by: Jeff Ong <jonger4@gmail.com> * update docs --------- Co-authored-by: Jeff Ong <jonger4@gmail.com> --- .../theme-json-reference/theme-json-living.md | 2 +- lib/class-wp-theme-json-gutenberg.php | 2 ++ schemas/json/theme.json | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/reference-guides/theme-json-reference/theme-json-living.md b/docs/reference-guides/theme-json-reference/theme-json-living.md index 24a5845381bfda..627fee6071816f 100644 --- a/docs/reference-guides/theme-json-reference/theme-json-living.md +++ b/docs/reference-guides/theme-json-reference/theme-json-living.md @@ -188,7 +188,7 @@ Settings related to typography. | textTransform | boolean | true | | | dropCap | boolean | true | | | fontSizes | array | | fluid, name, size, slug | -| fontFamilies | array | | fontFace, fontFamily, name, slug | +| fontFamilies | array | | fontFace, fontFamily, name, preview, slug | --- diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 9311001f2edd14..43a3772a1c3af0 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -431,6 +431,7 @@ class WP_Theme_JSON_Gutenberg { 'fontFamily' => null, 'name' => null, 'slug' => null, + 'preview' => null, 'fontFace' => array( array( 'ascentOverride' => null, @@ -446,6 +447,7 @@ class WP_Theme_JSON_Gutenberg { 'sizeAdjust' => null, 'src' => null, 'unicodeRange' => null, + 'preview' => null, ), ), ), diff --git a/schemas/json/theme.json b/schemas/json/theme.json index 782e59794a5d18..10695f493c40dd 100644 --- a/schemas/json/theme.json +++ b/schemas/json/theme.json @@ -623,6 +623,10 @@ "description": "CSS font-family value.", "type": "string" }, + "preview": { + "description": "URL to a preview image of the font family.", + "type": "string" + }, "fontFace": { "description": "Array of font-face declarations.", "type": "array", @@ -713,6 +717,10 @@ "unicodeRange": { "description": "CSS unicode-range value.", "type": "string" + }, + "preview": { + "description": "URL to a preview image of the font face.", + "type": "string" } }, "required": [ "fontFamily", "src" ], From d1d0b2ab17edcedb16714ce5d07aa118fdfa3b58 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Tue, 5 Dec 2023 16:35:10 -0600 Subject: [PATCH 044/325] Show template center UI when no block is selected (#56217) --- packages/edit-post/src/components/header/index.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 8ac8e47e01dbaa..3c0f57b9a69d1c 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -124,7 +124,9 @@ function Header( { className={ classnames( 'selected-block-tools-wrapper', { - 'is-collapsed': isBlockToolsCollapsed, + 'is-collapsed': + isEditingTemplate && + isBlockToolsCollapsed, } ) } > @@ -155,9 +157,11 @@ function Header( { <div className={ classnames( 'edit-post-header__center', { 'is-collapsed': + isEditingTemplate && + hasBlockSelected && ! isBlockToolsCollapsed && - isLargeViewport && - isEditingTemplate, + hasFixedToolbar && + isLargeViewport, } ) } > { isEditingTemplate && <DocumentActions /> } From a82ae41e58ba154cdaab22eae137dd06a002d61a Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 6 Dec 2023 09:00:37 +0200 Subject: [PATCH 045/325] Components: ToolsPanel: fix deregister/register on type (#56770) --- packages/components/CHANGELOG.md | 1 + .../src/tools-panel/tools-panel-item/hook.ts | 31 ++++++------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f3019c250a6c42..435a9bf22d9b20 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ ### Bug Fix - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). +- `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). ## 25.13.0 (2023-11-29) diff --git a/packages/components/src/tools-panel/tools-panel-item/hook.ts b/packages/components/src/tools-panel/tools-panel-item/hook.ts index 244349b6379eaf..fe415b8723a88f 100644 --- a/packages/components/src/tools-panel/tools-panel-item/hook.ts +++ b/packages/components/src/tools-panel/tools-panel-item/hook.ts @@ -52,11 +52,14 @@ export function useToolsPanelItem( __experimentalLastVisibleItemClass, } = useToolsPanelContext(); - const hasValueCallback = useCallback( hasValue, [ panelId, hasValue ] ); - const resetAllFilterCallback = useCallback( resetAllFilter, [ - panelId, - resetAllFilter, - ] ); + // hasValue is a new function on every render, so do not add it as a + // dependency to the useCallback hook! If needed, we should use a ref. + // eslint-disable-next-line react-hooks/exhaustive-deps + const hasValueCallback = useCallback( hasValue, [ panelId ] ); + // resetAllFilter is a new function on every render, so do not add it as a + // dependency to the useCallback hook! If needed, we should use a ref. + // eslint-disable-next-line react-hooks/exhaustive-deps + const resetAllFilterCallback = useCallback( resetAllFilter, [ panelId ] ); const previousPanelId = usePrevious( currentPanelId ); const hasMatchingPanel = @@ -126,27 +129,13 @@ export function useToolsPanelItem( const newValueSet = isValueSet && ! wasValueSet; // Notify the panel when an item's value has been set. - // - // 1. For default controls, this is so "reset" appears beside its menu item. - // 2. For optional controls, when the panel ID is `null`, it allows the - // panel to ensure the item is toggled on for display in the menu, given the - // value has been set external to the control. useEffect( () => { if ( ! newValueSet ) { return; } - if ( isShownByDefault || currentPanelId === null ) { - flagItemCustomization( label, menuGroup ); - } - }, [ - currentPanelId, - newValueSet, - isShownByDefault, - menuGroup, - label, - flagItemCustomization, - ] ); + flagItemCustomization( label, menuGroup ); + }, [ newValueSet, menuGroup, label, flagItemCustomization ] ); // Determine if the panel item's corresponding menu is being toggled and // trigger appropriate callback if it is. From 2f5b962fdf5c36c94d8be2b9924250ad445807c6 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Wed, 6 Dec 2023 08:49:56 +0100 Subject: [PATCH 046/325] Avoid to show unnecessary Tooltip for the Post Schedule button. (#56759) --- packages/editor/src/components/post-schedule/panel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/editor/src/components/post-schedule/panel.js b/packages/editor/src/components/post-schedule/panel.js index 2e725a06bc9fd7..899ecd9efaee7a 100644 --- a/packages/editor/src/components/post-schedule/panel.js +++ b/packages/editor/src/components/post-schedule/panel.js @@ -49,7 +49,7 @@ export default function PostSchedulePanel() { label ) } label={ fullLabel } - showTooltip + showTooltip={ label !== fullLabel } aria-expanded={ isOpen } > { label } From 0c0af1ad10c1324b8584f5ee0201e0c39efe4658 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 6 Dec 2023 18:37:26 +0900 Subject: [PATCH 047/325] Pattern inserter: fix Broken preview layout (#56814) --- packages/block-editor/src/components/block-preview/style.scss | 1 - packages/block-editor/src/components/inserter/style.scss | 4 ++++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-preview/style.scss b/packages/block-editor/src/components/block-preview/style.scss index c24de175eac9a0..9bdd85f66445f8 100644 --- a/packages/block-editor/src/components/block-preview/style.scss +++ b/packages/block-editor/src/components/block-preview/style.scss @@ -7,7 +7,6 @@ // 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%; - height: 100%; overflow: hidden; diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index be304a4b2a031a..a9038616aa5339 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -229,6 +229,10 @@ $block-inserter-tabs-height: 44px; display: block; } + .block-editor-block-preview__container { + height: 100%; + } + .block-editor-block-card { padding-left: 0; padding-right: 0; From 76f09f9b527b670126f915a2cf2966697d9f27cb Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Wed, 6 Dec 2023 10:10:42 +0000 Subject: [PATCH 048/325] [a11y][Dataviews] Fix: use span instead of heading for the template titles in the table view. (#56785) --- packages/edit-site/src/components/list/style.scss | 5 +++++ .../src/components/page-templates/dataviews-templates.js | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index cfc65252c8e685..4739243b7dc295 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -186,3 +186,8 @@ display: block; color: $gray-700; } + +.edit-site-list-title__customized-info { + font-size: 1.3em; + font-weight: 600; +} diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 0556efa5e63799..15750dd85b42c9 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -8,7 +8,7 @@ import removeAccents from 'remove-accents'; */ import { Icon, - __experimentalHeading as Heading, + __experimentalView as View, __experimentalText as Text, __experimentalHStack as HStack, __experimentalVStack as VStack, @@ -83,7 +83,7 @@ function TemplateTitle( { item } ) { const { isCustomized } = useAddedBy( item.type, item.id ); return ( <VStack spacing={ 1 }> - <Heading as="h3" level={ 5 }> + <View as="h3"> <Link params={ { postId: item.id, @@ -94,7 +94,7 @@ function TemplateTitle( { item } ) { { decodeEntities( item.title?.rendered || item.slug ) || __( '(no title)' ) } </Link> - </Heading> + </View> { isCustomized && ( <span className="edit-site-list-added-by__customized-info"> { item.type === TEMPLATE_POST_TYPE From ddfdcd9e91ecebb5249dcdbe639fa3df7eb0ea91 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 6 Dec 2023 20:37:07 +0900 Subject: [PATCH 049/325] Fix top position and height of Pattern Modal Sidebar (#56787) --- packages/block-editor/src/components/inserter/style.scss | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index a9038616aa5339..755445246e8596 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -272,15 +272,11 @@ $block-inserter-tabs-height: 44px; } } -.block-editor-inserter__block-patterns-tabs-container, -.block-editor-block-patterns-explorer__sidebar { +.block-editor-inserter__block-patterns-tabs-container { height: 100%; nav { height: 100%; } - .block-editor-block-patterns__source-filter select.components-select-control__input { - height: 40px; - } } .block-editor-inserter__block-patterns-tabs { @@ -449,7 +445,7 @@ $block-inserter-tabs-height: 44px; .block-editor-block-patterns-explorer { &__sidebar { position: absolute; - top: $header-height + $grid-unit-20; + top: $header-height + $grid-unit-15; left: 0; bottom: 0; width: $sidebar-width; From efa4b01e8eb3d01be82eb45f4838f99286e28640 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 6 Dec 2023 13:50:44 +0100 Subject: [PATCH 050/325] Site and Post Editor: Unify the DocumentBar component (#56778) --- package-lock.json | 2 + packages/commands/README.md | 2 - packages/commands/src/store/index.js | 2 - .../header/document-actions/index.js | 83 ------- .../header/document-actions/style.scss | 64 ------ .../edit-post/src/components/header/index.js | 4 +- packages/edit-post/src/store/actions.js | 2 +- packages/edit-post/src/style.scss | 1 - .../block-editor/use-site-editor-settings.js | 8 +- .../document-actions/index.js | 205 ------------------ .../src/components/header-edit-mode/index.js | 4 +- packages/edit-site/src/style.scss | 1 - packages/editor/package.json | 1 + .../src/components/document-bar/index.js | 182 ++++++++++++++++ .../src/components/document-bar}/style.scss | 51 ++--- packages/editor/src/components/index.js | 1 + packages/editor/src/store/defaults.js | 1 + packages/editor/src/style.scss | 1 + .../various/post-editor-template-mode.spec.js | 8 +- test/e2e/specs/site-editor/title.spec.js | 20 +- 20 files changed, 237 insertions(+), 406 deletions(-) delete mode 100644 packages/edit-post/src/components/header/document-actions/index.js delete mode 100644 packages/edit-post/src/components/header/document-actions/style.scss delete mode 100644 packages/edit-site/src/components/header-edit-mode/document-actions/index.js create mode 100644 packages/editor/src/components/document-bar/index.js rename packages/{edit-site/src/components/header-edit-mode/document-actions => editor/src/components/document-bar}/style.scss (67%) diff --git a/package-lock.json b/package-lock.json index e4b4aeb47f6bad..5707a537e7e565 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55410,6 +55410,7 @@ "@wordpress/blob": "file:../blob", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", @@ -70664,6 +70665,7 @@ "@wordpress/blob": "file:../blob", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", diff --git a/packages/commands/README.md b/packages/commands/README.md index 0a276d80c73e9a..946b101e9ef27d 100644 --- a/packages/commands/README.md +++ b/packages/commands/README.md @@ -62,8 +62,6 @@ _This package assumes that your code will run in an **ES2015+** environment. If Store definition for the commands namespace. -See how the Commands Store is being used in components like [site-hub](https://github.com/WordPress/gutenberg/blob/HEAD/packages/edit-site/src/components/site-hub/index.js#L23) and [document-actions](https://github.com/WordPress/gutenberg/blob/HEAD/packages/edit-post/src/components/header/document-actions/index.js#L14). - _Related_ - <https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore> diff --git a/packages/commands/src/store/index.js b/packages/commands/src/store/index.js index c3751f9ab44975..f3aa6f85f28b86 100644 --- a/packages/commands/src/store/index.js +++ b/packages/commands/src/store/index.js @@ -17,8 +17,6 @@ const STORE_NAME = 'core/commands'; /** * Store definition for the commands namespace. * - * See how the Commands Store is being used in components like [site-hub](https://github.com/WordPress/gutenberg/blob/HEAD/packages/edit-site/src/components/site-hub/index.js#L23) and [document-actions](https://github.com/WordPress/gutenberg/blob/HEAD/packages/edit-post/src/components/header/document-actions/index.js#L14). - * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/data/README.md#createReduxStore * * @type {Object} diff --git a/packages/edit-post/src/components/header/document-actions/index.js b/packages/edit-post/src/components/header/document-actions/index.js deleted file mode 100644 index 5ce58f179f3ab3..00000000000000 --- a/packages/edit-post/src/components/header/document-actions/index.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, isRTL } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { BlockIcon, store as blockEditorStore } from '@wordpress/block-editor'; -import { - Button, - VisuallyHidden, - __experimentalHStack as HStack, - __experimentalText as Text, -} from '@wordpress/components'; -import { layout, chevronLeftSmall, chevronRightSmall } from '@wordpress/icons'; -import { store as commandsStore } from '@wordpress/commands'; -import { displayShortcut } from '@wordpress/keycodes'; -import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - -function DocumentActions() { - const { template } = useSelect( ( select ) => { - const { getEditedPostTemplate } = select( editPostStore ); - - return { - template: getEditedPostTemplate(), - }; - }, [] ); - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { setRenderingMode } = useDispatch( editorStore ); - const { open: openCommandCenter } = useDispatch( commandsStore ); - - if ( ! template ) { - return null; - } - - let templateTitle = __( 'Default' ); - if ( template?.title ) { - templateTitle = template.title; - } else if ( !! template ) { - templateTitle = template.slug; - } - - return ( - <div className="edit-post-document-actions"> - <Button - className="edit-post-document-actions__back" - onClick={ () => { - clearSelectedBlock(); - setRenderingMode( 'post-only' ); - } } - icon={ isRTL() ? chevronRightSmall : chevronLeftSmall } - > - { __( 'Back' ) } - </Button> - <Button - className="edit-post-document-actions__command" - onClick={ () => openCommandCenter() } - > - <HStack - className="edit-post-document-actions__title" - spacing={ 1 } - justify="center" - > - <BlockIcon icon={ layout } /> - <Text size="body" as="h1"> - <VisuallyHidden as="span"> - { __( 'Editing template: ' ) } - </VisuallyHidden> - { templateTitle } - </Text> - </HStack> - <span className="edit-post-document-actions__shortcut"> - { displayShortcut.primary( 'k' ) } - </span> - </Button> - </div> - ); -} - -export default DocumentActions; diff --git a/packages/edit-post/src/components/header/document-actions/style.scss b/packages/edit-post/src/components/header/document-actions/style.scss deleted file mode 100644 index 7eb77f9c0bd88c..00000000000000 --- a/packages/edit-post/src/components/header/document-actions/style.scss +++ /dev/null @@ -1,64 +0,0 @@ -.edit-post-document-actions { - display: flex; - align-items: center; - gap: $grid-unit; - height: $button-size; - justify-content: space-between; - // Flex items will, by default, refuse to shrink below a minimum - // intrinsic width. In order to shrink this flexbox item, and - // subsequently truncate child text, we set an explicit min-width. - // See https://dev.w3.org/csswg/css-flexbox/#min-size-auto - min-width: 0; - background: $gray-100; - border-radius: 4px; - width: min(100%, 450px); - - .components-button { - &:hover { - color: var(--wp-block-synced-color); - background: $gray-200; - } - } -} - -.edit-post-document-actions__command { - flex-grow: 1; - color: var(--wp-block-synced-color); - overflow: hidden; -} - -.edit-post-document-actions__title { - flex-grow: 1; - color: var(--wp-block-synced-color); - overflow: hidden; - - &:hover { - color: var(--wp-block-synced-color); - } - - .block-editor-block-icon { - flex-shrink: 0; - } - - h1 { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: var(--wp-block-synced-color); - } -} - -.edit-post-document-actions__shortcut { - color: $gray-800; -} - -.edit-post-document-actions__back.components-button.has-icon.has-text { - min-width: $button-size; - flex-shrink: 0; - color: $gray-700; - gap: 0; - - &:hover { - color: currentColor; - } -} diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 3c0f57b9a69d1c..0b9a77206f2a61 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -14,6 +14,7 @@ import { PostSavedState, PostPreviewButton, store as editorStore, + DocumentBar, } from '@wordpress/editor'; import { useEffect, useRef, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; @@ -39,7 +40,6 @@ import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; -import DocumentActions from './document-actions'; import { unlock } from '../../lock-unlock'; const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); @@ -164,7 +164,7 @@ function Header( { isLargeViewport, } ) } > - { isEditingTemplate && <DocumentActions /> } + { isEditingTemplate && <DocumentBar /> } </div> </motion.div> <motion.div diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 0380b0f7e98f33..3d93e59c38a324 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -531,7 +531,7 @@ export function setIsEditingTemplate() { export const __unstableSwitchToTemplateMode = ( newTemplate = false ) => ( { registry, select } ) => { - registry.dispatch( editorStore ).setRenderingMode( 'all' ); + registry.dispatch( editorStore ).setRenderingMode( 'template-only' ); const isWelcomeGuideActive = select.isFeatureActive( 'welcomeGuideTemplate' ); diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index e015d084afae1b..474467ab2e25a4 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -2,7 +2,6 @@ @import "./components/header/style.scss"; @import "./components/header/fullscreen-mode-close/style.scss"; @import "./components/header/header-toolbar/style.scss"; -@import "./components/header/document-actions/style.scss"; @import "./components/keyboard-shortcut-help-modal/style.scss"; @import "./components/layout/style.scss"; @import "./components/block-manager/style.scss"; diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 3cca41d67985c5..962cfe09afb720 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -97,10 +97,12 @@ export function useSpecificEditorSettings() { keepCaretInsideBlock, canvasMode, settings, + postWithTemplate, } = useSelect( ( select ) => { const { getEditedPostType, getEditedPostId, + getEditedPostContext, getCanvasMode, getSettings, } = unlock( select( editSiteStore ) ); @@ -113,6 +115,7 @@ export function useSpecificEditorSettings() { usedPostType, usedPostId ); + const _context = getEditedPostContext(); return { templateSlug: _record.slug, focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), @@ -130,10 +133,11 @@ export function useSpecificEditorSettings() { ), canvasMode: getCanvasMode(), settings: getSettings(), + postWithTemplate: _context?.postId, }; }, [] ); const archiveLabels = useArchiveLabel( templateSlug ); - + const defaultRenderingMode = postWithTemplate ? 'template-locked' : 'all'; const defaultEditorSettings = useMemo( () => { return { ...settings, @@ -144,6 +148,7 @@ export function useSpecificEditorSettings() { isDistractionFree, hasFixedToolbar, keepCaretInsideBlock, + defaultRenderingMode, // I wonder if they should be set in the post editor too __experimentalArchiveTitleTypeLabel: archiveLabels.archiveTypeLabel, @@ -159,6 +164,7 @@ export function useSpecificEditorSettings() { canvasMode, archiveLabels.archiveTypeLabel, archiveLabels.archiveNameLabel, + defaultRenderingMode, ] ); return defaultEditorSettings; diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js b/packages/edit-site/src/components/header-edit-mode/document-actions/index.js deleted file mode 100644 index 801226200f20da..00000000000000 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/index.js +++ /dev/null @@ -1,205 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { __, isRTL } from '@wordpress/i18n'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { - Button, - VisuallyHidden, - __experimentalText as Text, - __experimentalHStack as HStack, -} from '@wordpress/components'; -import { BlockIcon } from '@wordpress/block-editor'; -import { store as commandsStore } from '@wordpress/commands'; -import { - chevronLeftSmall, - chevronRightSmall, - page as pageIcon, - navigation as navigationIcon, - symbol, -} from '@wordpress/icons'; -import { displayShortcut } from '@wordpress/keycodes'; -import { store as coreStore } from '@wordpress/core-data'; -import { store as editorStore } from '@wordpress/editor'; -import { useRef, useState, useEffect } from '@wordpress/element'; - -/** - * Internal dependencies - */ -import useEditedEntityRecord from '../../use-edited-entity-record'; -import { store as editSiteStore } from '../../../store'; -import { - TEMPLATE_POST_TYPE, - NAVIGATION_POST_TYPE, - TEMPLATE_PART_POST_TYPE, - PATTERN_TYPES, - PATTERN_SYNC_TYPES, -} from '../../../utils/constants'; - -const typeLabels = { - [ PATTERN_TYPES.user ]: __( 'Editing pattern:' ), - [ NAVIGATION_POST_TYPE ]: __( 'Editing navigation menu:' ), - [ TEMPLATE_POST_TYPE ]: __( 'Editing template:' ), - [ TEMPLATE_PART_POST_TYPE ]: __( 'Editing template part:' ), -}; - -export default function DocumentActions() { - const isPage = useSelect( - ( select ) => select( editSiteStore ).isPage(), - [] - ); - return isPage ? <PageDocumentActions /> : <TemplateDocumentActions />; -} - -function PageDocumentActions() { - const { isEditingPage, hasResolved, isFound, title } = useSelect( - ( select ) => { - const { getEditedPostContext } = select( editSiteStore ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const { getRenderingMode } = select( editorStore ); - const context = getEditedPostContext(); - const queryArgs = [ 'postType', context.postType, context.postId ]; - const page = getEditedEntityRecord( ...queryArgs ); - return { - isEditingPage: - !! context.postId && getRenderingMode() !== 'template-only', - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - isFound: !! page, - title: page?.title, - }; - }, - [] - ); - - const { setRenderingMode } = useDispatch( editorStore ); - const [ isAnimated, setIsAnimated ] = useState( false ); - const isLoading = useRef( true ); - - useEffect( () => { - if ( ! isLoading.current ) { - setIsAnimated( true ); - } - isLoading.current = false; - }, [ isEditingPage ] ); - - if ( ! hasResolved ) { - return null; - } - - if ( ! isFound ) { - return ( - <div className="edit-site-document-actions"> - { __( 'Document not found' ) } - </div> - ); - } - - return isEditingPage ? ( - <BaseDocumentActions - className={ classnames( 'is-page', { - 'is-animated': isAnimated, - } ) } - icon={ pageIcon } - > - { title } - </BaseDocumentActions> - ) : ( - <TemplateDocumentActions - className={ classnames( { - 'is-animated': isAnimated, - } ) } - onBack={ () => setRenderingMode( 'template-locked' ) } - /> - ); -} - -function TemplateDocumentActions( { className, onBack } ) { - const { isLoaded, record, getTitle, icon } = useEditedEntityRecord(); - - if ( ! isLoaded ) { - return null; - } - - if ( ! record ) { - return ( - <div className="edit-site-document-actions"> - { __( 'Document not found' ) } - </div> - ); - } - - let typeIcon = icon; - if ( record.type === NAVIGATION_POST_TYPE ) { - typeIcon = navigationIcon; - } else if ( record.type === PATTERN_TYPES.user ) { - typeIcon = symbol; - } - - return ( - <BaseDocumentActions - className={ classnames( className, { - 'is-synced-entity': - record.wp_pattern_sync_status !== - PATTERN_SYNC_TYPES.unsynced, - } ) } - icon={ typeIcon } - onBack={ onBack } - > - <VisuallyHidden as="span"> - { typeLabels[ record.type ] ?? - typeLabels[ TEMPLATE_POST_TYPE ] } - </VisuallyHidden> - { getTitle() } - </BaseDocumentActions> - ); -} - -function BaseDocumentActions( { className, icon, children, onBack } ) { - const { open: openCommandCenter } = useDispatch( commandsStore ); - return ( - <div - className={ classnames( 'edit-site-document-actions', className ) } - > - { onBack && ( - <Button - className="edit-site-document-actions__back" - icon={ isRTL() ? chevronRightSmall : chevronLeftSmall } - onClick={ ( event ) => { - event.stopPropagation(); - onBack(); - } } - > - { __( 'Back' ) } - </Button> - ) } - <Button - className="edit-site-document-actions__command" - onClick={ () => openCommandCenter() } - size="compact" - > - <HStack - className="edit-site-document-actions__title" - spacing={ 1 } - justify="center" - > - <BlockIcon icon={ icon } /> - <Text size="body" as="h1"> - { children } - </Text> - </HStack> - <span className="edit-site-document-actions__shortcut"> - { displayShortcut.primary( 'k' ) } - </span> - </Button> - </div> - ); -} diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 110e9ca3b4d842..f8a9d9d4e892bd 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -27,13 +27,13 @@ import { VisuallyHidden, } from '@wordpress/components'; import { store as preferencesStore } from '@wordpress/preferences'; +import { DocumentBar } from '@wordpress/editor'; /** * Internal dependencies */ import MoreMenu from './more-menu'; import SaveButton from '../save-button'; -import DocumentActions from './document-actions'; import DocumentTools from './document-tools'; import { store as editSiteStore } from '../../store'; import { @@ -205,7 +205,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { { ! hasDefaultEditorCanvasView ? ( getEditorCanvasContainerTitle( editorCanvasView ) ) : ( - <DocumentActions /> + <DocumentBar /> ) } </div> ) } diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 5a93375afec8b0..9e9cdbc6684b81 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -8,7 +8,6 @@ @import "./components/global-styles/style.scss"; @import "./components/global-styles/screen-revisions/style.scss"; @import "./components/header-edit-mode/style.scss"; -@import "./components/header-edit-mode/document-actions/style.scss"; @import "./components/list/style.scss"; @import "./components/page/style.scss"; @import "./components/page-pages/style.scss"; diff --git a/packages/editor/package.json b/packages/editor/package.json index 59b4c78b8235ee..81d24961a775ec 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -36,6 +36,7 @@ "@wordpress/blob": "file:../blob", "@wordpress/block-editor": "file:../block-editor", "@wordpress/blocks": "file:../blocks", + "@wordpress/commands": "file:../commands", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", "@wordpress/core-data": "file:../core-data", diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js new file mode 100644 index 00000000000000..ffb2be33074563 --- /dev/null +++ b/packages/editor/src/components/document-bar/index.js @@ -0,0 +1,182 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { __, isRTL, sprintf } from '@wordpress/i18n'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { + Button, + __experimentalText as Text, + __experimentalHStack as HStack, +} from '@wordpress/components'; +import { BlockIcon } from '@wordpress/block-editor'; +import { + chevronLeftSmall, + chevronRightSmall, + page as pageIcon, + navigation as navigationIcon, + symbol, +} from '@wordpress/icons'; +import { displayShortcut } from '@wordpress/keycodes'; +import { useEntityRecord } from '@wordpress/core-data'; +import { store as commandsStore } from '@wordpress/commands'; +import { useState, useEffect, useRef } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; + +const typeLabels = { + // translators: 1: Pattern title. + wp_pattern: __( 'Editing pattern: %s' ), + // translators: 1: Navigation menu title. + wp_navigation: __( 'Editing navigation menu: %s' ), + // translators: 1: Template title. + wp_template: __( 'Editing template: %s' ), + // translators: 1: Template part title. + wp_template_part: __( 'Editing template part: %s' ), +}; + +const icons = { + wp_block: symbol, + wp_navigation: navigationIcon, +}; + +export default function DocumentBar() { + const { isEditingTemplate, templateId, postType, postId } = useSelect( + ( select ) => { + const { + getRenderingMode, + getCurrentTemplateId, + getCurrentPostId, + getCurrentPostType, + } = select( editorStore ); + const _templateId = getCurrentTemplateId(); + return { + isEditingTemplate: + !! _templateId && getRenderingMode() === 'template-only', + templateId: _templateId, + postType: getCurrentPostType(), + postId: getCurrentPostId(), + }; + }, + [] + ); + const { getEditorSettings } = useSelect( editorStore ); + const { setRenderingMode } = useDispatch( editorStore ); + + return ( + <BaseDocumentActions + postType={ isEditingTemplate ? 'wp_template' : postType } + postId={ isEditingTemplate ? templateId : postId } + onBack={ + isEditingTemplate + ? () => + setRenderingMode( + getEditorSettings().defaultRenderingMode + ) + : undefined + } + /> + ); +} + +function BaseDocumentActions( { postType, postId, onBack } ) { + const { open: openCommandCenter } = useDispatch( commandsStore ); + const { editedRecord: document, isResolving } = useEntityRecord( + 'postType', + postType, + postId + ); + const { templateIcon, templateTitle } = useSelect( ( select ) => { + const { __experimentalGetTemplateInfo: getTemplateInfo } = + select( editorStore ); + const templateInfo = getTemplateInfo( document ); + return { + templateIcon: templateInfo.icon, + templateTitle: templateInfo.title, + }; + } ); + const isNotFound = ! document && ! isResolving; + const icon = icons[ postType ] ?? pageIcon; + const [ isAnimated, setIsAnimated ] = useState( false ); + const isMounting = useRef( true ); + const isTemplate = [ 'wp_template', 'wp_template_part' ].includes( + postType + ); + const isGlobalEntity = [ + 'wp_template', + 'wp_navigation', + 'wp_template_part', + 'wp_block', + ].includes( postType ); + + useEffect( () => { + if ( ! isMounting.current ) { + setIsAnimated( true ); + } + isMounting.current = false; + }, [ postType, postId ] ); + + const title = isTemplate ? templateTitle : document.title; + + return ( + <div + className={ classnames( 'editor-document-bar', { + 'has-back-button': !! onBack, + 'is-animated': isAnimated, + 'is-global': isGlobalEntity, + } ) } + > + { onBack && ( + <Button + className="editor-document-bar__back" + icon={ isRTL() ? chevronRightSmall : chevronLeftSmall } + onClick={ ( event ) => { + event.stopPropagation(); + onBack(); + } } + size="compact" + > + { __( 'Back' ) } + </Button> + ) } + { isNotFound && <Text>{ __( 'Document not found' ) }</Text> } + { ! isNotFound && ( + <Button + className="editor-document-bar__command" + onClick={ () => openCommandCenter() } + size="compact" + > + <HStack + className="editor-document-bar__title" + spacing={ 1 } + justify="center" + > + <BlockIcon icon={ isTemplate ? templateIcon : icon } /> + <Text + size="body" + as="h1" + aria-label={ + typeLabels[ postType ] + ? // eslint-disable-next-line @wordpress/valid-sprintf + sprintf( typeLabels[ postType ], title ) + : undefined + } + > + { title } + </Text> + </HStack> + <span className="editor-document-bar__shortcut"> + { displayShortcut.primary( 'k' ) } + </span> + </Button> + ) } + </div> + ); +} diff --git a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss b/packages/editor/src/components/document-bar/style.scss similarity index 67% rename from packages/edit-site/src/components/header-edit-mode/document-actions/style.scss rename to packages/editor/src/components/document-bar/style.scss index dce73f269a705c..0cd7e0689c7d31 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-actions/style.scss +++ b/packages/editor/src/components/document-bar/style.scss @@ -1,4 +1,4 @@ -.edit-site-document-actions { +.editor-document-bar { display: flex; align-items: center; height: $button-size-compact; @@ -28,26 +28,18 @@ @include break-large() { width: min(100%, 450px); } - - &.is-synced-entity { - .edit-site-document-actions__title { - color: var(--wp-block-synced-color); - h1 { - color: var(--wp-block-synced-color); - } - } - } } -.edit-site-document-actions__command { +.editor-document-bar__command { flex-grow: 1; color: var(--wp-block-synced-color); overflow: hidden; } -.edit-site-document-actions__title { +.editor-document-bar__title { flex-grow: 1; overflow: hidden; + color: $gray-800; // Offset the layout based on the width of the ⌘K label. This ensures the title is centrally aligned. @include break-small() { @@ -58,6 +50,10 @@ color: var(--wp-block-synced-color); } + .editor-document-bar.is-global & { + color: var(--wp-block-synced-color); + } + .block-editor-block-icon { min-width: $grid-unit-30; flex-shrink: 0; @@ -68,28 +64,21 @@ overflow: hidden; text-overflow: ellipsis; max-width: 50%; + color: currentColor; } - .edit-site-document-actions.is-page & { - color: $gray-800; - - h1 { - color: $gray-800; - } - } - - .edit-site-document-actions.is-animated & { - animation: edit-site-document-actions__slide-in-left 0.3s; + .editor-document-bar.is-animated.has-back-button & { + animation: editor-document-bar__slide-in-left 0.3s; @include reduce-motion("animation"); } - .edit-site-document-actions.is-animated.is-page & { - animation: edit-site-document-actions__slide-in-right 0.3s; + .editor-document-bar.is-animated & { + animation: editor-document-bar__slide-in-right 0.3s; @include reduce-motion("animation"); } } -.edit-site-document-actions__shortcut { +.editor-document-bar__shortcut { color: $gray-800; min-width: $grid-unit-40; display: none; @@ -99,7 +88,7 @@ } } -.edit-site-document-actions__back.components-button.has-icon.has-text { +.editor-document-bar__back.components-button.has-icon.has-text { min-width: $button-size; flex-shrink: 0; color: $gray-700; @@ -108,17 +97,17 @@ position: absolute; &:hover { - color: currentColor; + color: var(--wp-block-synced-color); background-color: transparent; } - .edit-site-document-actions.is-animated & { - animation: edit-site-document-actions__slide-in-left 0.3s; + .editor-document-bar.is-animated & { + animation: editor-document-bar__slide-in-left 0.3s; @include reduce-motion("animation"); } } -@keyframes edit-site-document-actions__slide-in-right { +@keyframes editor-document-bar__slide-in-right { from { transform: translateX(-15%); opacity: 0; @@ -129,7 +118,7 @@ } } -@keyframes edit-site-document-actions__slide-in-left { +@keyframes editor-document-bar__slide-in-left { from { transform: translateX(15%); opacity: 0; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 5fefc5506a02fc..64cd17746520a4 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -8,6 +8,7 @@ export * from './autocompleters'; // Post Related Components. export { default as AutosaveMonitor } from './autosave-monitor'; +export { default as DocumentBar } from './document-bar'; export { default as DocumentOutline } from './document-outline'; export { default as DocumentOutlineCheck } from './document-outline/check'; export { EditorKeyboardShortcuts }; diff --git a/packages/editor/src/store/defaults.js b/packages/editor/src/store/defaults.js index 38b79ad9a84cc8..686888f91de3d5 100644 --- a/packages/editor/src/store/defaults.js +++ b/packages/editor/src/store/defaults.js @@ -27,4 +27,5 @@ export const EDITOR_SETTINGS_DEFAULTS = { richEditingEnabled: true, codeEditingEnabled: true, enableCustomFields: undefined, + defaultRenderingMode: 'post-only', }; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 986cb645c271f5..ba12f58697a4a3 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -1,4 +1,5 @@ @import "./components/autocompleters/style.scss"; +@import "./components/document-bar/style.scss"; @import "./components/document-outline/style.scss"; @import "./components/editor-notices/style.scss"; @import "./components/entities-saved-states/style.scss"; diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js index b9bfbf9dd6b05e..f862c32556e061 100644 --- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js +++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js @@ -147,9 +147,11 @@ class PostEditorTemplateMode { 'role=button[name="Dismiss this notice"] >> text=Editing template. Changes made here affect all posts and pages that use the template.' ); - await expect( - this.editorTopBar.getByRole( 'heading[level=1]' ) - ).toHaveText( 'Editing template: Single Entries' ); + const title = this.editorTopBar.getByRole( 'heading', { + name: 'Editing template: Single Entries', + } ); + + await expect( title ).toBeVisible(); } async createPostAndSaveDraft() { diff --git a/test/e2e/specs/site-editor/title.spec.js b/test/e2e/specs/site-editor/title.spec.js index aa2942670c5a85..3224c519f4f9e8 100644 --- a/test/e2e/specs/site-editor/title.spec.js +++ b/test/e2e/specs/site-editor/title.spec.js @@ -22,11 +22,13 @@ test.describe( 'Site editor title', () => { postType: 'wp_template', canvas: 'edit', } ); - const title = page.locator( - 'role=region[name="Editor top bar"i] >> role=heading[level=1]' - ); + const title = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'heading', { + name: 'Editing template: Index', + } ); - await expect( title ).toHaveText( 'Editing template:Index' ); + await expect( title ).toBeVisible(); } ); test( 'displays the selected template name in the title for the header template', async ( { @@ -39,10 +41,12 @@ test.describe( 'Site editor title', () => { postType: 'wp_template_part', canvas: 'edit', } ); - const title = page.locator( - 'role=region[name="Editor top bar"i] >> role=heading[level=1]' - ); + const title = page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'heading', { + name: 'Editing template part: header', + } ); - await expect( title ).toHaveText( 'Editing template part:header' ); + await expect( title ).toBeVisible(); } ); } ); From a2d1d37a1ad803c92645220a3afa45fe1dc2459e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 6 Dec 2023 15:19:41 +0100 Subject: [PATCH 051/325] Editor: Cleanup default editor mode handling (#56819) --- packages/edit-post/src/editor.js | 8 +------- packages/edit-site/src/components/editor/index.js | 14 +------------- packages/editor/src/components/provider/index.js | 6 ++++++ 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 2394ebb3a3a742..cff867c3f7a2cb 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -9,7 +9,7 @@ import { store as editorStore, privateApis as editorPrivateApis, } from '@wordpress/editor'; -import { useEffect, useMemo } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -142,12 +142,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { keepCaretInsideBlock, ] ); - // The default mode of the post editor is "post-only" mode. - const { setRenderingMode } = useDispatch( editorStore ); - useEffect( () => { - setRenderingMode( 'post-only' ); - }, [ setRenderingMode ] ); - if ( ! post ) { return null; } diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 1d3fca36f5f4c0..5a2f1e2ec4d1a9 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { Notice } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -29,7 +29,6 @@ import { } from '@wordpress/editor'; import { __, sprintf } from '@wordpress/i18n'; import { store as coreDataStore } from '@wordpress/core-data'; -import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -149,7 +148,6 @@ export default function Editor( { listViewToggleElement, isLoading } ) { ), }; }, [] ); - const { setRenderingMode } = useDispatch( editorStore ); const isViewMode = canvasMode === 'view'; const isEditMode = canvasMode === 'edit'; @@ -192,16 +190,6 @@ export default function Editor( { listViewToggleElement, isLoading } ) { ( ( postWithTemplate && !! contextPost && !! editedPost ) || ( ! postWithTemplate && !! editedPost ) ); - // This is the only reliable way I've found to reinitialize the rendering mode - // when the canvas mode or the edited entity changes. - useEffect( () => { - if ( canvasMode === 'edit' && postWithTemplate ) { - setRenderingMode( 'template-locked' ); - } else { - setRenderingMode( 'all' ); - } - }, [ canvasMode, postWithTemplate, setRenderingMode ] ); - return ( <> { ! isReady ? <CanvasLoader id={ loadingProgressId } /> : null } diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 3bd5860501d4e0..3e32c7f80f48e1 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -199,6 +199,7 @@ export const ExperimentalEditorProvider = withRegistryProvider( updateEditorSettings, __experimentalTearDownEditor, setCurrentTemplateId, + setRenderingMode, } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); @@ -243,6 +244,11 @@ export const ExperimentalEditorProvider = withRegistryProvider( setCurrentTemplateId( template?.id ); }, [ template?.id, setCurrentTemplateId ] ); + // Sets the right rendering mode when loading the editor. + useEffect( () => { + setRenderingMode( settings.defaultRenderingMode ?? 'post-only' ); + }, [ settings.defaultRenderingMode, setRenderingMode ] ); + if ( ! isReady ) { return null; } From 29e52a105761b28c7767aeb7c68b4a558da31c7a Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Wed, 6 Dec 2023 10:13:17 -0500 Subject: [PATCH 052/325] Components: replace `TabPanel` with `Tabs` in the editor's `ColorGradientControl` (#56351) Co-authored-by: Marco Ciampini <marco.ciampo@gmail.com> --- .../components/colors-gradients/control.js | 79 ++++++++++++------- .../components/colors-gradients/style.scss | 6 -- 2 files changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/block-editor/src/components/colors-gradients/control.js b/packages/block-editor/src/components/colors-gradients/control.js index 51912e0a74e9d1..0cb2fcdda44875 100644 --- a/packages/block-editor/src/components/colors-gradients/control.js +++ b/packages/block-editor/src/components/colors-gradients/control.js @@ -10,15 +10,16 @@ import { __ } from '@wordpress/i18n'; import { BaseControl, __experimentalVStack as VStack, - TabPanel, ColorPalette, GradientPicker, + privateApis as componentsPrivateApis, } from '@wordpress/components'; /** * Internal dependencies */ import { useSettings } from '../use-settings'; +import { unlock } from '../../lock-unlock'; const colorsAndGradientKeys = [ 'colors', @@ -27,18 +28,7 @@ const colorsAndGradientKeys = [ 'disableCustomGradients', ]; -const TAB_COLOR = { - name: 'color', - title: __( 'Solid' ), - value: 'color', -}; -const TAB_GRADIENT = { - name: 'gradient', - title: __( 'Gradient' ), - value: 'gradient', -}; - -const TABS_SETTINGS = [ TAB_COLOR, TAB_GRADIENT ]; +const TAB_IDS = { color: 'color', gradient: 'gradient' }; function ColorGradientControlInner( { colors, @@ -69,7 +59,7 @@ function ColorGradientControlInner( { } const tabPanels = { - [ TAB_COLOR.value ]: ( + [ TAB_IDS.color ]: ( <ColorPalette value={ colorValue } onChange={ @@ -89,7 +79,7 @@ function ColorGradientControlInner( { headingLevel={ headingLevel } /> ), - [ TAB_GRADIENT.value ]: ( + [ TAB_IDS.gradient ]: ( <GradientPicker __nextHasNoMargin value={ gradientValue } @@ -117,6 +107,11 @@ function ColorGradientControlInner( { </div> ); + // Unlocking `Tabs` too early causes the `unlock` method to receive an empty + // object, due to circular dependencies. + // See https://github.com/WordPress/gutenberg/issues/52692 + const { Tabs } = unlock( componentsPrivateApis ); + return ( <BaseControl __nextHasNoMarginBottom @@ -137,22 +132,46 @@ function ColorGradientControlInner( { </legend> ) } { canChooseAColor && canChooseAGradient && ( - <TabPanel - className="block-editor-color-gradient-control__tabs" - tabs={ TABS_SETTINGS } - initialTabName={ - gradientValue - ? TAB_GRADIENT.value - : !! canChooseAColor && TAB_COLOR.value - } - > - { ( tab ) => renderPanelType( tab.value ) } - </TabPanel> + <div> + <Tabs + initialTabId={ + gradientValue + ? TAB_IDS.gradient + : !! canChooseAColor && TAB_IDS.color + } + > + <Tabs.TabList> + <Tabs.Tab id={ TAB_IDS.color }> + { __( 'Solid' ) } + </Tabs.Tab> + <Tabs.Tab id={ TAB_IDS.gradient }> + { __( 'Gradient' ) } + </Tabs.Tab> + </Tabs.TabList> + <Tabs.TabPanel + id={ TAB_IDS.color } + className={ + 'block-editor-color-gradient-control__panel' + } + focusable={ false } + > + { tabPanels.color } + </Tabs.TabPanel> + <Tabs.TabPanel + id={ TAB_IDS.gradient } + className={ + 'block-editor-color-gradient-control__panel' + } + focusable={ false } + > + { tabPanels.gradient } + </Tabs.TabPanel> + </Tabs> + </div> ) } - { ! canChooseAGradient && - renderPanelType( TAB_COLOR.value ) } - { ! canChooseAColor && - renderPanelType( TAB_GRADIENT.value ) } + + { ! canChooseAGradient && renderPanelType( TAB_IDS.color ) } + { ! canChooseAColor && renderPanelType( TAB_IDS.gradient ) } </VStack> </fieldset> </BaseControl> diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index 6cade124b7fe46..fcae4a99189eed 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -15,12 +15,6 @@ $swatch-gap: 12px; min-width: 0; } -.block-editor-color-gradient-control__tabs { - .block-editor-color-gradient-control__panel { - padding: $grid-unit-20; - } -} - .block-editor-panel-color-gradient-settings.block-editor-panel-color-gradient-settings { &, & > div:not(:first-of-type) { From fceed16b305f3af61d305e9005e8c68777af2451 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Thu, 7 Dec 2023 03:10:17 +1100 Subject: [PATCH 053/325] Image: Fix resetting behaviour for alt image text (#56809) --- packages/block-library/src/image/image.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 768c7272e56dba..b74079b2b8b79d 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -380,6 +380,7 @@ export default function Image( { const resetAll = () => { setAttributes( { + alt: undefined, width: undefined, height: undefined, scale: undefined, @@ -456,14 +457,14 @@ export default function Image( { <ToolsPanelItem label={ __( 'Alternative text' ) } isShownByDefault={ true } - hasValue={ () => alt !== '' } + hasValue={ () => !! alt } onDeselect={ () => setAttributes( { alt: undefined } ) } > <TextareaControl label={ __( 'Alternative text' ) } - value={ alt } + value={ alt || '' } onChange={ updateAlt } help={ <> @@ -481,11 +482,13 @@ export default function Image( { </ToolsPanelItem> ) } { isResizable && dimensionsControl } - <ResolutionTool - value={ sizeSlug } - onChange={ updateImage } - options={ imageSizeOptions } - /> + { !! imageSizeOptions.length && ( + <ResolutionTool + value={ sizeSlug } + onChange={ updateImage } + options={ imageSizeOptions } + /> + ) } { showLightboxToggle && ( <ToolsPanelItem hasValue={ () => !! lightbox } From df1e806d2d4c4ec8a67d0dc0462744deb0d1edd7 Mon Sep 17 00:00:00 2001 From: Rich Tabor <hi@richtabor.com> Date: Wed, 6 Dec 2023 11:49:18 -0500 Subject: [PATCH 054/325] Use consistent styling for duotone panels (#56801) * Use consistent styling for duotone panels * Update CHANGELOG.md --- .../src/components/colors-gradients/style.scss | 1 - .../src/components/duotone-control/index.js | 7 ++----- .../src/components/duotone-control/style.scss | 7 +------ .../src/components/global-styles/filters-panel.js | 12 ++++++++---- packages/components/CHANGELOG.md | 1 + packages/components/src/palette-edit/style.scss | 4 ++-- 6 files changed, 14 insertions(+), 18 deletions(-) diff --git a/packages/block-editor/src/components/colors-gradients/style.scss b/packages/block-editor/src/components/colors-gradients/style.scss index fcae4a99189eed..b3539637a9904c 100644 --- a/packages/block-editor/src/components/colors-gradients/style.scss +++ b/packages/block-editor/src/components/colors-gradients/style.scss @@ -27,7 +27,6 @@ $swatch-gap: 12px; .components-circular-option-picker__swatches { display: grid; grid-template-columns: repeat(6, $swatch-size); - justify-content: space-between; } } diff --git a/packages/block-editor/src/components/duotone-control/index.js b/packages/block-editor/src/components/duotone-control/index.js index e110286976b1a6..247e0c51354b0e 100644 --- a/packages/block-editor/src/components/duotone-control/index.js +++ b/packages/block-editor/src/components/duotone-control/index.js @@ -65,14 +65,11 @@ function DuotoneControl( { } } renderContent={ () => ( <MenuGroup label={ __( 'Duotone' ) }> - <div - id={ descriptionId } - className="block-editor-duotone-control__description" - > + <p> { __( 'Create a two-tone color effect without losing your original image.' ) } - </div> + </p> <DuotonePicker aria-label={ actionLabel } aria-describedby={ descriptionId } diff --git a/packages/block-editor/src/components/duotone-control/style.scss b/packages/block-editor/src/components/duotone-control/style.scss index d80f7c4628c35f..3a3a5a143289b8 100644 --- a/packages/block-editor/src/components/duotone-control/style.scss +++ b/packages/block-editor/src/components/duotone-control/style.scss @@ -3,7 +3,7 @@ $swatch-size: 28px; $swatch-gap: 12px; -$popover-width: $sidebar-width; +$popover-width: 260px; $popover-padding: $grid-unit-20; $swatch-columns: math.floor(math.div($popover-width + $swatch-gap - 2 * $popover-padding, $swatch-size + $swatch-gap)); @@ -26,11 +26,6 @@ $swatch-columns: math.floor(math.div($popover-width + $swatch-gap - 2 * $popover } } -.block-editor-duotone-control__description { - margin: $grid-unit-20 0; - font-size: $helptext-font-size; -} - .block-editor-duotone-control__unset-indicator { // Show a diagonal line (crossed out) for empty swatches. background: linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); diff --git a/packages/block-editor/src/components/global-styles/filters-panel.js b/packages/block-editor/src/components/global-styles/filters-panel.js index 6477f354280e12..42c2494489ed14 100644 --- a/packages/block-editor/src/components/global-styles/filters-panel.js +++ b/packages/block-editor/src/components/global-styles/filters-panel.js @@ -12,9 +12,9 @@ import { __experimentalItemGroup as ItemGroup, __experimentalHStack as HStack, __experimentalZStack as ZStack, - __experimentalVStack as VStack, __experimentalDropdownContentWrapper as DropdownContentWrapper, Button, + MenuGroup, ColorIndicator, DuotonePicker, DuotoneSwatch, @@ -82,6 +82,10 @@ function FiltersToolsPanel( { label={ _x( 'Filters', 'Name for applying graphical effects' ) } resetAll={ resetAll } panelId={ panelId } + dropdownMenuProps={ { + placement: 'left-start', + offset: 258, // sidebar width (280px) - button width (24px) + border (2px) + } } > { children } </ToolsPanel> @@ -197,8 +201,8 @@ export default function FiltersPanel( { ); } } renderContent={ () => ( - <DropdownContentWrapper paddingSize="medium"> - <VStack> + <DropdownContentWrapper paddingSize="small"> + <MenuGroup label={ __( 'Duotone' ) }> <p> { __( 'Create a two-tone color effect without losing your original image.' @@ -213,7 +217,7 @@ export default function FiltersPanel( { value={ duotone } onChange={ setDuotone } /> - </VStack> + </MenuGroup> </DropdownContentWrapper> ) } /> diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 435a9bf22d9b20..5e851ab2b63df0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,6 +7,7 @@ - `FormToggle`: fix sass deprecation warning ([#56672](https://github.com/WordPress/gutenberg/pull/56672)). - `QueryControls`: Add opt-in prop for 40px default size ([#56576](https://github.com/WordPress/gutenberg/pull/56576)). - `CheckboxControl`: Add option to not render label ([#56158](https://github.com/WordPress/gutenberg/pull/56158)). +- `PaletteEdit`: Gradient pickers to use same width as color pickers ([#56801](https://github.com/WordPress/gutenberg/pull/56801)). ### Bug Fix diff --git a/packages/components/src/palette-edit/style.scss b/packages/components/src/palette-edit/style.scss index 55fdfbf42cb525..d73c7ff46cc3c0 100644 --- a/packages/components/src/palette-edit/style.scss +++ b/packages/components/src/palette-edit/style.scss @@ -1,6 +1,6 @@ .components-palette-edit__popover-gradient-picker { - width: 280px; - padding: 8px; + width: 260px; + padding: $grid-unit-10; } .components-dropdown-menu__menu { .components-palette-edit__menu-button { From 065c4fb09348a274c082670c552e5204da24f008 Mon Sep 17 00:00:00 2001 From: Andrei Draganescu <me@andreidraganescu.info> Date: Wed, 6 Dec 2023 18:49:56 +0200 Subject: [PATCH 055/325] Update image block save to only save align none class (#56449) Co-authored-by: Alex Lende <alex+github.com@lende.xyz> --- packages/block-library/src/image/save.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/save.js b/packages/block-library/src/image/save.js index 81565af09ababf..0b750068e9a874 100644 --- a/packages/block-library/src/image/save.js +++ b/packages/block-library/src/image/save.js @@ -36,7 +36,9 @@ export default function save( { attributes } ) { const borderProps = getBorderClassesAndStyles( attributes ); const classes = classnames( { - [ `align${ align }` ]: align, + // All other align classes are handled by block supports. + // `{ align: 'none' }` is unique to transforms for the image block. + alignnone: 'none' === align, [ `size-${ sizeSlug }` ]: sizeSlug, 'is-resized': width || height, 'has-custom-border': From 97dfb08200c45197767dba3a028388532f77a757 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 7 Dec 2023 04:57:06 +0900 Subject: [PATCH 056/325] Patterns: End pattern page descriptions with a period (#56828) --- .../use-pattern-categories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js index 4dee1bae3f314f..4d80d838ae90d5 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-patterns/use-pattern-categories.js @@ -95,7 +95,7 @@ export default function usePatternCategories() { sortedCategories.unshift( { name: PATTERN_DEFAULT_CATEGORY, label: __( 'All patterns' ), - description: __( 'A list of all patterns from all sources' ), + description: __( 'A list of all patterns from all sources.' ), count: themePatterns.length + userPatterns.length, } ); From 8a7b07e3ee9184133283723b2a9d8b16a34a94eb Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 6 Dec 2023 20:24:00 +0000 Subject: [PATCH 057/325] Bump plugin version to 17.2.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 2526aac3770548..7970dd5461fc4f 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.2.0-rc.1 + * Version: 17.2.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 5707a537e7e565..86ff1a4d21af92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.2.0-rc.1", + "version": "17.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.2.0-rc.1", + "version": "17.2.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 8f6a3baeeb283f..122a1368eaf1ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.2.0-rc.1", + "version": "17.2.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 4be83f829d3808b357c602c2b7edc89161f64601 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 6 Dec 2023 20:34:00 +0000 Subject: [PATCH 058/325] Update Changelog for 17.2.0 --- changelog.txt | 347 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 347 insertions(+) diff --git a/changelog.txt b/changelog.txt index b8946492403702..2f7d2f02ff6291 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,352 @@ == Changelog == += 17.2.0 = + + + +## Changelog + +### Bug Fixes + +#### Post Editor +- Editor: Fix issue where createBlock in block template caused list view collapse. ([56666](https://github.com/WordPress/gutenberg/pull/56666)) + +#### Modules API +- Modules: Fix import map polyfill not being copied on the generated plugin ZIP. ([56655](https://github.com/WordPress/gutenberg/pull/56655)) + + +### Documentation + +- Interactivity API: New store() API documentation. ([56764](https://github.com/WordPress/gutenberg/pull/56764)) +- Interactivity API: Update TS/JSDocs after migrating to the new `store()` API. ([56748](https://github.com/WordPress/gutenberg/pull/56748)) + + +### Tools + +#### Build Tooling +- Add missing labels to changelog script, and enhance mapping function. ([55066](https://github.com/WordPress/gutenberg/pull/55066)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@andrewserong @DAreRodz @luisherranz @vcanales += 17.2.0-rc.1 = + + + +## Changelog + +### Features + +#### Modules API +- Interactivity API: Use modules instead of scripts in the frontend. ([56143](https://github.com/WordPress/gutenberg/pull/56143)) + + +### Enhancements + +- Add translator comments for strings containing date formats. ([56531](https://github.com/WordPress/gutenberg/pull/56531)) +- Block Settings: Only display parent block selector on small screens. ([56431](https://github.com/WordPress/gutenberg/pull/56431)) +- Block Theme Preview: Display the theme name on the activate button. ([55752](https://github.com/WordPress/gutenberg/pull/55752)) +- Core data revisions: Extend support to other post types. ([56353](https://github.com/WordPress/gutenberg/pull/56353)) +- Improve tooltip for parent blocks on the block toolbar. ([56146](https://github.com/WordPress/gutenberg/pull/56146)) +- Simplify template author token. ([56566](https://github.com/WordPress/gutenberg/pull/56566)) +- Style engine: Allow CSS var output for fontSize and fontFamily and update documentation. ([56528](https://github.com/WordPress/gutenberg/pull/56528)) +- Try: Change "Detach pattern" to "Detach". ([56323](https://github.com/WordPress/gutenberg/pull/56323)) +- useEntityRecord: Improve unit tests. ([56415](https://github.com/WordPress/gutenberg/pull/56415)) + +#### Components +- Add focus rings to focusable disabled buttons. ([56383](https://github.com/WordPress/gutenberg/pull/56383)) +- DropdownMenu V2 tweaks. ([56041](https://github.com/WordPress/gutenberg/pull/56041)) +- DropdownMenu V2: Add support for rendering in legacy popover slot. ([56342](https://github.com/WordPress/gutenberg/pull/56342)) +- FormToggle: Refine animation. ([56515](https://github.com/WordPress/gutenberg/pull/56515)) +- Slot: Add styles prop to bubblesVirtually version. ([56428](https://github.com/WordPress/gutenberg/pull/56428)) +- Tabs: Cleanup and improvements. ([56224](https://github.com/WordPress/gutenberg/pull/56224)) +- Try Ariakit Select for new CustomSelectControl component. ([55790](https://github.com/WordPress/gutenberg/pull/55790)) + +#### Data Views +- Data list view: Make filter row, table header, and pagination sticky. ([56157](https://github.com/WordPress/gutenberg/pull/56157)) +- Simplify dataviews view button. ([56485](https://github.com/WordPress/gutenberg/pull/56485)) +- Update data view menu item actions. ([56398](https://github.com/WordPress/gutenberg/pull/56398)) + +#### Global Styles +- Global style revisions: Redesign style revision items. ([55913](https://github.com/WordPress/gutenberg/pull/55913)) +- Global styles revisions: Migrate API call to getRevisions(). ([56349](https://github.com/WordPress/gutenberg/pull/56349)) +- Style Revisions: Remove style revisions dropdown menu. ([56454](https://github.com/WordPress/gutenberg/pull/56454)) + +#### Site Editor +- Add 'View site' action to 'Site updated' snackbar. ([52693](https://github.com/WordPress/gutenberg/pull/52693)) +- Add the Post Author component to the Page sidebar. ([56368](https://github.com/WordPress/gutenberg/pull/56368)) +- Redirect to main page menu if page record not found. ([56177](https://github.com/WordPress/gutenberg/pull/56177)) + +#### Block Editor +- Drag and drop: Allow dragging to the beginning and end of a document. ([56070](https://github.com/WordPress/gutenberg/pull/56070)) +- List View: Expand state if a block is dragged to within a collapsed block in the editor canvas. ([56493](https://github.com/WordPress/gutenberg/pull/56493)) + +#### Layout +- Add layout classes to legacy Group inner container. ([56130](https://github.com/WordPress/gutenberg/pull/56130)) +- Add setting to disable custom content size controls. ([56236](https://github.com/WordPress/gutenberg/pull/56236)) + +#### Patterns +- Small tweaks to CreatePatternModal. ([56016](https://github.com/WordPress/gutenberg/pull/56016)) +- Update Labels in Block Inserter (block patterns tab). ([55986](https://github.com/WordPress/gutenberg/pull/55986)) + +#### Icons +- Update trash icon. ([56569](https://github.com/WordPress/gutenberg/pull/56569)) + +#### Block Library +- Disable block renaming support for Nav Link block. ([56425](https://github.com/WordPress/gutenberg/pull/56425)) + +#### Distraction Free +- Add top toolbar to distraction free mode. ([56295](https://github.com/WordPress/gutenberg/pull/56295)) + +#### CSS & Styling +- Gallery Block: Use styled scrollbars for image captions. ([56252](https://github.com/WordPress/gutenberg/pull/56252)) + +#### Typography +- Font Library: Remove insecure properties. ([56230](https://github.com/WordPress/gutenberg/pull/56230)) + + +### New APIs + +- Revisions: Add new selectors to fetch entity revisions. ([54046](https://github.com/WordPress/gutenberg/pull/54046)) + +#### Interactivity API +- Migration to the new `store()` API. ([55459](https://github.com/WordPress/gutenberg/pull/55459)) + + +### Bug Fixes + +- Block Editor: Undeprecate the '__experimentalImageSizeControl' component. ([56414](https://github.com/WordPress/gutenberg/pull/56414)) +- Core data: Harmonize getRevision selector and resolver function signatures. ([56416](https://github.com/WordPress/gutenberg/pull/56416)) +- Editor styles: Scope without adding specificity. ([56564](https://github.com/WordPress/gutenberg/pull/56564)) +- Fix Restore Post title placeholder. ([56580](https://github.com/WordPress/gutenberg/pull/56580)) +- Post Schedule Panel: Remove text overflow ellipsis. ([56319](https://github.com/WordPress/gutenberg/pull/56319)) +- PostCSS style transformation: Fail gracefully instead of throwing an error. ([56093](https://github.com/WordPress/gutenberg/pull/56093)) +- Rich text: Pad multiple spaces through en/em replacement. ([56341](https://github.com/WordPress/gutenberg/pull/56341)) +- Site Editor Sidebar: Fix actions vertical alignment. ([56218](https://github.com/WordPress/gutenberg/pull/56218)) +- Site Editor: Add a fallback template showing the title and content for the post only mode. ([56509](https://github.com/WordPress/gutenberg/pull/56509)) +- useEntityRecord: Do not trigger REST API requests when disabled. ([56108](https://github.com/WordPress/gutenberg/pull/56108)) + +#### Block Library +- File block: Remove anchor tag when copy pasting to file name. ([56508](https://github.com/WordPress/gutenberg/pull/56508)) +- Fix label of columns inspector panel. ([56647](https://github.com/WordPress/gutenberg/pull/56647)) +- Post Template: Fix incorrect offset query. ([56440](https://github.com/WordPress/gutenberg/pull/56440)) + +#### Block Editor +- (RichText)(Workaround)(17.1.x) Fallback to a string arg in `collapseWhiteSpace()` if `value` is not a string. ([56570](https://github.com/WordPress/gutenberg/pull/56570)) +- Cover block: Pass dropZoneElement reference to fix dragging within cover block area. ([56312](https://github.com/WordPress/gutenberg/pull/56312)) +- useMovingAnimation: Clear translate3d rule when animation is finished. ([56410](https://github.com/WordPress/gutenberg/pull/56410)) + +#### Components +- Design Tools: Fix last ToolsPanelItem styling. ([56536](https://github.com/WordPress/gutenberg/pull/56536)) +- Fix FormTokenField suggestions broken scrollbar when `__experimentalExpandOnFocus` is defined. ([56426](https://github.com/WordPress/gutenberg/pull/56426)) +- Tabs: Fix flaky unit tests. ([55950](https://github.com/WordPress/gutenberg/pull/55950)) + +#### Global Styles +- Additional CSS: Fix on change validation. ([56434](https://github.com/WordPress/gutenberg/pull/56434)) +- Global styles revisions: Update isResolving flag. ([56491](https://github.com/WordPress/gutenberg/pull/56491)) +- Spacing: Fix block error if spacing unit array empty in theme.json. ([56306](https://github.com/WordPress/gutenberg/pull/56306)) + +#### CSS & Styling +- Reduce specificity of default Cover text color styles. ([56411](https://github.com/WordPress/gutenberg/pull/56411)) +- Restore Post Title visual styles in Code View mode. ([56582](https://github.com/WordPress/gutenberg/pull/56582)) + +#### Saving +- Editor: Reinstate anonymous callback for saved post state. ([56529](https://github.com/WordPress/gutenberg/pull/56529)) + +#### Post Editor +- Save post button: Avoid extra re-renders when enablng/disabling tooltip. ([56502](https://github.com/WordPress/gutenberg/pull/56502)) + +#### Plugin +- Update Readme.txt tested up to 6.4. ([56427](https://github.com/WordPress/gutenberg/pull/56427)) + +#### Site Editor +- Fix template resolution for templates assigned as home page. ([56418](https://github.com/WordPress/gutenberg/pull/56418)) + +#### Patterns +- Fix issue with template in replace template screen. ([56407](https://github.com/WordPress/gutenberg/pull/56407)) + +#### Layout +- Fix issue where layout classnames are injected for blocks without layout support. ([56187](https://github.com/WordPress/gutenberg/pull/56187)) + +#### Typography +- Font Library: Fix fonts not displaying correctly. ([55393](https://github.com/WordPress/gutenberg/pull/55393)) + +#### Colors +- Duotone: Backport from Core to fix filters in classic themes. ([54778](https://github.com/WordPress/gutenberg/pull/54778)) + + +### Accessibility + +- Migrating `StyleBook` to use updated `Composite` implementation. ([55344](https://github.com/WordPress/gutenberg/pull/55344)) + +#### Data Views +- DataViews: Make disabled pagination buttons focusable. ([56422](https://github.com/WordPress/gutenberg/pull/56422)) + +#### Block Library +- Image Block: Enable image block to be selected correctly when clicked. ([56043](https://github.com/WordPress/gutenberg/pull/56043)) + +#### Post Editor +- Tooltip: Don't render buttons tooltip when show button text labels is enabled. ([55842](https://github.com/WordPress/gutenberg/pull/55842)) + +#### Components +- Improve `Button` saving state accessibility. ([55547](https://github.com/WordPress/gutenberg/pull/55547)) + +#### Patterns +- Fix focus loss after converting to a synced pattern. ([55473](https://github.com/WordPress/gutenberg/pull/55473)) + + +### Performance + +- Avoid calling postcss when not needed. ([56601](https://github.com/WordPress/gutenberg/pull/56601)) +- Block Editor: Optimize 'Connections' inspector controls. ([56443](https://github.com/WordPress/gutenberg/pull/56443)) + +#### Global Styles +- Make search more responsive for block type list. ([56139](https://github.com/WordPress/gutenberg/pull/56139)) + + +### Experiments + +#### Data Views +- DataViews: Document `view.layout`. ([56637](https://github.com/WordPress/gutenberg/pull/56637)) +- DataViews: Extract common constants to file. ([56251](https://github.com/WordPress/gutenberg/pull/56251)) +- DataViews: Rename `InFilter` component to `FilterSummary`. ([56506](https://github.com/WordPress/gutenberg/pull/56506)) +- DataViews: Scope names of V2 UI components. ([56503](https://github.com/WordPress/gutenberg/pull/56503)) +- DataViews: Update field API to generate filters based on type. ([55996](https://github.com/WordPress/gutenberg/pull/55996)) +- DataViews: Update filter component. ([56110](https://github.com/WordPress/gutenberg/pull/56110)) +- Dataviews: Add confirmation step before deleting a page. ([56504](https://github.com/WordPress/gutenberg/pull/56504)) +- Dataviews: Add preview and grid view in templates list. ([56382](https://github.com/WordPress/gutenberg/pull/56382)) +- Dataviews: Grid layout refinements. ([56441](https://github.com/WordPress/gutenberg/pull/56441)) +- Dataviews: Remove link from author. ([56467](https://github.com/WordPress/gutenberg/pull/56467)) +- Dataviews: Update item actions in grid view. ([56501](https://github.com/WordPress/gutenberg/pull/56501)) +- Fix data view menu item radius. ([56395](https://github.com/WordPress/gutenberg/pull/56395)) + +#### Post Editor +- Render html in post titles in visual mode and edit HTML in post title in code view. ([54718](https://github.com/WordPress/gutenberg/pull/54718)) + + +### Documentation + +- Add the attributes definition page to the create block tutorial of the platform documentation. ([56429](https://github.com/WordPress/gutenberg/pull/56429)) +- Add the transforms page to the create block tutorial of the platform documentation. ([56559](https://github.com/WordPress/gutenberg/pull/56559)) +- Add thee block supports page to the create block tutorial of the framework docs. ([56483](https://github.com/WordPress/gutenberg/pull/56483)) +- Added clarifications and examples to "Get started with wp-scripts". ([56298](https://github.com/WordPress/gutenberg/pull/56298)) +- Block Editor: Fix typo in `URLInput`'s `onKeyDown` prop documentation. ([56322](https://github.com/WordPress/gutenberg/pull/56322)) +- Bring back non-JS tabs in block editor handbook. ([56561](https://github.com/WordPress/gutenberg/pull/56561)) +- Docs: Fix incorrect build script description in script package. ([56332](https://github.com/WordPress/gutenberg/pull/56332)) +- Docs: Fundamentals of Block Development - File structure of a block. ([56551](https://github.com/WordPress/gutenberg/pull/56551)) +- Docs: Fundamentals of Block Development - Registration of a block. ([56334](https://github.com/WordPress/gutenberg/pull/56334)) +- Docs: Fundamentals of Block Development - The block wrapper. ([56596](https://github.com/WordPress/gutenberg/pull/56596)) +- Docs: Fundamentals of Block Development - Working with Javascript in the Block Editor. ([56553](https://github.com/WordPress/gutenberg/pull/56553)) +- Docs: Fundamentals of Block Development - block.json. ([56435](https://github.com/WordPress/gutenberg/pull/56435)) +- Docs: Improve downloadBlob example. ([56225](https://github.com/WordPress/gutenberg/pull/56225)) +- Documentation - Block Editor Handbook - Add end user documentation about Block Editor as a resource on the Landing Page of the Block Editor Handbook. ([49854](https://github.com/WordPress/gutenberg/pull/49854)) +- Fix overly complex code example in ComboboxControl readme. ([56365](https://github.com/WordPress/gutenberg/pull/56365)) +- Fix version in useSetting deprecation notice. ([56377](https://github.com/WordPress/gutenberg/pull/56377)) +- Fundamentals block development - landing and first pages. ([56584](https://github.com/WordPress/gutenberg/pull/56584)) +- Fundamentals of Block Development - fix save definition. ([56605](https://github.com/WordPress/gutenberg/pull/56605)) +- Link preview image to live example using WordPress Playground. ([56292](https://github.com/WordPress/gutenberg/pull/56292)) +- NavigableContainers: Fix doc typo in onKeyDown prop. ([56352](https://github.com/WordPress/gutenberg/pull/56352)) +- Release docs: Add new section about troubleshooting the release. ([56436](https://github.com/WordPress/gutenberg/pull/56436)) +- Remove all {% codetabs %} instances and any vanilla JS references. ([56121](https://github.com/WordPress/gutenberg/pull/56121)) +- Simplify code example in ToggleControl component readme. ([56389](https://github.com/WordPress/gutenberg/pull/56389)) +- Text and Heading: Improve documentation around default values and truncation logic. ([56518](https://github.com/WordPress/gutenberg/pull/56518)) +- Theme JSON schema: Add heading/button key to color definition. ([55674](https://github.com/WordPress/gutenberg/pull/55674)) +- Update for 6.4.1 for versions in WP. ([56216](https://github.com/WordPress/gutenberg/pull/56216)) +- Update references to the gutenberg-examples repo to the new block-development-examples. ([56119](https://github.com/WordPress/gutenberg/pull/56119)) +- Update template name in `create-block` command. ([56281](https://github.com/WordPress/gutenberg/pull/56281)) +- Update webpack options for wp-scripts in README.md. ([56314](https://github.com/WordPress/gutenberg/pull/56314)) +- `BoxControl`: Update story and refactor to Typescript. ([56462](https://github.com/WordPress/gutenberg/pull/56462)) + + +### Code Quality + +- Blocks pkg: Remove 'browser' dependencies. ([56433](https://github.com/WordPress/gutenberg/pull/56433)) +- DataViews: Code Quality remove some unused props from action. ([56477](https://github.com/WordPress/gutenberg/pull/56477)) +- Editor: Move the template focus modes to the editor store. ([56472](https://github.com/WordPress/gutenberg/pull/56472)) +- Extract a PostPanelRow component from the different sidebar panels. ([56238](https://github.com/WordPress/gutenberg/pull/56238)) +- Interactivity API: Add missing changelog entry for the new `store()` API. ([56611](https://github.com/WordPress/gutenberg/pull/56611)) +- Migrating block editor `BlockPatternsList` component. ([56210](https://github.com/WordPress/gutenberg/pull/56210)) +- Move the DisableNonContentBlocks component to the editor package. ([56423](https://github.com/WordPress/gutenberg/pull/56423)) +- Post Schedule Panel: Fix Sass deprecation warning for division. ([56412](https://github.com/WordPress/gutenberg/pull/56412)) +- Remove compatibility layer for WP 6.2. ([56464](https://github.com/WordPress/gutenberg/pull/56464)) +- Unify the PostSchedule component between site and post editors. ([56196](https://github.com/WordPress/gutenberg/pull/56196)) +- Update: Refactor useAddedBy to use authorText and originalSource fields. ([56568](https://github.com/WordPress/gutenberg/pull/56568)) + +#### Block Library +- Add align support to the image block - alternative. ([55954](https://github.com/WordPress/gutenberg/pull/55954)) +- Backmerge block renaming fixes/refactors from 6.4 branch into Gutenberg trunk. ([56386](https://github.com/WordPress/gutenberg/pull/56386)) +- Pattern placeholder: Remove duplicate 'useDispatch' hook. ([56397](https://github.com/WordPress/gutenberg/pull/56397)) + +#### Components +- Remove incorrect version from deprecated `__nextHasNoMarginBottom` prop of `AnglePickerControl` Component. ([56336](https://github.com/WordPress/gutenberg/pull/56336)) +- Revert "DropdownMenu V2: Add support for rendering in legacy popover slot". ([56484](https://github.com/WordPress/gutenberg/pull/56484)) + +#### Data Views +- Dataviews: Ensure items and fields are using a unique id. ([56366](https://github.com/WordPress/gutenberg/pull/56366)) + +#### Block Editor +- useInnerBlocksProps: Stabilise dropZoneElement prop. ([56313](https://github.com/WordPress/gutenberg/pull/56313)) + +#### Design Tools +- Fix: Theme.json font settings in unit test. ([56309](https://github.com/WordPress/gutenberg/pull/56309)) + + +### Tools + +- Workflows: Update 'days-before-stale' for flaky test report issues. ([56585](https://github.com/WordPress/gutenberg/pull/56585)) +- scripts: Update `jest-dev-server` to v9. ([56552](https://github.com/WordPress/gutenberg/pull/56552)) + +#### Testing +- Dataviews: Add first end-to-end tests. ([56634](https://github.com/WordPress/gutenberg/pull/56634)) +- Migrate 'align hook' end-to-end tests to Playwright. ([56480](https://github.com/WordPress/gutenberg/pull/56480)) +- Migrate 'block directory' end-to-end tests to Playwright. ([56593](https://github.com/WordPress/gutenberg/pull/56593)) +- Migrate 'block icons' end-to-end tests to Playwright. ([56610](https://github.com/WordPress/gutenberg/pull/56610)) +- Migrate 'custom taxonomies' end-to-end test to Playwright. ([56486](https://github.com/WordPress/gutenberg/pull/56486)) +- Migrate 'sidebar permalink' end-to-end tests to Playwright. ([56253](https://github.com/WordPress/gutenberg/pull/56253)) +- Migrate Is Typing Test to Playwright. ([56616](https://github.com/WordPress/gutenberg/pull/56616)) +- Page spec: Merging create page and toggle preview tests. ([56129](https://github.com/WordPress/gutenberg/pull/56129)) +- Playwright Utils: Fix the method of getting post ID in 'publishPost'. ([56421](https://github.com/WordPress/gutenberg/pull/56421)) +- end-to-end tests: Merge Puppeteer into single job, split Playwright further. ([56363](https://github.com/WordPress/gutenberg/pull/56363)) + +#### Build Tooling +- Create block: Update `interactive-template` to the new `store()` API. ([56613](https://github.com/WordPress/gutenberg/pull/56613)) + + +### Security + +- WP_Theme_JSON_Gutenberg: Add nested indexed array schema sanitization. ([56447](https://github.com/WordPress/gutenberg/pull/56447)) + + +### Various + +- Add: Author text and original source to wp_template_part. ([56567](https://github.com/WordPress/gutenberg/pull/56567)) +- Migrating `BlockPatternSetup` to use updated `Composite` implementation. ([55425](https://github.com/WordPress/gutenberg/pull/55425)) +- Migrating `InserterListbox` to use updated Composite implementation. ([56246](https://github.com/WordPress/gutenberg/pull/56246)) + +#### Data Views +- Dataviews: All Templates: Add filters to template author. ([56338](https://github.com/WordPress/gutenberg/pull/56338)) +- Dataviews: All templates: Add: Sorting to template author and add author_text to the rest API. ([56333](https://github.com/WordPress/gutenberg/pull/56333)) + +#### HTML API +- Backport updates from Core. ([56578](https://github.com/WordPress/gutenberg/pull/56578)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@aaronrobertshaw @afercia @andrewhayward @andrewserong @annezazu @apeatling @arthur791004 @bph @brookewp @chad1008 @chiilog @ciampo @DAreRodz @dmsnell @draganescu @ellatrix @fabiankaegy @flootr @fluiddot @fullofcaffeine @geriux @getdave @glendaviesnz @jameskoster @jasmussen @jeryj @jffng @jorgefilipecosta @juanmaguitar @kevin940726 @luisherranz @MaggieCabrera @Mamaduka @matiasbenedetto @megane9988 @NekoJonez @ntsekouras @oandregal @ramonjd @richtabor @ryanwelcher @SavPhill @Soean @t-hamano @talldan @tellthemachines @youknowriad @zaguiini + + + + = 17.1.4 = ## Changelog From ed1b2467b0cdbc1ac553d71fd6d16bd254da4c8b Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Wed, 6 Dec 2023 15:47:44 -0500 Subject: [PATCH 059/325] feat: Add Hook to monitor network connectivity status (#56609) * feat: Frame useNetInfo hook foundation This code is non-functioning currently. * feat: Add iOS connection status bridge utilities This bridge will be required for the planned JavaScript Hook to monitor connection status. * feat: Add `useIsConnected` hook Provides React Hook for monitoring the network connection status via the bridge to the host app. * Revert "feat: Frame useNetInfo hook foundation" This reverts commit a8d3660845457787f6b368fe0276bfcfdbd213a6. * refactor: Align with project Swift syntax Semicolon is unnecessary. Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> * feat: Add Android connection status bridge utilities This bridge enables monitoring the connection status on Android. * feat: Android network connection status request utility Allow the Android platform to request the current network connection status. * fix: Add missing `requestConnectionStatus` bridge method mock The Demo editor fails to build without a mocked bridge method. --------- Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> --- .../GutenbergBridgeJS2Parent.java | 6 +++ .../RNReactNativeGutenbergBridgeModule.java | 17 +++++++ .../WPAndroidGlue/DeferredEventEmitter.java | 9 ++++ .../WPAndroidGlue/WPAndroidGlueCode.java | 17 +++++++ packages/react-native-bridge/index.js | 48 +++++++++++++++++++ .../react-native-bridge/ios/Gutenberg.swift | 5 ++ .../ios/GutenbergBridgeDelegate.swift | 2 + .../ios/RNReactNativeGutenbergBridge.m | 1 + .../ios/RNReactNativeGutenbergBridge.swift | 6 +++ .../java/com/gutenberg/MainApplication.java | 5 ++ .../GutenbergViewController.swift | 4 ++ 11 files changed, 120 insertions(+) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index c6e20b29db072e..c1dc4bab896b3a 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -60,6 +60,10 @@ interface BlockTypeImpressionsCallback { void onRequestBlockTypeImpressions(ReadableMap impressions); } + interface ConnectionStatusCallback { + void onRequestConnectionStatus(boolean isConnected); + } + // Ref: https://github.com/facebook/react-native/blob/HEAD/Libraries/polyfills/console.js#L376 enum LogLevel { TRACE(0), @@ -183,4 +187,6 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void toggleUndoButton(boolean isDisabled); void toggleRedoButton(boolean isDisabled); + + void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback); } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index d922d863cb3011..0073db769d9cd5 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ConnectionStatusCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaType; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.OtherMediaOptionsReceivedCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FocalPointPickerTooltipShownCallback; @@ -85,6 +86,8 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; + public static final String MAP_KEY_IS_CONNECTED = "isConnected"; + private boolean mIsDarkMode; public RNReactNativeGutenbergBridgeModule(ReactApplicationContext reactContext, @@ -533,4 +536,18 @@ public void generateHapticFeedback() { } } } + + @ReactMethod + public void requestConnectionStatus(final Callback jsCallback) { + ConnectionStatusCallback connectionStatusCallback = requestConnectionStatusCallback(jsCallback); + mGutenbergBridgeJS2Parent.requestConnectionStatus(connectionStatusCallback); + } + + private ConnectionStatusCallback requestConnectionStatusCallback(final Callback jsCallback) { + return new GutenbergBridgeJS2Parent.ConnectionStatusCallback() { + @Override public void onRequestConnectionStatus(boolean isConnected) { + jsCallback.invoke(isConnected); + } + }; + } } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index 7dd4dbf3811feb..fe83bc8a14b540 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -15,6 +15,7 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_IS_CONNECTED; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL; @@ -44,6 +45,8 @@ public interface JSEventEmitter { private static final String EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED = "featuredImageIdNativeUpdated"; + private static final String EVENT_CONNECTION_STATUS_CHANGE = "connectionStatusChange"; + private static final String MAP_KEY_MEDIA_FILE_STATE = "state"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS = "progress"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_SERVER_ID = "mediaServerId"; @@ -222,6 +225,12 @@ public void sendToJSFeaturedImageId(int mediaId) { queueActionToJS(EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED, writableMap); } + public void onConnectionStatusChange(boolean isConnected) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putBoolean(MAP_KEY_IS_CONNECTED, isConnected); + queueActionToJS(EVENT_CONNECTION_STATUS_CHANGE, writableMap); + } + @Override public void onReplaceMediaFilesEditedBlock(String mediaFiles, String blockId) { WritableMap writableMap = new WritableNativeMap(); writableMap.putString(MAP_KEY_REPLACE_BLOCK_HTML, mediaFiles); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 69adb653211da2..c0916d1417a34f 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -112,6 +112,7 @@ public class WPAndroidGlueCode { private OnToggleUndoButtonListener mOnToggleUndoButtonListener; private OnToggleRedoButtonListener mOnToggleRedoButtonListener; + private OnConnectionStatusEventListener mOnConnectionStatusEventListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -259,6 +260,10 @@ public interface OnToggleRedoButtonListener { void onToggleRedoButton(boolean isDisabled); } + public interface OnConnectionStatusEventListener { + boolean onRequestConnectionStatus(); + } + public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -594,6 +599,12 @@ public void toggleUndoButton(boolean isDisabled) { public void toggleRedoButton(boolean isDisabled) { mOnToggleRedoButtonListener.onToggleRedoButton(isDisabled); } + + @Override + public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { + boolean isConnected = mOnConnectionStatusEventListener.onRequestConnectionStatus(); + connectionStatusCallback.onRequestConnectionStatus(isConnected); + } }, mIsDarkMode); return Arrays.asList( @@ -688,6 +699,7 @@ public void attachToContainer(ViewGroup viewGroup, OnSendEventToHostListener onSendEventToHostListener, OnToggleUndoButtonListener onToggleUndoButtonListener, OnToggleRedoButtonListener onToggleRedoButtonListener, + OnConnectionStatusEventListener onConnectionStatusEventListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -713,6 +725,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnSendEventToHostListener = onSendEventToHostListener; mOnToggleUndoButtonListener = onToggleUndoButtonListener; mOnToggleRedoButtonListener = onToggleRedoButtonListener; + mOnConnectionStatusEventListener = onConnectionStatusEventListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -1149,6 +1162,10 @@ public void sendToJSFeaturedImageId(int mediaId) { mDeferredEventEmitter.sendToJSFeaturedImageId(mediaId); } + public void connectionStatusChange(boolean isConnected) { + mDeferredEventEmitter.onConnectionStatusChange(isConnected); + } + public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 89f9f029901f9a..8e9065cc568e56 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -3,6 +3,11 @@ */ import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; + const { RNReactNativeGutenbergBridge } = NativeModules; const isIOS = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; @@ -185,6 +190,49 @@ export function subscribeOnRedoPressed( callback ) { return gutenbergBridgeEvents.addListener( 'onRedoPressed', callback ); } +export function useIsConnected() { + const [ isConnected, setIsConnected ] = useState( null ); + + useEffect( () => { + let isCurrent = true; + + RNReactNativeGutenbergBridge.requestConnectionStatus( + ( isBridgeConnected ) => { + if ( ! isCurrent ) { + return; + } + + setIsConnected( isBridgeConnected ); + } + ); + + return () => { + isCurrent = false; + }; + }, [] ); + + useEffect( () => { + const subscription = subscribeConnectionStatus( + ( { isConnected: isBridgeConnected } ) => { + setIsConnected( isBridgeConnected ); + } + ); + + return () => { + subscription.remove(); + }; + }, [] ); + + return { isConnected }; +} + +function subscribeConnectionStatus( callback ) { + return gutenbergBridgeEvents.addListener( + 'connectionStatusChange', + callback + ); +} + /** * Request media picker for the given media source. * diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 4175c1e2343c32..de0d1b513f00dc 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -210,6 +210,11 @@ public class Gutenberg: UIResponder { bridgeModule.sendEventIfNeeded(.onRedoPressed, body: nil) } + public func connectionStatusChange(isConnected: Bool) { + var data: [String: Any] = ["isConnected": isConnected] + bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) + } + private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() settingsUpdates["isFSETheme"] = editorSettings?.isFSETheme ?? false diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 83d087bccab9d1..8890cd4de0f7ec 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -283,6 +283,8 @@ public protocol GutenbergBridgeDelegate: AnyObject { func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) + + func gutenbergDidRequestConnectionStatus() -> Bool } // MARK: - Optional GutenbergBridgeDelegate methods diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index d333f8c1722ad9..3d68e51ebcacb5 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -42,5 +42,6 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(generateHapticFeedback) RCT_EXTERN_METHOD(toggleUndoButton:(BOOL)isDisabled) RCT_EXTERN_METHOD(toggleRedoButton:(BOOL)isDisabled) +RCT_EXTERN_METHOD(requestConnectionStatus:(RCTResponseSenderBlock)callback) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 8cf4f685bd22c4..ec763b2b8aaa2c 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -421,6 +421,11 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { func toggleRedoButton(_ isDisabled: Bool) { self.delegate?.gutenbergDidRequestToggleRedoButton(isDisabled) } + + @objc + func requestConnectionStatus(_ callback: @escaping RCTResponseSenderBlock) { + callback([self.delegate?.gutenbergDidRequestConnectionStatus() ?? true]) + } } // MARK: - RCTBridgeModule delegate @@ -450,6 +455,7 @@ extension RNReactNativeGutenbergBridge { case showEditorHelp case onUndoPressed case onRedoPressed + case connectionStatusChange } public override func supportedEvents() -> [String]! { diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index d718b34f25db3b..4477f1cc1d9f35 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -308,6 +308,11 @@ public void toggleRedoButton(boolean isDisabled) { mainActivity.updateRedoItem(isDisabled); } } + + @Override + public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { + connectionStatusCallback.onRequestConnectionStatus(true); + } }, isDarkMode()); return new DefaultReactNativeHost(this) { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index b269d1feb8ddfe..ef95c7e65862f6 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -345,6 +345,10 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } } } + + func gutenbergDidRequestConnectionStatus() -> Bool { + return true + } } extension GutenbergViewController: GutenbergWebDelegate { From d7226112354dcca205750cbfa4a34160b040cbf3 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 6 Dec 2023 23:18:51 +0200 Subject: [PATCH 060/325] Block editor: make all BlockEdit hooks pure (#56813) --- packages/block-editor/src/hooks/align.js | 18 +- packages/block-editor/src/hooks/anchor.js | 15 +- packages/block-editor/src/hooks/background.js | 3 + .../block-editor/src/hooks/block-hooks.js | 9 +- .../block-editor/src/hooks/block-renaming.js | 56 +++-- packages/block-editor/src/hooks/border.js | 3 + packages/block-editor/src/hooks/color.js | 3 + .../block-editor/src/hooks/content-lock-ui.js | 225 +++++++++--------- .../src/hooks/custom-class-name.js | 15 +- .../block-editor/src/hooks/custom-fields.js | 28 ++- packages/block-editor/src/hooks/dimensions.js | 3 + packages/block-editor/src/hooks/duotone.js | 23 +- packages/block-editor/src/hooks/layout.js | 24 +- packages/block-editor/src/hooks/position.js | 34 ++- packages/block-editor/src/hooks/typography.js | 3 + 15 files changed, 291 insertions(+), 171 deletions(-) diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 563c7bae6cde93..3b916d9577f1a7 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -108,9 +108,9 @@ export function addAttribute( settings ) { return settings; } -function BlockEditAlignmentToolbarControls( { +function BlockEditAlignmentToolbarControlsPure( { blockName, - attributes, + align, setAttributes, } ) { // Compute the block valid alignments by taking into account, @@ -144,7 +144,7 @@ function BlockEditAlignmentToolbarControls( { return ( <BlockControls group="block" __experimentalShareWithChildBlocks> <BlockAlignmentControl - value={ attributes.align } + value={ align } onChange={ updateAlignment } controls={ validAlignments } /> @@ -152,6 +152,13 @@ function BlockEditAlignmentToolbarControls( { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const BlockEditAlignmentToolbarControls = pure( + BlockEditAlignmentToolbarControlsPure +); + /** * Override the default edit UI to include new toolbar controls for block * alignment, if block defines support. @@ -173,7 +180,8 @@ export const withAlignmentControls = createHigherOrderComponent( { hasAlignmentSupport && ( <BlockEditAlignmentToolbarControls blockName={ props.name } - attributes={ props.attributes } + // This component is pure, so only pass needed props! + align={ props.attributes.align } setAttributes={ props.setAttributes } /> ) } diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 3d404c4a868116..9902ed479531c6 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -5,7 +5,7 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { Platform } from '@wordpress/element'; /** @@ -52,7 +52,7 @@ export function addAttribute( settings ) { return settings; } -function BlockEditAnchorControl( { blockName, attributes, setAttributes } ) { +function BlockEditAnchorControlPure( { blockName, anchor, setAttributes } ) { const blockEditingMode = useBlockEditingMode(); const isWeb = Platform.OS === 'web'; @@ -79,7 +79,7 @@ function BlockEditAnchorControl( { blockName, attributes, setAttributes } ) { ) } </> } - value={ attributes.anchor || '' } + value={ anchor || '' } placeholder={ ! isWeb ? __( 'Add an anchor' ) : null } onChange={ ( nextValue ) => { nextValue = nextValue.replace( ANCHOR_REGEX, '-' ); @@ -116,6 +116,11 @@ function BlockEditAnchorControl( { blockName, attributes, setAttributes } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const BlockEditAnchorControl = pure( BlockEditAnchorControlPure ); + /** * Override the default edit UI to include a new block inspector control for * assigning the anchor ID, if block supports anchor. @@ -133,7 +138,9 @@ export const withAnchorControls = createHigherOrderComponent( ( BlockEdit ) => { hasBlockSupport( props.name, 'anchor' ) && ( <BlockEditAnchorControl blockName={ props.name } - attributes={ props.attributes } + // This component is pure, so only pass needed + // props! + anchor={ props.attributes.anchor } setAttributes={ props.setAttributes } /> ) } diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 342db267ff3315..b75dc95b75241f 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -318,4 +318,7 @@ function BackgroundImagePanelPure( props ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. export const BackgroundImagePanel = pure( BackgroundImagePanelPure ); diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 0d75999192e5bf..59c0e3c85486f0 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -9,7 +9,7 @@ import { PanelBody, ToggleControl, } from '@wordpress/components'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { createBlock, store as blocksStore } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -21,7 +21,7 @@ import { store as blockEditorStore } from '../store'; const EMPTY_OBJECT = {}; -function BlockHooksControl( props ) { +function BlockHooksControlPure( props ) { const blockTypes = useSelect( ( select ) => select( blocksStore ).getBlockTypes(), [] @@ -235,6 +235,11 @@ function BlockHooksControl( props ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const BlockHooksControl = pure( BlockHooksControlPure ); + export const withBlockHooksControls = createHigherOrderComponent( ( BlockEdit ) => { return ( props ) => { diff --git a/packages/block-editor/src/hooks/block-renaming.js b/packages/block-editor/src/hooks/block-renaming.js index 48e3b801d4eb91..452be6e686dbf4 100644 --- a/packages/block-editor/src/hooks/block-renaming.js +++ b/packages/block-editor/src/hooks/block-renaming.js @@ -3,7 +3,7 @@ */ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { TextControl } from '@wordpress/components'; @@ -47,30 +47,46 @@ export function addLabelCallback( settings ) { return settings; } +function BlockRenameControlPure( { name, metadata, setAttributes } ) { + const { canRename } = useBlockRename( name ); + + if ( ! canRename ) { + return null; + } + + return ( + <InspectorControls group="advanced"> + <TextControl + __nextHasNoMarginBottom + label={ __( 'Block name' ) } + value={ metadata?.name || '' } + onChange={ ( newName ) => { + setAttributes( { + metadata: { ...metadata, name: newName }, + } ); + } } + /> + </InspectorControls> + ); +} + +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const BlockRenameControl = pure( BlockRenameControlPure ); + export const withBlockRenameControl = createHigherOrderComponent( ( BlockEdit ) => ( props ) => { const { name, attributes, setAttributes, isSelected } = props; - - const { canRename } = useBlockRename( name ); - return ( <> - { isSelected && canRename && ( - <InspectorControls group="advanced"> - <TextControl - __nextHasNoMarginBottom - label={ __( 'Block name' ) } - value={ attributes?.metadata?.name || '' } - onChange={ ( newName ) => { - setAttributes( { - metadata: { - ...attributes?.metadata, - name: newName, - }, - } ); - } } - /> - </InspectorControls> + { isSelected && ( + <BlockRenameControl + name={ name } + // This component is pure, so only pass needed props! + metadata={ attributes.metadata } + setAttributes={ setAttributes } + /> ) } <BlockEdit key="edit" { ...props } /> </> diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index 29e4afd2f018ca..735d91e76538e5 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -175,6 +175,9 @@ function BorderPanelPure( { clientId, name, setAttributes } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. export const BorderPanel = pure( BorderPanelPure ); /** diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 94bcc599dd6371..6addd94d93ee58 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -361,6 +361,9 @@ function ColorEditPure( { clientId, name, setAttributes } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. export const ColorEdit = pure( ColorEditPure ); /** diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index 5d277d6a516d2d..8f95c14d118e56 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { ToolbarButton, MenuItem } from '@wordpress/components'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; @@ -37,120 +37,129 @@ function StopEditingAsBlocksOnOutsideSelect( { return null; } -export const withContentLockControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { getBlockListSettings, getSettings } = - useSelect( blockEditorStore ); - const focusModeToRevert = useRef(); - const { templateLock, isLockedByParent, isEditingAsBlocks } = useSelect( - ( select ) => { - const { - __unstableGetContentLockingParent, - getTemplateLock, - __unstableGetTemporarilyEditingAsBlocks, - } = select( blockEditorStore ); - return { - templateLock: getTemplateLock( props.clientId ), - isLockedByParent: !! __unstableGetContentLockingParent( - props.clientId - ), - isEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks() === - props.clientId, - }; - }, - [ props.clientId ] - ); +function ContentLockControlsPure( { clientId, isSelected } ) { + const { getBlockListSettings, getSettings } = useSelect( blockEditorStore ); + const focusModeToRevert = useRef(); + const { templateLock, isLockedByParent, isEditingAsBlocks } = useSelect( + ( select ) => { + const { + __unstableGetContentLockingParent, + getTemplateLock, + __unstableGetTemporarilyEditingAsBlocks, + } = select( blockEditorStore ); + return { + templateLock: getTemplateLock( clientId ), + isLockedByParent: + !! __unstableGetContentLockingParent( clientId ), + isEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks() === clientId, + }; + }, + [ clientId ] + ); - const { - updateSettings, - updateBlockListSettings, - __unstableSetTemporarilyEditingAsBlocks, - } = useDispatch( blockEditorStore ); - const isContentLocked = - ! isLockedByParent && templateLock === 'contentOnly'; - const { - __unstableMarkNextChangeAsNotPersistent, - updateBlockAttributes, - } = useDispatch( blockEditorStore ); + const { + updateSettings, + updateBlockListSettings, + __unstableSetTemporarilyEditingAsBlocks, + } = useDispatch( blockEditorStore ); + const isContentLocked = + ! isLockedByParent && templateLock === 'contentOnly'; + const { __unstableMarkNextChangeAsNotPersistent, updateBlockAttributes } = + useDispatch( blockEditorStore ); - const stopEditingAsBlock = useCallback( () => { - __unstableMarkNextChangeAsNotPersistent(); - updateBlockAttributes( props.clientId, { - templateLock: 'contentOnly', - } ); - updateBlockListSettings( props.clientId, { - ...getBlockListSettings( props.clientId ), - templateLock: 'contentOnly', - } ); - updateSettings( { focusMode: focusModeToRevert.current } ); - __unstableSetTemporarilyEditingAsBlocks(); - }, [ - props.clientId, - updateSettings, - updateBlockListSettings, - getBlockListSettings, - __unstableMarkNextChangeAsNotPersistent, - updateBlockAttributes, - __unstableSetTemporarilyEditingAsBlocks, - ] ); + const stopEditingAsBlock = useCallback( () => { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( clientId, { + templateLock: 'contentOnly', + } ); + updateBlockListSettings( clientId, { + ...getBlockListSettings( clientId ), + templateLock: 'contentOnly', + } ); + updateSettings( { focusMode: focusModeToRevert.current } ); + __unstableSetTemporarilyEditingAsBlocks(); + }, [ + clientId, + updateSettings, + updateBlockListSettings, + getBlockListSettings, + __unstableMarkNextChangeAsNotPersistent, + updateBlockAttributes, + __unstableSetTemporarilyEditingAsBlocks, + ] ); - if ( ! isContentLocked && ! isEditingAsBlocks ) { - return <BlockEdit key="edit" { ...props } />; - } + if ( ! isContentLocked && ! isEditingAsBlocks ) { + return null; + } + + const showStopEditingAsBlocks = isEditingAsBlocks && ! isContentLocked; + const showStartEditingAsBlocks = + ! isEditingAsBlocks && isContentLocked && isSelected; - const showStopEditingAsBlocks = isEditingAsBlocks && ! isContentLocked; - const showStartEditingAsBlocks = - ! isEditingAsBlocks && isContentLocked && props.isSelected; + return ( + <> + { showStopEditingAsBlocks && ( + <> + <StopEditingAsBlocksOnOutsideSelect + clientId={ clientId } + stopEditingAsBlock={ stopEditingAsBlock } + /> + <BlockControls group="other"> + <ToolbarButton + onClick={ () => { + stopEditingAsBlock(); + } } + > + { __( 'Done' ) } + </ToolbarButton> + </BlockControls> + </> + ) } + { showStartEditingAsBlocks && ( + <BlockSettingsMenuControls> + { ( { onClose } ) => ( + <MenuItem + onClick={ () => { + __unstableMarkNextChangeAsNotPersistent(); + updateBlockAttributes( clientId, { + templateLock: undefined, + } ); + updateBlockListSettings( clientId, { + ...getBlockListSettings( clientId ), + templateLock: false, + } ); + focusModeToRevert.current = + getSettings().focusMode; + updateSettings( { focusMode: true } ); + __unstableSetTemporarilyEditingAsBlocks( + clientId + ); + onClose(); + } } + > + { __( 'Modify' ) } + </MenuItem> + ) } + </BlockSettingsMenuControls> + ) } + </> + ); +} +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const ContentLockControls = pure( ContentLockControlsPure ); + +export const withContentLockControls = createHigherOrderComponent( + ( BlockEdit ) => ( props ) => { return ( <> - { showStopEditingAsBlocks && ( - <> - <StopEditingAsBlocksOnOutsideSelect - clientId={ props.clientId } - stopEditingAsBlock={ stopEditingAsBlock } - /> - <BlockControls group="other"> - <ToolbarButton - onClick={ () => { - stopEditingAsBlock(); - } } - > - { __( 'Done' ) } - </ToolbarButton> - </BlockControls> - </> - ) } - { showStartEditingAsBlocks && ( - <BlockSettingsMenuControls> - { ( { onClose } ) => ( - <MenuItem - onClick={ () => { - __unstableMarkNextChangeAsNotPersistent(); - updateBlockAttributes( props.clientId, { - templateLock: undefined, - } ); - updateBlockListSettings( props.clientId, { - ...getBlockListSettings( - props.clientId - ), - templateLock: false, - } ); - focusModeToRevert.current = - getSettings().focusMode; - updateSettings( { focusMode: true } ); - __unstableSetTemporarilyEditingAsBlocks( - props.clientId - ); - onClose(); - } } - > - { __( 'Modify' ) } - </MenuItem> - ) } - </BlockSettingsMenuControls> - ) } + <ContentLockControls + clientId={ props.clientId } + isSelected={ props.isSelected } + /> <BlockEdit key="edit" { ...props } /> </> ); diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index 8a3becc8691421..8c0f58ddda682d 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -10,7 +10,7 @@ import { addFilter } from '@wordpress/hooks'; import { TextControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -39,7 +39,7 @@ export function addAttribute( settings ) { return settings; } -function CustomClassNameControls( { attributes, setAttributes } ) { +function CustomClassNameControlsPure( { className, setAttributes } ) { const blockEditingMode = useBlockEditingMode(); if ( blockEditingMode !== 'default' ) { return null; @@ -52,7 +52,7 @@ function CustomClassNameControls( { attributes, setAttributes } ) { __next40pxDefaultSize autoComplete="off" label={ __( 'Additional CSS class(es)' ) } - value={ attributes.className || '' } + value={ className || '' } onChange={ ( nextValue ) => { setAttributes( { className: nextValue !== '' ? nextValue : undefined, @@ -64,6 +64,11 @@ function CustomClassNameControls( { attributes, setAttributes } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const CustomClassNameControls = pure( CustomClassNameControlsPure ); + /** * Override the default edit UI to include a new block inspector control for * assigning the custom class name, if block supports custom class name. @@ -87,7 +92,9 @@ export const withCustomClassNameControls = createHigherOrderComponent( <BlockEdit { ...props } /> { hasCustomClassName && props.isSelected && ( <CustomClassNameControls - attributes={ props.attributes } + // This component is pure, so only pass needed + // props! + className={ props.attributes.className } setAttributes={ props.setAttributes } /> ) } diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 8ab816abc7352a..19729d00ad61a6 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -5,7 +5,7 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent } from '@wordpress/compose'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -34,7 +34,7 @@ function addAttribute( settings ) { return settings; } -function CustomFieldsControl( props ) { +function CustomFieldsControlPure( { name, connections, setAttributes } ) { const blockEditingMode = useBlockEditingMode(); if ( blockEditingMode !== 'default' ) { return null; @@ -44,8 +44,8 @@ function CustomFieldsControl( props ) { // attribute to use for the connection. Only the `content` attribute // of the paragraph block and the `url` attribute of the image block are supported. let attributeName; - if ( props.name === 'core/paragraph' ) attributeName = 'content'; - if ( props.name === 'core/image' ) attributeName = 'url'; + if ( name === 'core/paragraph' ) attributeName = 'content'; + if ( name === 'core/image' ) attributeName = 'url'; return ( <InspectorControls> @@ -55,19 +55,17 @@ function CustomFieldsControl( props ) { autoComplete="off" label={ __( 'Custom field meta_key' ) } value={ - props.attributes?.connections?.attributes?.[ - attributeName - ]?.value || '' + connections?.attributes?.[ attributeName ]?.value || '' } onChange={ ( nextValue ) => { if ( nextValue === '' ) { - props.setAttributes( { + setAttributes( { connections: undefined, [ attributeName ]: undefined, placeholder: undefined, } ); } else { - props.setAttributes( { + setAttributes( { connections: { attributes: { // The attributeName will be either `content` or `url`. @@ -93,6 +91,11 @@ function CustomFieldsControl( props ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const CustomFieldsControl = pure( CustomFieldsControlPure ); + /** * Override the default edit UI to include a new block inspector control for * assigning a connection to blocks that has support for connections. @@ -121,7 +124,12 @@ const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { <> <BlockEdit key="edit" { ...props } /> { hasCustomFieldsSupport && props.isSelected && ( - <CustomFieldsControl { ...props } /> + <CustomFieldsControl + name={ props.name } + // This component is pure, so only pass needed props! + connections={ props.attributes.connections } + setAttributes={ props.setAttributes } + /> ) } </> ); diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 4e2b17f363bddf..c6d64d4ef785f3 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -132,6 +132,9 @@ function DimensionsPanelPure( { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. export const DimensionsPanel = pure( DimensionsPanelPure ); /** diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 5442e394e68c78..6e18b44cef1633 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -13,7 +13,11 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { + createHigherOrderComponent, + useInstanceId, + pure, +} from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; @@ -95,8 +99,7 @@ export function getDuotonePresetFromColors( colors, duotonePalette ) { return preset ? `var:preset|duotone|${ preset.slug }` : undefined; } -function DuotonePanel( { attributes, setAttributes, name } ) { - const style = attributes?.style; +function DuotonePanelPure( { style, setAttributes, name } ) { const duotoneStyle = style?.color?.duotone; const settings = useBlockSettings( name ); const blockEditingMode = useBlockEditingMode(); @@ -176,6 +179,11 @@ function DuotonePanel( { attributes, setAttributes, name } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const DuotonePanel = pure( DuotonePanelPure ); + /** * Filters registered block settings, extending attributes to include * the `duotone` attribute. @@ -227,7 +235,14 @@ const withDuotoneControls = createHigherOrderComponent( // performance. return ( <> - { hasDuotoneSupport && <DuotonePanel { ...props } /> } + { hasDuotoneSupport && ( + <DuotonePanel + // This component is pure, so only pass needed props! + style={ props.attributes.style } + setAttributes={ props.setAttributes } + name={ props.name } + /> + ) } <BlockEdit { ...props } /> </> ); diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 171290e180ee95..3ea5c56da8e776 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -6,7 +6,11 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { + createHigherOrderComponent, + pure, + useInstanceId, +} from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; @@ -133,12 +137,11 @@ export function useLayoutStyles( blockAttributes = {}, blockName, selector ) { return css; } -function LayoutPanel( { setAttributes, attributes, name: blockName } ) { +function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { const settings = useBlockSettings( blockName ); // Block settings come from theme.json under settings.[blockName]. const { layout: layoutSettings } = settings; // Layout comes from block attributes. - const { layout } = attributes; const [ defaultThemeLayout ] = useSettings( 'layout' ); const { themeSupportsLayout } = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); @@ -287,6 +290,11 @@ function LayoutPanel( { setAttributes, attributes, name: blockName } ) { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const LayoutPanel = pure( LayoutPanelPure ); + function LayoutTypeSwitcher( { type, onChange } ) { return ( <ButtonGroup> @@ -340,7 +348,15 @@ export const withLayoutControls = createHigherOrderComponent( const supportLayout = hasLayoutBlockSupport( props.name ); return [ - supportLayout && <LayoutPanel key="layout" { ...props } />, + supportLayout && ( + <LayoutPanel + key="layout" + // This component is pure, so only pass needed props! + layout={ props.attributes.layout } + setAttributes={ props.setAttributes } + name={ props.name } + /> + ), <BlockEdit key="edit" { ...props } />, ]; }, diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index 710dbfaf5ace04..32d4f6582969ee 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -12,7 +12,11 @@ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { + createHigherOrderComponent, + pure, + useInstanceId, +} from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo, Platform } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; @@ -207,14 +211,12 @@ export function useIsPositionDisabled( { name: blockName } = {} ) { * * @return {Element} Position panel. */ -export function PositionPanel( props ) { - const { - attributes: { style = {} }, - clientId, - name: blockName, - setAttributes, - } = props; - +export function PositionPanelPure( { + style = {}, + clientId, + name: blockName, + setAttributes, +} ) { const allowFixed = hasFixedPositionSupport( blockName ); const allowSticky = hasStickyPositionSupport( blockName ); const value = style?.position?.type; @@ -316,6 +318,11 @@ export function PositionPanel( props ) { } ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +const PositionPanel = pure( PositionPanelPure ); + /** * Override the default edit UI to include position controls. * @@ -335,7 +342,14 @@ export const withPositionControls = createHigherOrderComponent( return [ showPositionControls && ( - <PositionPanel key="position" { ...props } /> + <PositionPanel + key="position" + // This component is pure, so only pass needed props! + style={ props.attributes.style } + name={ blockName } + setAttributes={ props.setAttributes } + clientId={ props.clientId } + /> ), <BlockEdit key="edit" { ...props } />, ]; diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 7d0e5e1c318d56..d5bf9ec42ad040 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -153,6 +153,9 @@ function TypographyPanelPure( { ); } +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. export const TypographyPanel = pure( TypographyPanelPure ); export const hasTypographySupport = ( blockName ) => { From 49ac9cade99a33f92cb0187d1dbb9b9600ec4715 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Wed, 6 Dec 2023 16:37:57 -0500 Subject: [PATCH 061/325] Revert "feat: Add Hook to monitor network connectivity status (#56609)" (#56836) This reverts commit ed1b2467b0cdbc1ac553d71fd6d16bd254da4c8b. --- .../GutenbergBridgeJS2Parent.java | 6 --- .../RNReactNativeGutenbergBridgeModule.java | 17 ------- .../WPAndroidGlue/DeferredEventEmitter.java | 9 ---- .../WPAndroidGlue/WPAndroidGlueCode.java | 17 ------- packages/react-native-bridge/index.js | 48 ------------------- .../react-native-bridge/ios/Gutenberg.swift | 5 -- .../ios/GutenbergBridgeDelegate.swift | 2 - .../ios/RNReactNativeGutenbergBridge.m | 1 - .../ios/RNReactNativeGutenbergBridge.swift | 6 --- .../java/com/gutenberg/MainApplication.java | 5 -- .../GutenbergViewController.swift | 4 -- 11 files changed, 120 deletions(-) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index c1dc4bab896b3a..c6e20b29db072e 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -60,10 +60,6 @@ interface BlockTypeImpressionsCallback { void onRequestBlockTypeImpressions(ReadableMap impressions); } - interface ConnectionStatusCallback { - void onRequestConnectionStatus(boolean isConnected); - } - // Ref: https://github.com/facebook/react-native/blob/HEAD/Libraries/polyfills/console.js#L376 enum LogLevel { TRACE(0), @@ -187,6 +183,4 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void toggleUndoButton(boolean isDisabled); void toggleRedoButton(boolean isDisabled); - - void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback); } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 0073db769d9cd5..d922d863cb3011 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -23,7 +23,6 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; -import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ConnectionStatusCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaType; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.OtherMediaOptionsReceivedCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FocalPointPickerTooltipShownCallback; @@ -86,8 +85,6 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; - public static final String MAP_KEY_IS_CONNECTED = "isConnected"; - private boolean mIsDarkMode; public RNReactNativeGutenbergBridgeModule(ReactApplicationContext reactContext, @@ -536,18 +533,4 @@ public void generateHapticFeedback() { } } } - - @ReactMethod - public void requestConnectionStatus(final Callback jsCallback) { - ConnectionStatusCallback connectionStatusCallback = requestConnectionStatusCallback(jsCallback); - mGutenbergBridgeJS2Parent.requestConnectionStatus(connectionStatusCallback); - } - - private ConnectionStatusCallback requestConnectionStatusCallback(final Callback jsCallback) { - return new GutenbergBridgeJS2Parent.ConnectionStatusCallback() { - @Override public void onRequestConnectionStatus(boolean isConnected) { - jsCallback.invoke(isConnected); - } - }; - } } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index fe83bc8a14b540..7dd4dbf3811feb 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -15,7 +15,6 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; -import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_IS_CONNECTED; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL; @@ -45,8 +44,6 @@ public interface JSEventEmitter { private static final String EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED = "featuredImageIdNativeUpdated"; - private static final String EVENT_CONNECTION_STATUS_CHANGE = "connectionStatusChange"; - private static final String MAP_KEY_MEDIA_FILE_STATE = "state"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS = "progress"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_SERVER_ID = "mediaServerId"; @@ -225,12 +222,6 @@ public void sendToJSFeaturedImageId(int mediaId) { queueActionToJS(EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED, writableMap); } - public void onConnectionStatusChange(boolean isConnected) { - WritableMap writableMap = new WritableNativeMap(); - writableMap.putBoolean(MAP_KEY_IS_CONNECTED, isConnected); - queueActionToJS(EVENT_CONNECTION_STATUS_CHANGE, writableMap); - } - @Override public void onReplaceMediaFilesEditedBlock(String mediaFiles, String blockId) { WritableMap writableMap = new WritableNativeMap(); writableMap.putString(MAP_KEY_REPLACE_BLOCK_HTML, mediaFiles); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index c0916d1417a34f..69adb653211da2 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -112,7 +112,6 @@ public class WPAndroidGlueCode { private OnToggleUndoButtonListener mOnToggleUndoButtonListener; private OnToggleRedoButtonListener mOnToggleRedoButtonListener; - private OnConnectionStatusEventListener mOnConnectionStatusEventListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -260,10 +259,6 @@ public interface OnToggleRedoButtonListener { void onToggleRedoButton(boolean isDisabled); } - public interface OnConnectionStatusEventListener { - boolean onRequestConnectionStatus(); - } - public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -599,12 +594,6 @@ public void toggleUndoButton(boolean isDisabled) { public void toggleRedoButton(boolean isDisabled) { mOnToggleRedoButtonListener.onToggleRedoButton(isDisabled); } - - @Override - public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { - boolean isConnected = mOnConnectionStatusEventListener.onRequestConnectionStatus(); - connectionStatusCallback.onRequestConnectionStatus(isConnected); - } }, mIsDarkMode); return Arrays.asList( @@ -699,7 +688,6 @@ public void attachToContainer(ViewGroup viewGroup, OnSendEventToHostListener onSendEventToHostListener, OnToggleUndoButtonListener onToggleUndoButtonListener, OnToggleRedoButtonListener onToggleRedoButtonListener, - OnConnectionStatusEventListener onConnectionStatusEventListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -725,7 +713,6 @@ public void attachToContainer(ViewGroup viewGroup, mOnSendEventToHostListener = onSendEventToHostListener; mOnToggleUndoButtonListener = onToggleUndoButtonListener; mOnToggleRedoButtonListener = onToggleRedoButtonListener; - mOnConnectionStatusEventListener = onConnectionStatusEventListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -1162,10 +1149,6 @@ public void sendToJSFeaturedImageId(int mediaId) { mDeferredEventEmitter.sendToJSFeaturedImageId(mediaId); } - public void connectionStatusChange(boolean isConnected) { - mDeferredEventEmitter.onConnectionStatusChange(isConnected); - } - public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 8e9065cc568e56..89f9f029901f9a 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -3,11 +3,6 @@ */ import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; -/** - * WordPress dependencies - */ -import { useEffect, useState } from '@wordpress/element'; - const { RNReactNativeGutenbergBridge } = NativeModules; const isIOS = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; @@ -190,49 +185,6 @@ export function subscribeOnRedoPressed( callback ) { return gutenbergBridgeEvents.addListener( 'onRedoPressed', callback ); } -export function useIsConnected() { - const [ isConnected, setIsConnected ] = useState( null ); - - useEffect( () => { - let isCurrent = true; - - RNReactNativeGutenbergBridge.requestConnectionStatus( - ( isBridgeConnected ) => { - if ( ! isCurrent ) { - return; - } - - setIsConnected( isBridgeConnected ); - } - ); - - return () => { - isCurrent = false; - }; - }, [] ); - - useEffect( () => { - const subscription = subscribeConnectionStatus( - ( { isConnected: isBridgeConnected } ) => { - setIsConnected( isBridgeConnected ); - } - ); - - return () => { - subscription.remove(); - }; - }, [] ); - - return { isConnected }; -} - -function subscribeConnectionStatus( callback ) { - return gutenbergBridgeEvents.addListener( - 'connectionStatusChange', - callback - ); -} - /** * Request media picker for the given media source. * diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index de0d1b513f00dc..4175c1e2343c32 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -210,11 +210,6 @@ public class Gutenberg: UIResponder { bridgeModule.sendEventIfNeeded(.onRedoPressed, body: nil) } - public func connectionStatusChange(isConnected: Bool) { - var data: [String: Any] = ["isConnected": isConnected] - bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) - } - private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() settingsUpdates["isFSETheme"] = editorSettings?.isFSETheme ?? false diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 8890cd4de0f7ec..83d087bccab9d1 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -283,8 +283,6 @@ public protocol GutenbergBridgeDelegate: AnyObject { func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) - - func gutenbergDidRequestConnectionStatus() -> Bool } // MARK: - Optional GutenbergBridgeDelegate methods diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index 3d68e51ebcacb5..d333f8c1722ad9 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -42,6 +42,5 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(generateHapticFeedback) RCT_EXTERN_METHOD(toggleUndoButton:(BOOL)isDisabled) RCT_EXTERN_METHOD(toggleRedoButton:(BOOL)isDisabled) -RCT_EXTERN_METHOD(requestConnectionStatus:(RCTResponseSenderBlock)callback) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index ec763b2b8aaa2c..8cf4f685bd22c4 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -421,11 +421,6 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { func toggleRedoButton(_ isDisabled: Bool) { self.delegate?.gutenbergDidRequestToggleRedoButton(isDisabled) } - - @objc - func requestConnectionStatus(_ callback: @escaping RCTResponseSenderBlock) { - callback([self.delegate?.gutenbergDidRequestConnectionStatus() ?? true]) - } } // MARK: - RCTBridgeModule delegate @@ -455,7 +450,6 @@ extension RNReactNativeGutenbergBridge { case showEditorHelp case onUndoPressed case onRedoPressed - case connectionStatusChange } public override func supportedEvents() -> [String]! { diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index 4477f1cc1d9f35..d718b34f25db3b 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -308,11 +308,6 @@ public void toggleRedoButton(boolean isDisabled) { mainActivity.updateRedoItem(isDisabled); } } - - @Override - public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { - connectionStatusCallback.onRequestConnectionStatus(true); - } }, isDarkMode()); return new DefaultReactNativeHost(this) { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index ef95c7e65862f6..b269d1feb8ddfe 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -345,10 +345,6 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } } } - - func gutenbergDidRequestConnectionStatus() -> Bool { - return true - } } extension GutenbergViewController: GutenbergWebDelegate { From e41e973a049c66f63bc2f631949145a24ddebe26 Mon Sep 17 00:00:00 2001 From: Derek Blank <derekpblank@gmail.com> Date: Thu, 7 Dec 2023 08:10:39 +1000 Subject: [PATCH 062/325] Move mobile ImageLinkDestinationsScreen from components package to block-editor package (#56775) * Move mobile ImageLinkDestinationsScreen from components package to block-editor package * Update react-native-editor CHANGELOG * Update ImageLinkDestinationsScreen imports --- .../block-settings/container.native.js | 8 +++----- .../image-link-destinations/index.native.js} | 8 ++++---- .../image-link-destinations/style.native.scss | 16 ++++++++++++++++ packages/components/src/index.native.js | 1 - .../src/mobile/link-settings/style.native.scss | 17 ----------------- packages/react-native-editor/CHANGELOG.md | 1 + 6 files changed, 24 insertions(+), 27 deletions(-) rename packages/{components/src/mobile/link-settings/image-link-destinations-screen.native.js => block-editor/src/components/image-link-destinations/index.native.js} (95%) create mode 100644 packages/block-editor/src/components/image-link-destinations/style.native.scss diff --git a/packages/block-editor/src/components/block-settings/container.native.js b/packages/block-editor/src/components/block-settings/container.native.js index 0947ac61ca6cf1..d9d9cda9eadd96 100644 --- a/packages/block-editor/src/components/block-settings/container.native.js +++ b/packages/block-editor/src/components/block-settings/container.native.js @@ -1,15 +1,10 @@ /** * WordPress dependencies */ -import { - InspectorControls, - useMultipleOriginColorsAndGradients, -} from '@wordpress/block-editor'; import { BottomSheet, ColorSettings, FocalPointSettingsPanel, - ImageLinkDestinationsScreen, LinkPickerScreen, } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -18,6 +13,9 @@ import { useDispatch, useSelect } from '@wordpress/data'; * Internal dependencies */ import styles from './container.native.scss'; +import InspectorControls from '../inspector-controls'; +import ImageLinkDestinationsScreen from '../image-link-destinations'; +import useMultipleOriginColorsAndGradients from '../colors-gradients/use-multiple-origin-colors-and-gradients'; export const blockSettingsScreens = { settings: 'Settings', diff --git a/packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js b/packages/block-editor/src/components/image-link-destinations/index.native.js similarity index 95% rename from packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js rename to packages/block-editor/src/components/image-link-destinations/index.native.js index 02d29326e303e2..ed815426f4e27d 100644 --- a/packages/components/src/mobile/link-settings/image-link-destinations-screen.native.js +++ b/packages/block-editor/src/components/image-link-destinations/index.native.js @@ -9,15 +9,14 @@ import { StyleSheet } from 'react-native'; */ import { __ } from '@wordpress/i18n'; import { Icon, check, chevronRight } from '@wordpress/icons'; -import { blockSettingsScreens } from '@wordpress/block-editor'; import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { BottomSheet, PanelBody } from '@wordpress/components'; /** * Internal dependencies */ -import styles from './style.scss'; -import PanelBody from '../../panel/body'; -import BottomSheet from '../bottom-sheet'; +import styles from './style.native.scss'; +import { blockSettingsScreens } from '../block-settings'; const LINK_DESTINATION_NONE = 'none'; const LINK_DESTINATION_MEDIA = 'media'; @@ -36,6 +35,7 @@ function LinkDestination( { styles.optionIcon, styles.optionIconDark ); + return ( <BottomSheet.Cell icon={ check } diff --git a/packages/block-editor/src/components/image-link-destinations/style.native.scss b/packages/block-editor/src/components/image-link-destinations/style.native.scss new file mode 100644 index 00000000000000..cc1486efa2487d --- /dev/null +++ b/packages/block-editor/src/components/image-link-destinations/style.native.scss @@ -0,0 +1,16 @@ +// used in both light and dark modes +.placeholderTextColor { + color: #87a6bc; +} + +.optionIcon { + color: $blue-50; +} + +.optionIconDark { + color: $blue-30; +} + +.unselectedOptionIcon { + opacity: 0; +} diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 6c793499102e1c..dc8a77ad77d1e5 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -106,7 +106,6 @@ export { default as LinkPickerScreen } from './mobile/link-picker/link-picker-sc export { default as LinkSettings } from './mobile/link-settings'; export { default as LinkSettingsScreen } from './mobile/link-settings/link-settings-screen'; export { default as LinkSettingsNavigation } from './mobile/link-settings/link-settings-navigation'; -export { default as ImageLinkDestinationsScreen } from './mobile/link-settings/image-link-destinations-screen'; export { default as SegmentedControl } from './mobile/segmented-control'; export { default as Image, IMAGE_DEFAULT_FOCAL_POINT } from './mobile/image'; export { default as ImageEditingButton } from './mobile/image/image-editing-button'; diff --git a/packages/components/src/mobile/link-settings/style.native.scss b/packages/components/src/mobile/link-settings/style.native.scss index 137c2e32dfc768..b9545b80ec4ab1 100644 --- a/packages/components/src/mobile/link-settings/style.native.scss +++ b/packages/components/src/mobile/link-settings/style.native.scss @@ -2,20 +2,3 @@ padding-left: 0; padding-right: 0; } - -// used in both light and dark modes -.placeholderTextColor { - color: #87a6bc; -} - -.optionIcon { - color: $blue-50; -} - -.optionIconDark { - color: $blue-30; -} - -.unselectedOptionIcon { - opacity: 0; -} diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index ea7b841879c4fb..992d61ea9ce37b 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] [internal] Move InserterButton from components package to block-editor package [#56494] +- [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] From 86cf00a0731903b9ce6c062142adc41ebbae984a Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:30:46 +1100 Subject: [PATCH 063/325] Adding a space between translated strings (#56839) --- packages/edit-site/src/components/welcome-guide/styles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/welcome-guide/styles.js b/packages/edit-site/src/components/welcome-guide/styles.js index 03a3014b1b9fa5..f0e6f65a9e434d 100644 --- a/packages/edit-site/src/components/welcome-guide/styles.js +++ b/packages/edit-site/src/components/welcome-guide/styles.js @@ -118,7 +118,7 @@ export default function WelcomeGuideStyles() { <p className="edit-site-welcome-guide__text"> { __( 'New to block themes and styling your site?' - ) } + ) }{ ' ' } <ExternalLink href={ __( 'https://wordpress.org/documentation/article/styles-overview/' From 72280c197aa11e7388749f3a6b2a1ea684f8e715 Mon Sep 17 00:00:00 2001 From: Rich Tabor <hi@richtabor.com> Date: Wed, 6 Dec 2023 19:54:51 -0500 Subject: [PATCH 064/325] FocalPointPicker with __next40pxDefaultSize (#56021) * __next40pxDefaultSize and size adjustment * Make `__next40pxDefaultSize ` prop opt in * Update changelog * Rebase to update changelog --------- Co-authored-by: brookewp <brooke.kaminski@automattic.com> --- packages/components/CHANGELOG.md | 1 + packages/components/src/focal-point-picker/controls.tsx | 4 ++++ packages/components/src/focal-point-picker/index.tsx | 2 ++ .../focal-point-picker/styles/focal-point-picker-style.ts | 2 +- packages/components/src/focal-point-picker/types.ts | 7 +++++++ 5 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 5e851ab2b63df0..edbd02867c0103 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,6 +8,7 @@ - `QueryControls`: Add opt-in prop for 40px default size ([#56576](https://github.com/WordPress/gutenberg/pull/56576)). - `CheckboxControl`: Add option to not render label ([#56158](https://github.com/WordPress/gutenberg/pull/56158)). - `PaletteEdit`: Gradient pickers to use same width as color pickers ([#56801](https://github.com/WordPress/gutenberg/pull/56801)). +- `FocalPointPicker`: Add opt-in prop for 40px default size ([#56021](https://github.com/WordPress/gutenberg/pull/56021)). ### Bug Fix diff --git a/packages/components/src/focal-point-picker/controls.tsx b/packages/components/src/focal-point-picker/controls.tsx index f204d5736779cb..40cf8d215704b5 100644 --- a/packages/components/src/focal-point-picker/controls.tsx +++ b/packages/components/src/focal-point-picker/controls.tsx @@ -23,6 +23,7 @@ const noop = () => {}; export default function FocalPointPickerControls( { __nextHasNoMarginBottom, + __next40pxDefaultSize, hasHelpText, onChange = noop, point = { @@ -51,8 +52,10 @@ export default function FocalPointPickerControls( { className="focal-point-picker__controls" __nextHasNoMarginBottom={ __nextHasNoMarginBottom } hasHelpText={ hasHelpText } + gap={ 4 } > <FocalPointUnitControl + __next40pxDefaultSize={ __next40pxDefaultSize } label={ __( 'Left' ) } aria-label={ __( 'Focal point left position' ) } value={ [ valueX, '%' ].join( '' ) } @@ -66,6 +69,7 @@ export default function FocalPointPickerControls( { dragDirection="e" /> <FocalPointUnitControl + __next40pxDefaultSize={ __next40pxDefaultSize } label={ __( 'Top' ) } aria-label={ __( 'Focal point top position' ) } value={ [ valueY, '%' ].join( '' ) } diff --git a/packages/components/src/focal-point-picker/index.tsx b/packages/components/src/focal-point-picker/index.tsx index 65efdc322cf0c6..1b4c4d9dff9665 100644 --- a/packages/components/src/focal-point-picker/index.tsx +++ b/packages/components/src/focal-point-picker/index.tsx @@ -84,6 +84,7 @@ const GRID_OVERLAY_TIMEOUT = 600; */ export function FocalPointPicker( { __nextHasNoMarginBottom, + __next40pxDefaultSize = false, autoPlay = true, className, help, @@ -273,6 +274,7 @@ export function FocalPointPicker( { </MediaWrapper> <Controls __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __next40pxDefaultSize={ __next40pxDefaultSize } hasHelpText={ !! help } point={ { x, y } } onChange={ ( value ) => { diff --git a/packages/components/src/focal-point-picker/styles/focal-point-picker-style.ts b/packages/components/src/focal-point-picker/styles/focal-point-picker-style.ts index f405b02959f9d7..3df6d1bc6eafbe 100644 --- a/packages/components/src/focal-point-picker/styles/focal-point-picker-style.ts +++ b/packages/components/src/focal-point-picker/styles/focal-point-picker-style.ts @@ -53,7 +53,7 @@ export const MediaPlaceholder = styled.div` `; export const StyledUnitControl = styled( UnitControl )` - width: 100px; + width: 100%; `; const deprecatedBottomMargin = ( { diff --git a/packages/components/src/focal-point-picker/types.ts b/packages/components/src/focal-point-picker/types.ts index 81b683b7ce16cb..bd66ae02451a95 100644 --- a/packages/components/src/focal-point-picker/types.ts +++ b/packages/components/src/focal-point-picker/types.ts @@ -26,6 +26,12 @@ export type FocalPointPickerProps = Pick< * @default false */ __nextHasNoMarginBottom?: boolean; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next40pxDefaultSize?: boolean; /** * Autoplays HTML5 video. This only applies to video sources (`url`). * @@ -62,6 +68,7 @@ export type FocalPointPickerProps = Pick< export type FocalPointPickerControlsProps = { __nextHasNoMarginBottom?: boolean; + __next40pxDefaultSize?: boolean; /** * A bit of extra bottom margin will be added if a `help` text * needs to be rendered under it. From 550966a9d6eee2cb187e3bfcdc432ef4c14737fd Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Wed, 6 Dec 2023 19:06:23 -0800 Subject: [PATCH 065/325] `DimensionControl`: Add __next40pxDefaultSize prop (#56805) * `DimensionControl`: Add __next40pxDefaultSize prop * Update changelog --- packages/components/CHANGELOG.md | 1 + packages/components/src/dimension-control/index.tsx | 2 ++ packages/components/src/dimension-control/types.ts | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index edbd02867c0103..c4153503fd798f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ - `CheckboxControl`: Add option to not render label ([#56158](https://github.com/WordPress/gutenberg/pull/56158)). - `PaletteEdit`: Gradient pickers to use same width as color pickers ([#56801](https://github.com/WordPress/gutenberg/pull/56801)). - `FocalPointPicker`: Add opt-in prop for 40px default size ([#56021](https://github.com/WordPress/gutenberg/pull/56021)). +- `DimensionControl`: Add opt-in prop for 40px default size ([#56805](https://github.com/WordPress/gutenberg/pull/56805)). ### Bug Fix diff --git a/packages/components/src/dimension-control/index.tsx b/packages/components/src/dimension-control/index.tsx index 0fbdd62b58a00d..38ad5b2f85ccc7 100644 --- a/packages/components/src/dimension-control/index.tsx +++ b/packages/components/src/dimension-control/index.tsx @@ -42,6 +42,7 @@ import type { SelectControlSingleSelectionProps } from '../select-control/types' */ export function DimensionControl( props: DimensionControlProps ) { const { + __next40pxDefaultSize = false, label, value, sizes = sizesTable, @@ -85,6 +86,7 @@ export function DimensionControl( props: DimensionControlProps ) { return ( <SelectControl + __next40pxDefaultSize={ __next40pxDefaultSize } className={ classnames( className, 'block-editor-dimension-control' diff --git a/packages/components/src/dimension-control/types.ts b/packages/components/src/dimension-control/types.ts index 534b80053db968..671454f18c8a9b 100644 --- a/packages/components/src/dimension-control/types.ts +++ b/packages/components/src/dimension-control/types.ts @@ -45,4 +45,10 @@ export type DimensionControlProps = { * @default '' */ className?: string; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next40pxDefaultSize?: boolean; }; From cb3bb3d37c02c431a2e89a26a8f14cf23fbe4806 Mon Sep 17 00:00:00 2001 From: Andrea Fercia <a.fercia@gmail.com> Date: Thu, 7 Dec 2023 09:33:59 +0100 Subject: [PATCH 066/325] Increase right padding of URL field to take the Submit button into account. (#56685) --- packages/block-editor/src/components/link-control/style.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/link-control/style.scss b/packages/block-editor/src/components/link-control/style.scss index 7b6bbff0700a37..4821359cab8fc0 100644 --- a/packages/block-editor/src/components/link-control/style.scss +++ b/packages/block-editor/src/components/link-control/style.scss @@ -74,7 +74,7 @@ $preview-image-height: 140px; border-radius: $radius-block-ui; height: $button-size-next-default-40px; // components do not properly support unstable-large yet. margin: 0; - padding: $grid-unit-10 $grid-unit-20; + padding: $grid-unit-10 $button-size-next-default-40px $grid-unit-10 $grid-unit-20; position: relative; width: 100%; } From a66cf2ae7525fc72713a7e389e2155f465705279 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 7 Dec 2023 09:50:53 +0100 Subject: [PATCH 067/325] Editor: Use the same PostTemplatePanel between post and site editors (#56817) --- packages/base-styles/_z-index.scss | 5 +- .../edit-post/src/components/header/index.js | 2 +- .../components/header/mode-switcher/index.js | 2 +- .../edit-post/src/components/layout/index.js | 8 +- .../components/sidebar/post-status/index.js | 4 +- .../components/sidebar/post-template/form.js | 141 ------------ .../components/sidebar/post-template/index.js | 120 ---------- .../sidebar/post-template/style.scss | 22 -- .../sidebar/settings-header/index.js | 2 +- .../sidebar/settings-sidebar/index.js | 3 +- .../components/sidebar/template/style.scss | 35 --- .../src/components/visual-editor/index.js | 28 ++- .../src/components/welcome-guide/index.js | 2 +- .../plugins/welcome-guide-menu-item/index.js | 3 +- packages/edit-post/src/store/actions.js | 41 +--- packages/edit-post/src/style.scss | 1 - .../back-to-page-notification.js | 58 ----- .../page-content-focus-notifications/index.js | 8 +- .../page-panels/edit-template.js | 108 --------- .../page-panels/page-summary.js | 4 +- .../sidebar-edit-mode/page-panels/style.scss | 43 +--- packages/editor/src/components/index.js | 3 +- .../components/post-template/block-theme.js | 109 +++++++++ .../components/post-template/classic-theme.js | 213 ++++++++++++++++++ .../create-new-template-modal.js} | 19 +- .../post-template/create-new-template.js | 50 ++++ .../src/components/post-template}/hooks.js | 40 ++-- .../src/components/post-template/index.js | 64 ------ .../src/components/post-template/panel.js | 67 ++++++ .../post-template}/reset-default-template.js | 32 ++- .../src/components/post-template/style.scss | 52 +++++ .../post-template}/swap-template-button.js | 4 +- packages/editor/src/store/private-actions.js | 48 ++++ packages/editor/src/style.scss | 1 + .../various/post-editor-template-mode.spec.js | 11 +- test/e2e/specs/site-editor/pages.spec.js | 4 +- 36 files changed, 641 insertions(+), 716 deletions(-) delete mode 100644 packages/edit-post/src/components/sidebar/post-template/form.js delete mode 100644 packages/edit-post/src/components/sidebar/post-template/index.js delete mode 100644 packages/edit-post/src/components/sidebar/post-template/style.scss delete mode 100644 packages/edit-post/src/components/sidebar/template/style.scss delete mode 100644 packages/edit-site/src/components/page-content-focus-notifications/back-to-page-notification.js delete mode 100644 packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js create mode 100644 packages/editor/src/components/post-template/block-theme.js create mode 100644 packages/editor/src/components/post-template/classic-theme.js rename packages/{edit-post/src/components/sidebar/post-template/create-modal.js => editor/src/components/post-template/create-new-template-modal.js} (84%) create mode 100644 packages/editor/src/components/post-template/create-new-template.js rename packages/{edit-site/src/components/sidebar-edit-mode/page-panels => editor/src/components/post-template}/hooks.js (78%) delete mode 100644 packages/editor/src/components/post-template/index.js create mode 100644 packages/editor/src/components/post-template/panel.js rename packages/{edit-site/src/components/sidebar-edit-mode/page-panels => editor/src/components/post-template}/reset-default-template.js (69%) create mode 100644 packages/editor/src/components/post-template/style.scss rename packages/{edit-site/src/components/sidebar-edit-mode/page-panels => editor/src/components/post-template}/swap-template-button.js (94%) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 536d07ec4da34b..7d1fd0796f109b 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -128,7 +128,7 @@ $z-layers: ( ".block-editor-block-rename-modal": 1000001, ".edit-site-list__rename-modal": 1000001, ".dataviews-action-modal": 1000001, - ".edit-site-swap-template-modal": 1000001, + ".editor-post-template__swap-template-modal": 1000001, ".edit-site-template-panel__replace-template-modal": 1000001, // Note: The ConfirmDialog component's z-index is being set to 1000001 in packages/components/src/confirm-dialog/styles.ts @@ -156,9 +156,6 @@ $z-layers: ( // Show tooltips above NUX tips, wp-admin menus, submenus, and sidebar: ".components-tooltip": 1000002, - // Keep template popover underneath 'Create custom template' modal overlay. - ".edit-post-post-template__dialog": 99999, - // Make sure corner handles are above side handles for ResizableBox component ".components-resizable-box__handle": 2, ".components-resizable-box__side-handle": 2, diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 0b9a77206f2a61..8bfeb2d253ce11 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -78,7 +78,7 @@ function Header( { select( blockEditorStore ).getBlockSelectionStart(), hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), isEditingTemplate: - select( editorStore ).getRenderingMode() !== 'post-only', + select( editorStore ).getRenderingMode() === 'template-only', isPublishSidebarOpened: select( editPostStore ).isPublishSidebarOpened(), hasFixedToolbar: getPreference( 'core/edit-post', 'fixedToolbar' ), diff --git a/packages/edit-post/src/components/header/mode-switcher/index.js b/packages/edit-post/src/components/header/mode-switcher/index.js index 93c4ead745ffb4..34521bba19e96e 100644 --- a/packages/edit-post/src/components/header/mode-switcher/index.js +++ b/packages/edit-post/src/components/header/mode-switcher/index.js @@ -45,7 +45,7 @@ function ModeSwitcher() { isCodeEditingEnabled: select( editorStore ).getEditorSettings().codeEditingEnabled, isEditingTemplate: - select( editorStore ).getRenderingMode() !== 'post-only', + select( editorStore ).getRenderingMode() === 'template-only', mode: select( editPostStore ).getEditorMode(), } ), [] diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index bbf33613cb4241..6aa4e3dd960070 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -157,7 +157,7 @@ function Layout() { showIconLabels, isDistractionFree, showBlockBreadcrumbs, - isTemplateMode, + showMetaBoxes, documentLabel, } = useSelect( ( select ) => { const { getEditorSettings, getPostTypeLabel } = select( editorStore ); @@ -165,8 +165,8 @@ function Layout() { const postTypeLabel = getPostTypeLabel(); return { - isTemplateMode: - select( editorStore ).getRenderingMode() !== 'post-only', + showMetaBoxes: + select( editorStore ).getRenderingMode() === 'post-only', hasFixedToolbar: select( editPostStore ).isFeatureActive( 'fixedToolbar' ), sidebarIsOpened: !! ( @@ -349,7 +349,7 @@ function Layout() { { isRichEditingEnabled && mode === 'visual' && ( <VisualEditor styles={ styles } /> ) } - { ! isDistractionFree && ! isTemplateMode && ( + { ! isDistractionFree && showMetaBoxes && ( <div className="edit-post-layout__metaboxes"> <MetaBoxes location="normal" /> <MetaBoxes location="advanced" /> diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js index 1b24de6082d160..0d14265b15f820 100644 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-status/index.js @@ -13,6 +13,7 @@ import { PostSwitchToDraftButton, PostSyncStatus, PostURLPanel, + PostTemplatePanel, } from '@wordpress/editor'; /** @@ -26,7 +27,6 @@ import PostFormat from '../post-format'; import PostPendingStatus from '../post-pending-status'; import PluginPostStatusInfo from '../plugin-post-status-info'; import { store as editPostStore } from '../../../store'; -import PostTemplate from '../post-template'; /** * Module Constants @@ -62,7 +62,7 @@ export default function PostStatus() { <> <PostVisibility /> <PostSchedulePanel /> - <PostTemplate /> + <PostTemplatePanel /> <PostURLPanel /> <PostSyncStatus /> <PostSticky /> diff --git a/packages/edit-post/src/components/sidebar/post-template/form.js b/packages/edit-post/src/components/sidebar/post-template/form.js deleted file mode 100644 index 14c1e6dee76c7d..00000000000000 --- a/packages/edit-post/src/components/sidebar/post-template/form.js +++ /dev/null @@ -1,141 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useMemo } from '@wordpress/element'; -import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; -import { __ } from '@wordpress/i18n'; -import { addTemplate } from '@wordpress/icons'; -import { Notice, SelectControl, Button } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; -import PostTemplateCreateModal from './create-modal'; - -export default function PostTemplateForm( { onClose } ) { - const { - isPostsPage, - availableTemplates, - fetchedTemplates, - selectedTemplateSlug, - canCreate, - canEdit, - } = useSelect( ( select ) => { - const { canUser, getEntityRecord, getEntityRecords } = - select( coreStore ); - const editorSettings = select( editorStore ).getEditorSettings(); - const siteSettings = canUser( 'read', 'settings' ) - ? getEntityRecord( 'root', 'site' ) - : undefined; - const _isPostsPage = - select( editorStore ).getCurrentPostId() === - siteSettings?.page_for_posts; - const canCreateTemplates = canUser( 'create', 'templates' ); - - return { - isPostsPage: _isPostsPage, - availableTemplates: editorSettings.availableTemplates, - fetchedTemplates: canCreateTemplates - ? getEntityRecords( 'postType', 'wp_template', { - post_type: select( editorStore ).getCurrentPostType(), - per_page: -1, - } ) - : undefined, - selectedTemplateSlug: - select( editorStore ).getEditedPostAttribute( 'template' ), - canCreate: - canCreateTemplates && - ! _isPostsPage && - editorSettings.supportsTemplateMode, - canEdit: - canCreateTemplates && - editorSettings.supportsTemplateMode && - !! select( editPostStore ).getEditedPostTemplate(), - }; - }, [] ); - - const options = useMemo( - () => - Object.entries( { - ...availableTemplates, - ...Object.fromEntries( - ( fetchedTemplates ?? [] ).map( ( { slug, title } ) => [ - slug, - title.rendered, - ] ) - ), - } ).map( ( [ slug, title ] ) => ( { value: slug, label: title } ) ), - [ availableTemplates, fetchedTemplates ] - ); - - const selectedOption = - options.find( ( option ) => option.value === selectedTemplateSlug ) ?? - options.find( ( option ) => ! option.value ); // The default option has '' value. - - const { editPost } = useDispatch( editorStore ); - const { __unstableSwitchToTemplateMode } = useDispatch( editPostStore ); - - const [ isCreateModalOpen, setIsCreateModalOpen ] = useState( false ); - - return ( - <div className="edit-post-post-template__form"> - <InspectorPopoverHeader - title={ __( 'Template' ) } - help={ __( - 'Templates define the way content is displayed when viewing your site.' - ) } - actions={ - canCreate - ? [ - { - icon: addTemplate, - label: __( 'Add template' ), - onClick: () => setIsCreateModalOpen( true ), - }, - ] - : [] - } - onClose={ onClose } - /> - { isPostsPage ? ( - <Notice - className="edit-post-post-template__notice" - status="warning" - isDismissible={ false } - > - { __( 'The posts page template cannot be changed.' ) } - </Notice> - ) : ( - <SelectControl - __nextHasNoMarginBottom - hideLabelFromVision - label={ __( 'Template' ) } - value={ selectedOption?.value ?? '' } - options={ options } - onChange={ ( slug ) => - editPost( { template: slug || '' } ) - } - /> - ) } - { canEdit && ( - <p> - <Button - variant="link" - onClick={ () => __unstableSwitchToTemplateMode() } - > - { __( 'Edit template' ) } - </Button> - </p> - ) } - { isCreateModalOpen && ( - <PostTemplateCreateModal - onClose={ () => setIsCreateModalOpen( false ) } - /> - ) } - </div> - ); -} diff --git a/packages/edit-post/src/components/sidebar/post-template/index.js b/packages/edit-post/src/components/sidebar/post-template/index.js deleted file mode 100644 index fa1579fa3a82a2..00000000000000 --- a/packages/edit-post/src/components/sidebar/post-template/index.js +++ /dev/null @@ -1,120 +0,0 @@ -/** - * WordPress dependencies - */ -import { useState, useMemo } from '@wordpress/element'; -import { Dropdown, Button } from '@wordpress/components'; -import { __, sprintf } from '@wordpress/i18n'; -import { useSelect } from '@wordpress/data'; -import { - store as editorStore, - privateApis as editorPrivateApis, -} from '@wordpress/editor'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import PostTemplateForm from './form'; -import { store as editPostStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; - -const { PostPanelRow } = unlock( editorPrivateApis ); - -export default function PostTemplate() { - // Use internal state instead of a ref to make sure that the component - // re-renders when the popover's anchor updates. - const [ popoverAnchor, setPopoverAnchor ] = useState( null ); - // Memoize popoverProps to avoid returning a new object every time. - const popoverProps = useMemo( - () => ( { anchor: popoverAnchor, placement: 'bottom-end' } ), - [ popoverAnchor ] - ); - - const isVisible = useSelect( ( select ) => { - const postTypeSlug = select( editorStore ).getCurrentPostType(); - const postType = select( coreStore ).getPostType( postTypeSlug ); - if ( ! postType?.viewable ) { - return false; - } - - const settings = select( editorStore ).getEditorSettings(); - const hasTemplates = - !! settings.availableTemplates && - Object.keys( settings.availableTemplates ).length > 0; - if ( hasTemplates ) { - return true; - } - - if ( ! settings.supportsTemplateMode ) { - return false; - } - - const canCreateTemplates = - select( coreStore ).canUser( 'create', 'templates' ) ?? false; - return canCreateTemplates; - }, [] ); - - if ( ! isVisible ) { - return null; - } - - return ( - <PostPanelRow label={ __( 'Template' ) } ref={ setPopoverAnchor }> - <Dropdown - popoverProps={ popoverProps } - contentClassName="edit-post-post-template__dialog" - focusOnMount - renderToggle={ ( { isOpen, onToggle } ) => ( - <PostTemplateToggle - isOpen={ isOpen } - onClick={ onToggle } - /> - ) } - renderContent={ ( { onClose } ) => ( - <PostTemplateForm onClose={ onClose } /> - ) } - /> - </PostPanelRow> - ); -} - -function PostTemplateToggle( { isOpen, onClick } ) { - const templateTitle = useSelect( ( select ) => { - const templateSlug = - select( editorStore ).getEditedPostAttribute( 'template' ); - - const { supportsTemplateMode, availableTemplates } = - select( editorStore ).getEditorSettings(); - if ( ! supportsTemplateMode && availableTemplates[ templateSlug ] ) { - return availableTemplates[ templateSlug ]; - } - const template = - select( coreStore ).canUser( 'create', 'templates' ) && - select( editPostStore ).getEditedPostTemplate(); - return ( - template?.title || - template?.slug || - availableTemplates?.[ templateSlug ] - ); - }, [] ); - - return ( - <Button - className="edit-post-post-template__toggle" - variant="tertiary" - aria-expanded={ isOpen } - aria-label={ - templateTitle - ? sprintf( - // translators: %s: Name of the currently selected template. - __( 'Select template: %s' ), - templateTitle - ) - : __( 'Select template' ) - } - onClick={ onClick } - > - { templateTitle ?? __( 'Default template' ) } - </Button> - ); -} diff --git a/packages/edit-post/src/components/sidebar/post-template/style.scss b/packages/edit-post/src/components/sidebar/post-template/style.scss deleted file mode 100644 index 91f82d4d0f9f30..00000000000000 --- a/packages/edit-post/src/components/sidebar/post-template/style.scss +++ /dev/null @@ -1,22 +0,0 @@ -.components-button.edit-post-post-template__toggle { - display: inline-block; - width: 100%; - overflow: hidden; - text-overflow: ellipsis; -} - -.edit-post-post-template__dialog { - z-index: z-index(".edit-post-post-template__dialog"); -} - -.edit-post-post-template__form { - // sidebar width - popover padding - form margin - min-width: $sidebar-width - $grid-unit-20 - $grid-unit-20; - margin: $grid-unit-10; -} - -.edit-post-post-template__create-form { - @include break-medium() { - width: $grid-unit * 40; - } -} diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index fb2bf8f486f51c..ef32450e7209fd 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -23,7 +23,7 @@ const SettingsHeader = ( { sidebarName } ) => { return { // translators: Default label for the Document sidebar tab, not selected. documentLabel: getPostTypeLabel() || _x( 'Document', 'noun' ), - isTemplateMode: getRenderingMode() !== 'post-only', + isTemplateMode: getRenderingMode() === 'template-only', }; }, [] ); diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 42ad255d79e053..e566ea400c12b1 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -65,7 +65,8 @@ const SettingsSidebar = () => { sidebarName: sidebar, keyboardShortcut: shortcut, isTemplateMode: - select( editorStore ).getRenderingMode() !== 'post-only', + select( editorStore ).getRenderingMode() === + 'template-only', }; }, [] diff --git a/packages/edit-post/src/components/sidebar/template/style.scss b/packages/edit-post/src/components/sidebar/template/style.scss deleted file mode 100644 index 38d593f44d76dc..00000000000000 --- a/packages/edit-post/src/components/sidebar/template/style.scss +++ /dev/null @@ -1,35 +0,0 @@ -.edit-post-template__modal { - .components-base-control { - @include break-medium() { - width: $grid-unit * 40; - } - } -} - -.edit-post-template__modal-actions { - margin-top: $grid-unit-15; -} - -.edit-post-template-modal__tip { - padding: $grid-unit-20 $grid-unit-30; - background: $gray-100; - border-radius: $radius-block-ui; - - @include break-medium() { - width: $grid-unit * 30; - } -} - -.edit-post-template__notice { - margin: 0 0 $grid-unit-10 0; - - .components-notice__content { - margin: 0; - } -} - -.edit-post-template__actions { - button:not(:last-child) { - margin-right: $grid-unit-10; - } -} diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 3c5ba8d0373049..655549138d3ebe 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -39,7 +39,7 @@ export default function VisualEditor( { styles } ) { const { deviceType, isWelcomeGuideVisible, - isTemplateMode, + renderingMode, isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { @@ -52,7 +52,7 @@ export default function VisualEditor( { styles } ) { return { deviceType: __experimentalGetPreviewDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), - isTemplateMode: getRenderingMode() !== 'post-only', + renderingMode: getRenderingMode(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, hasV3BlocksOnly: getBlockTypes().every( ( type ) => { return type.apiVersion >= 3; @@ -80,12 +80,13 @@ export default function VisualEditor( { styles } ) { border: '1px solid #ddd', borderBottom: 0, }; - const resizedCanvasStyles = useResizeCanvas( deviceType, isTemplateMode ); + const resizedCanvasStyles = useResizeCanvas( deviceType ); const previewMode = 'is-' + deviceType.toLowerCase() + '-preview'; - let animatedStyles = isTemplateMode - ? templateModeStyles - : desktopCanvasStyles; + let animatedStyles = + renderingMode === 'template-only' + ? templateModeStyles + : desktopCanvasStyles; if ( resizedCanvasStyles ) { animatedStyles = resizedCanvasStyles; } @@ -94,7 +95,11 @@ export default function VisualEditor( { styles } ) { // Add a constant padding for the typewritter effect. When typing at the // bottom, there needs to be room to scroll up. - if ( ! hasMetaBoxes && ! resizedCanvasStyles && ! isTemplateMode ) { + if ( + ! hasMetaBoxes && + ! resizedCanvasStyles && + renderingMode === 'post-only' + ) { paddingBottom = '40vh'; } @@ -111,13 +116,13 @@ export default function VisualEditor( { styles } ) { : '', }, ], - [ styles ] + [ styles, paddingBottom ] ); const isToBeIframed = ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && ! hasMetaBoxes ) || - isTemplateMode || + renderingMode === 'template-only' || deviceType === 'Tablet' || deviceType === 'Mobile'; @@ -125,14 +130,15 @@ export default function VisualEditor( { styles } ) { <BlockTools __unstableContentRef={ ref } className={ classnames( 'edit-post-visual-editor', { - 'is-template-mode': isTemplateMode, + 'is-template-mode': renderingMode === 'template-only', 'has-inline-canvas': ! isToBeIframed, } ) } > <motion.div className="edit-post-visual-editor__content-area" animate={ { - padding: isTemplateMode ? '48px 48px 0' : 0, + padding: + renderingMode === 'template-only' ? '48px 48px 0' : 0, } } > <motion.div diff --git a/packages/edit-post/src/components/welcome-guide/index.js b/packages/edit-post/src/components/welcome-guide/index.js index 2809d9daa88a0a..9543fde1373082 100644 --- a/packages/edit-post/src/components/welcome-guide/index.js +++ b/packages/edit-post/src/components/welcome-guide/index.js @@ -15,7 +15,7 @@ export default function WelcomeGuide() { const { isActive, isTemplateMode } = useSelect( ( select ) => { const { isFeatureActive } = select( editPostStore ); const { getRenderingMode } = select( editorStore ); - const _isTemplateMode = getRenderingMode() !== 'post-only'; + const _isTemplateMode = getRenderingMode() === 'template-only'; const feature = _isTemplateMode ? 'welcomeGuideTemplate' : 'welcomeGuide'; diff --git a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js index 394e148603eb0a..e43f7910b5ffc6 100644 --- a/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js +++ b/packages/edit-post/src/plugins/welcome-guide-menu-item/index.js @@ -8,7 +8,8 @@ import { store as editorStore } from '@wordpress/editor'; export default function WelcomeGuideMenuItem() { const isTemplateMode = useSelect( - ( select ) => select( editorStore ).getRenderingMode() !== 'post-only', + ( select ) => + select( editorStore ).getRenderingMode() === 'template-only', [] ); diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 3d93e59c38a324..2659b7ad333981 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -7,7 +7,6 @@ import { store as interfaceStore } from '@wordpress/interface'; import { store as preferencesStore } from '@wordpress/preferences'; import { speak } from '@wordpress/a11y'; import { store as noticesStore } from '@wordpress/notices'; -import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; import deprecated from '@wordpress/deprecated'; @@ -525,46 +524,24 @@ export function setIsEditingTemplate() { /** * Switches to the template mode. - * - * @param {boolean} newTemplate Is new template. */ export const __unstableSwitchToTemplateMode = - ( newTemplate = false ) => - ( { registry, select } ) => { + () => + ( { registry } ) => { registry.dispatch( editorStore ).setRenderingMode( 'template-only' ); - const isWelcomeGuideActive = select.isFeatureActive( - 'welcomeGuideTemplate' - ); - if ( ! isWelcomeGuideActive ) { - const message = newTemplate - ? __( "Custom template created. You're in template mode now." ) - : __( - 'Editing template. Changes made here affect all posts and pages that use the template.' - ); - registry.dispatch( noticesStore ).createSuccessNotice( message, { - type: 'snackbar', - } ); - } }; /** * Create a block based template. * - * @param {Object?} template Template to create and assign. + * @deprecated */ -export const __unstableCreateTemplate = - ( template ) => - async ( { registry } ) => { - const savedTemplate = await registry - .dispatch( coreStore ) - .saveEntityRecord( 'postType', 'wp_template', template ); - const post = registry.select( editorStore ).getCurrentPost(); - registry - .dispatch( coreStore ) - .editEntityRecord( 'postType', post.type, post.id, { - template: savedTemplate.slug, - } ); - }; +export function __unstableCreateTemplate() { + deprecated( "dispatch( 'core/edit-post' ).__unstableCreateTemplate", { + since: '6.5', + } ); + return { type: 'NOTHING' }; +} let metaBoxesInitialized = false; diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 474467ab2e25a4..53219bc6a37368 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -11,7 +11,6 @@ @import "./components/sidebar/last-revision/style.scss"; @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; -@import "./components/sidebar/post-template/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; @import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-summary/style.scss"; diff --git a/packages/edit-site/src/components/page-content-focus-notifications/back-to-page-notification.js b/packages/edit-site/src/components/page-content-focus-notifications/back-to-page-notification.js deleted file mode 100644 index 7cf963246bed81..00000000000000 --- a/packages/edit-site/src/components/page-content-focus-notifications/back-to-page-notification.js +++ /dev/null @@ -1,58 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef } from '@wordpress/element'; -import { store as noticesStore } from '@wordpress/notices'; -import { __ } from '@wordpress/i18n'; -import { store as editorStore } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../store'; - -/** - * Component that displays a 'You are editing a template' notification when the - * user switches from focusing on editing page content to editing a template. - */ -export default function BackToPageNotification() { - useBackToPageNotification(); - return null; -} - -/** - * Hook that displays a 'You are editing a template' notification when the user - * switches from focusing on editing page content to editing a template. - */ -export function useBackToPageNotification() { - const renderingMode = useSelect( - ( select ) => select( editorStore ).getRenderingMode(), - [] - ); - const { isPage } = useSelect( editSiteStore ); - const { setRenderingMode } = useDispatch( editorStore ); - const { createInfoNotice } = useDispatch( noticesStore ); - - const alreadySeen = useRef( false ); - - useEffect( () => { - if ( - isPage() && - ! alreadySeen.current && - renderingMode === 'template-only' - ) { - createInfoNotice( __( 'You are editing a template.' ), { - isDismissible: true, - type: 'snackbar', - actions: [ - { - label: __( 'Back to page' ), - onClick: () => setRenderingMode( 'template-locked' ), - }, - ], - } ); - alreadySeen.current = true; - } - }, [ isPage, renderingMode, createInfoNotice, setRenderingMode ] ); -} diff --git a/packages/edit-site/src/components/page-content-focus-notifications/index.js b/packages/edit-site/src/components/page-content-focus-notifications/index.js index 3f76c91eeadeec..b4d8f62436ba27 100644 --- a/packages/edit-site/src/components/page-content-focus-notifications/index.js +++ b/packages/edit-site/src/components/page-content-focus-notifications/index.js @@ -2,13 +2,7 @@ * Internal dependencies */ import EditTemplateNotification from './edit-template-notification'; -import BackToPageNotification from './back-to-page-notification'; export default function PageContentFocusNotifications( { contentRef } ) { - return ( - <> - <EditTemplateNotification contentRef={ contentRef } /> - <BackToPageNotification /> - </> - ); + return <EditTemplateNotification contentRef={ contentRef } />; } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js deleted file mode 100644 index a11d119e1cecb7..00000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/edit-template.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { decodeEntities } from '@wordpress/html-entities'; -import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { store as coreStore } from '@wordpress/core-data'; -import { check } from '@wordpress/icons'; -import { - privateApis as editorPrivateApis, - store as editorStore, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import { store as editSiteStore } from '../../../store'; -import SwapTemplateButton from './swap-template-button'; -import ResetDefaultTemplate from './reset-default-template'; -import { unlock } from '../../../lock-unlock'; - -const { PostPanelRow } = unlock( editorPrivateApis ); - -const POPOVER_PROPS = { - className: 'edit-site-page-panels-edit-template__dropdown', - placement: 'bottom-start', -}; - -export default function EditTemplate() { - const { hasResolved, template, isTemplateHidden } = useSelect( - ( select ) => { - const { getEditedPostContext, getEditedPostType, getEditedPostId } = - select( editSiteStore ); - const { getRenderingMode } = unlock( select( editorStore ) ); - const { getEditedEntityRecord, hasFinishedResolution } = - select( coreStore ); - const _context = getEditedPostContext(); - const _postType = getEditedPostType(); - const queryArgs = [ 'postType', _postType, getEditedPostId() ]; - return { - context: _context, - hasResolved: hasFinishedResolution( - 'getEditedEntityRecord', - queryArgs - ), - template: getEditedEntityRecord( ...queryArgs ), - isTemplateHidden: getRenderingMode() === 'post-only', - postType: _postType, - }; - }, - [] - ); - - const { setRenderingMode } = useDispatch( editorStore ); - - if ( ! hasResolved ) { - return null; - } - - return ( - <PostPanelRow label={ __( 'Template' ) }> - <DropdownMenu - popoverProps={ POPOVER_PROPS } - focusOnMount - toggleProps={ { - variant: 'tertiary', - className: 'edit-site-summary-field__trigger', - } } - label={ __( 'Template options' ) } - text={ decodeEntities( template.title ) } - icon={ null } - > - { ( { onClose } ) => ( - <> - <MenuGroup> - <MenuItem - onClick={ () => { - setRenderingMode( 'template-only' ); - onClose(); - } } - > - { __( 'Edit template' ) } - </MenuItem> - <SwapTemplateButton onClick={ onClose } /> - </MenuGroup> - <ResetDefaultTemplate onClick={ onClose } /> - <MenuGroup> - <MenuItem - icon={ ! isTemplateHidden ? check : undefined } - isPressed={ ! isTemplateHidden } - onClick={ () => { - setRenderingMode( - isTemplateHidden - ? 'template-locked' - : 'post-only' - ); - } } - > - { __( 'Template preview' ) } - </MenuItem> - </MenuGroup> - </> - ) } - </DropdownMenu> - </PostPanelRow> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js index 0219b568e57c50..fd10946fd989d0 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/page-summary.js @@ -6,13 +6,13 @@ import { PostAuthorPanel, PostURLPanel, PostSchedulePanel, + PostTemplatePanel, } from '@wordpress/editor'; /** * Internal dependencies */ import PageStatus from './page-status'; -import EditTemplate from './edit-template'; export default function PageSummary( { status, @@ -31,7 +31,7 @@ export default function PageSummary( { postType={ postType } /> <PostSchedulePanel /> - <EditTemplate /> + <PostTemplatePanel /> <PostURLPanel /> <PostAuthorPanel /> </VStack> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss index f3da54c244bd1b..f05a3e6fe1deb5 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/style.scss @@ -1,39 +1,9 @@ -.edit-site-swap-template-modal { - z-index: z-index(".edit-site-swap-template-modal"); -} + .edit-site-page-panels__swap-template__confirm-modal__actions { margin-top: $grid-unit-30; } -.edit-site-page-panels__swap-template__modal-content .block-editor-block-patterns-list { - column-count: 2; - column-gap: $grid-unit-30; - - // Small top padding required to avoid cutting off the visible outline when hovering items - padding-top: $border-width-focus-fallback; - - @include break-medium() { - column-count: 3; - } - - @include break-wide() { - column-count: 4; - } - - .block-editor-block-patterns-list__list-item { - break-inside: avoid-column; - } - - .block-editor-block-patterns-list__item { - // Avoid to override the BlockPatternList component - // default hover and focus styles. - &:not(:focus):not(:hover) .block-editor-block-preview__container { - box-shadow: 0 0 0 1px $gray-300; - } - } -} - .edit-site-change-status__content { .components-popover__content { min-width: 320px; @@ -69,14 +39,3 @@ overflow: hidden; text-overflow: ellipsis; } - -.edit-site-page-panels-edit-template__dropdown { - .components-popover__content { - min-width: 240px; - } - .components-button.is-pressed, - .components-button.is-pressed:hover { - background: inherit; - color: inherit; - } -} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 64cd17746520a4..7efa33dc243b5d 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -24,7 +24,8 @@ export { default as LocalAutosaveMonitor } from './local-autosave-monitor'; export { default as PageAttributesCheck } from './page-attributes/check'; export { default as PageAttributesOrder } from './page-attributes/order'; export { default as PageAttributesParent } from './page-attributes/parent'; -export { default as PageTemplate } from './post-template'; +export { default as PageTemplate } from './post-template/classic-theme'; +export { default as PostTemplatePanel } from './post-template/panel'; export { default as PostAuthor } from './post-author'; export { default as PostAuthorCheck } from './post-author/check'; export { default as PostAuthorPanel } from './post-author/panel'; diff --git a/packages/editor/src/components/post-template/block-theme.js b/packages/editor/src/components/post-template/block-theme.js new file mode 100644 index 00000000000000..a8b07cdacb554e --- /dev/null +++ b/packages/editor/src/components/post-template/block-theme.js @@ -0,0 +1,109 @@ +/** + * WordPress dependencies + */ +import { useSelect, useDispatch } from '@wordpress/data'; +import { decodeEntities } from '@wordpress/html-entities'; +import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useEntityRecord } from '@wordpress/core-data'; +import { check } from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import SwapTemplateButton from './swap-template-button'; +import ResetDefaultTemplate from './reset-default-template'; +import { unlock } from '../../lock-unlock'; +import CreateNewTemplate from './create-new-template'; + +const POPOVER_PROPS = { + className: 'editor-post-template__dropdown', + placement: 'bottom-start', +}; + +export default function BlockThemeControl( { id } ) { + const { isTemplateHidden } = useSelect( ( select ) => { + const { getRenderingMode } = unlock( select( editorStore ) ); + return { + isTemplateHidden: getRenderingMode() === 'post-only', + }; + }, [] ); + const { editedRecord: template, hasResolved } = useEntityRecord( + 'postType', + 'wp_template', + id + ); + const { getEditorSettings } = useSelect( editorStore ); + const { createSuccessNotice } = useDispatch( noticesStore ); + const { setRenderingMode } = useDispatch( editorStore ); + + if ( ! hasResolved ) { + return null; + } + + return ( + <DropdownMenu + popoverProps={ POPOVER_PROPS } + focusOnMount + toggleProps={ { + variant: 'tertiary', + } } + label={ __( 'Template options' ) } + text={ decodeEntities( template.title ) } + icon={ null } + > + { ( { onClose } ) => ( + <> + <MenuGroup> + <MenuItem + onClick={ () => { + setRenderingMode( 'template-only' ); + onClose(); + createSuccessNotice( + __( + 'Editing template. Changes made here affect all posts and pages that use the template.' + ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Go back' ), + onClick: () => + setRenderingMode( + getEditorSettings() + .defaultRenderingMode + ), + }, + ], + } + ); + } } + > + { __( 'Edit template' ) } + </MenuItem> + <SwapTemplateButton onClick={ onClose } /> + <ResetDefaultTemplate onClick={ onClose } /> + <CreateNewTemplate onClick={ onClose } /> + </MenuGroup> + <MenuGroup> + <MenuItem + icon={ ! isTemplateHidden ? check : undefined } + isPressed={ ! isTemplateHidden } + onClick={ () => { + setRenderingMode( + isTemplateHidden + ? 'template-locked' + : 'post-only' + ); + } } + > + { __( 'Template preview' ) } + </MenuItem> + </MenuGroup> + </> + ) } + </DropdownMenu> + ); +} diff --git a/packages/editor/src/components/post-template/classic-theme.js b/packages/editor/src/components/post-template/classic-theme.js new file mode 100644 index 00000000000000..2aac8f90a02189 --- /dev/null +++ b/packages/editor/src/components/post-template/classic-theme.js @@ -0,0 +1,213 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { SelectControl, Dropdown, Button, Notice } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { __experimentalInspectorPopoverHeader as InspectorPopoverHeader } from '@wordpress/block-editor'; +import { useState, useMemo } from '@wordpress/element'; +import { addTemplate } from '@wordpress/icons'; +import { store as noticesStore } from '@wordpress/notices'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import CreateNewTemplateModal from './create-new-template-modal'; +import { useAllowSwitchingTemplates } from './hooks'; + +const POPOVER_PROPS = { + className: 'editor-post-template__dropdown', + placement: 'bottom-start', +}; + +function PostTemplateToggle( { isOpen, onClick } ) { + const templateTitle = useSelect( ( select ) => { + const templateSlug = + select( editorStore ).getEditedPostAttribute( 'template' ); + + const { supportsTemplateMode, availableTemplates } = + select( editorStore ).getEditorSettings(); + if ( ! supportsTemplateMode && availableTemplates[ templateSlug ] ) { + return availableTemplates[ templateSlug ]; + } + const template = + select( coreStore ).canUser( 'create', 'templates' ) && + select( editorStore ).getCurrentTemplateId(); + return ( + template?.title || + template?.slug || + availableTemplates?.[ templateSlug ] + ); + }, [] ); + + return ( + <Button + className="edit-post-post-template__toggle" + variant="tertiary" + aria-expanded={ isOpen } + aria-label={ __( 'Template options' ) } + onClick={ onClick } + > + { templateTitle ?? __( 'Default template' ) } + </Button> + ); +} + +function PostTemplateDropdownContent( { onClose } ) { + const allowSwitchingTemplate = useAllowSwitchingTemplates(); + const { + availableTemplates, + fetchedTemplates, + selectedTemplateSlug, + canCreate, + canEdit, + } = useSelect( + ( select ) => { + const { canUser, getEntityRecords } = select( coreStore ); + const editorSettings = select( editorStore ).getEditorSettings(); + const canCreateTemplates = canUser( 'create', 'templates' ); + + return { + availableTemplates: editorSettings.availableTemplates, + fetchedTemplates: canCreateTemplates + ? getEntityRecords( 'postType', 'wp_template', { + post_type: + select( editorStore ).getCurrentPostType(), + per_page: -1, + } ) + : undefined, + selectedTemplateSlug: + select( editorStore ).getEditedPostAttribute( 'template' ), + canCreate: + allowSwitchingTemplate && + canCreateTemplates && + editorSettings.supportsTemplateMode, + canEdit: + allowSwitchingTemplate && + canCreateTemplates && + editorSettings.supportsTemplateMode && + !! select( editorStore ).getCurrentTemplateId(), + }; + }, + [ allowSwitchingTemplate ] + ); + + const options = useMemo( + () => + Object.entries( { + ...availableTemplates, + ...Object.fromEntries( + ( fetchedTemplates ?? [] ).map( ( { slug, title } ) => [ + slug, + title.rendered, + ] ) + ), + } ).map( ( [ slug, title ] ) => ( { value: slug, label: title } ) ), + [ availableTemplates, fetchedTemplates ] + ); + + const selectedOption = + options.find( ( option ) => option.value === selectedTemplateSlug ) ?? + options.find( ( option ) => ! option.value ); // The default option has '' value. + + const { editPost } = useDispatch( editorStore ); + const { getEditorSettings } = useSelect( editorStore ); + const { createSuccessNotice } = useDispatch( noticesStore ); + const { setRenderingMode } = useDispatch( editorStore ); + const [ isCreateModalOpen, setIsCreateModalOpen ] = useState( false ); + + return ( + <div className="editor-post-template__classic-theme-dropdown"> + <InspectorPopoverHeader + title={ __( 'Template' ) } + help={ __( + 'Templates define the way content is displayed when viewing your site.' + ) } + actions={ + canCreate + ? [ + { + icon: addTemplate, + label: __( 'Add template' ), + onClick: () => setIsCreateModalOpen( true ), + }, + ] + : [] + } + onClose={ onClose } + /> + { ! allowSwitchingTemplate ? ( + <Notice status="warning" isDismissible={ false }> + { __( 'The posts page template cannot be changed.' ) } + </Notice> + ) : ( + <SelectControl + __next40pxDefaultSize + __nextHasNoMarginBottom + hideLabelFromVision + label={ __( 'Template' ) } + value={ selectedOption?.value ?? '' } + options={ options } + onChange={ ( slug ) => + editPost( { template: slug || '' } ) + } + /> + ) } + { canEdit && ( + <p> + <Button + variant="link" + onClick={ () => { + setRenderingMode( 'template-only' ); + onClose(); + createSuccessNotice( + __( + 'Editing template. Changes made here affect all posts and pages that use the template.' + ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Go back' ), + onClick: () => + setRenderingMode( + getEditorSettings() + .defaultRenderingMode + ), + }, + ], + } + ); + } } + > + { __( 'Edit template' ) } + </Button> + </p> + ) } + { isCreateModalOpen && ( + <CreateNewTemplateModal + onClose={ () => setIsCreateModalOpen( false ) } + /> + ) } + </div> + ); +} + +function ClassicThemeControl() { + return ( + <Dropdown + popoverProps={ POPOVER_PROPS } + focusOnMount + renderToggle={ ( { isOpen, onToggle } ) => ( + <PostTemplateToggle isOpen={ isOpen } onClick={ onToggle } /> + ) } + renderContent={ ( { onClose } ) => ( + <PostTemplateDropdownContent onClose={ onClose } /> + ) } + /> + ); +} + +export default ClassicThemeControl; diff --git a/packages/edit-post/src/components/sidebar/post-template/create-modal.js b/packages/editor/src/components/post-template/create-new-template-modal.js similarity index 84% rename from packages/edit-post/src/components/sidebar/post-template/create-modal.js rename to packages/editor/src/components/post-template/create-new-template-modal.js index a25cfa5c25cb30..61b1b165aca272 100644 --- a/packages/edit-post/src/components/sidebar/post-template/create-modal.js +++ b/packages/editor/src/components/post-template/create-new-template-modal.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; import { useState } from '@wordpress/element'; import { serialize, createBlock } from '@wordpress/blocks'; import { @@ -18,19 +17,21 @@ import { cleanForSlug } from '@wordpress/url'; /** * Internal dependencies */ -import { store as editPostStore } from '../../../store'; +import { unlock } from '../../lock-unlock'; +import { store as editorStore } from '../../store'; const DEFAULT_TITLE = __( 'Custom Template' ); -export default function PostTemplateCreateModal( { onClose } ) { +export default function CreateNewTemplateModal( { onClose } ) { const defaultBlockTemplate = useSelect( ( select ) => select( editorStore ).getEditorSettings().defaultBlockTemplate, [] ); - const { __unstableCreateTemplate, __unstableSwitchToTemplateMode } = - useDispatch( editPostStore ); + const { createTemplate, setRenderingMode } = unlock( + useDispatch( editorStore ) + ); const [ title, setTitle ] = useState( '' ); @@ -85,7 +86,7 @@ export default function PostTemplateCreateModal( { onClose } ) { ), ] ); - await __unstableCreateTemplate( { + await createTemplate( { slug: cleanForSlug( title || DEFAULT_TITLE ), content: newTemplateContent, title: title || DEFAULT_TITLE, @@ -93,18 +94,16 @@ export default function PostTemplateCreateModal( { onClose } ) { setIsBusy( false ); cancel(); - - __unstableSwitchToTemplateMode( true ); + setRenderingMode( 'template-only' ); }; return ( <Modal title={ __( 'Create custom template' ) } onRequestClose={ cancel } - className="edit-post-post-template__create-modal" > <form - className="edit-post-post-template__create-form" + className="editor-post-template__create-form" onSubmit={ submit } > <VStack spacing="3"> diff --git a/packages/editor/src/components/post-template/create-new-template.js b/packages/editor/src/components/post-template/create-new-template.js new file mode 100644 index 00000000000000..04a7ab8febdff5 --- /dev/null +++ b/packages/editor/src/components/post-template/create-new-template.js @@ -0,0 +1,50 @@ +/** + * WordPress dependencies + */ +import { MenuItem } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; +import { useState } from '@wordpress/element'; + +/** + * Internal dependencies + */ +import CreateNewTemplateModal from './create-new-template-modal'; +import { useAllowSwitchingTemplates } from './hooks'; + +export default function CreateNewTemplate( { onClick } ) { + const { canCreateTemplates } = useSelect( ( select ) => { + const { canUser } = select( coreStore ); + return { + canCreateTemplates: canUser( 'create', 'templates' ), + }; + }, [] ); + const [ isCreateModalOpen, setIsCreateModalOpen ] = useState( false ); + const allowSwitchingTemplate = useAllowSwitchingTemplates(); + + // The default template in a post is indicated by an empty string. + if ( ! canCreateTemplates || ! allowSwitchingTemplate ) { + return null; + } + return ( + <> + <MenuItem + onClick={ () => { + setIsCreateModalOpen( true ); + } } + > + { __( 'Create new template' ) } + </MenuItem> + + { isCreateModalOpen && ( + <CreateNewTemplateModal + onClose={ () => { + setIsCreateModalOpen( false ); + onClick(); + } } + /> + ) } + </> + ); +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js b/packages/editor/src/components/post-template/hooks.js similarity index 78% rename from packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js rename to packages/editor/src/components/post-template/hooks.js index 1071479b2f9f22..e676bf66cf2fbd 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/hooks.js +++ b/packages/editor/src/components/post-template/hooks.js @@ -8,50 +8,46 @@ import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../../store'; -import { TEMPLATE_POST_TYPE } from '../../../utils/constants'; +import { store as editorStore } from '../../store'; export function useEditedPostContext() { - return useSelect( - ( select ) => select( editSiteStore ).getEditedPostContext(), - [] - ); + return useSelect( ( select ) => { + const { getCurrentPostId, getCurrentPostType } = select( editorStore ); + return { + postId: getCurrentPostId(), + postType: getCurrentPostType(), + }; + }, [] ); } - export function useAllowSwitchingTemplates() { - const { postId } = useEditedPostContext(); + const { postType, postId } = useEditedPostContext(); return useSelect( ( select ) => { const { getEntityRecord, getEntityRecords } = select( coreStore ); const siteSettings = getEntityRecord( 'root', 'site' ); - const templates = getEntityRecords( - 'postType', - TEMPLATE_POST_TYPE, - { per_page: -1 } - ); + const templates = getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + } ); const isPostsPage = +postId === siteSettings?.page_for_posts; // If current page is set front page or posts page, we also need // to check if the current theme has a template for it. If not const isFrontPage = + postType === 'page' && +postId === siteSettings?.page_on_front && templates?.some( ( { slug } ) => slug === 'front-page' ); return ! isPostsPage && ! isFrontPage; }, - [ postId ] + [ postId, postType ] ); } function useTemplates() { return useSelect( ( select ) => - select( coreStore ).getEntityRecords( - 'postType', - TEMPLATE_POST_TYPE, - { - per_page: -1, - post_type: 'page', - } - ), + select( coreStore ).getEntityRecords( 'postType', 'wp_template', { + per_page: -1, + post_type: 'page', + } ), [] ); } diff --git a/packages/editor/src/components/post-template/index.js b/packages/editor/src/components/post-template/index.js deleted file mode 100644 index 91efcd77e8742c..00000000000000 --- a/packages/editor/src/components/post-template/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { SelectControl } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editorStore } from '../../store'; - -export function PostTemplate() { - const { availableTemplates, selectedTemplate, isViewable } = useSelect( - ( select ) => { - const { - getEditedPostAttribute, - getEditorSettings, - getCurrentPostType, - } = select( editorStore ); - const { getPostType } = select( coreStore ); - - return { - selectedTemplate: getEditedPostAttribute( 'template' ), - availableTemplates: getEditorSettings().availableTemplates, - isViewable: - getPostType( getCurrentPostType() )?.viewable ?? false, - }; - }, - [] - ); - - const { editPost } = useDispatch( editorStore ); - - if ( - ! isViewable || - ! availableTemplates || - ! Object.keys( availableTemplates ).length - ) { - return null; - } - - return ( - <SelectControl - __nextHasNoMarginBottom - label={ __( 'Template:' ) } - value={ selectedTemplate } - onChange={ ( templateSlug ) => { - editPost( { - template: templateSlug || '', - } ); - } } - options={ Object.entries( availableTemplates ?? {} ).map( - ( [ templateSlug, templateName ] ) => ( { - value: templateSlug, - label: templateName, - } ) - ) } - /> - ); -} - -export default PostTemplate; diff --git a/packages/editor/src/components/post-template/panel.js b/packages/editor/src/components/post-template/panel.js new file mode 100644 index 00000000000000..8fcaeec8f3a3b2 --- /dev/null +++ b/packages/editor/src/components/post-template/panel.js @@ -0,0 +1,67 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { __ } from '@wordpress/i18n'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import ClassicThemeControl from './classic-theme'; +import BlockThemeControl from './block-theme'; +import PostPanelRow from '../post-panel-row'; + +export default function PostTemplatePanel() { + const { templateId, isBlockTheme } = useSelect( ( select ) => { + const { getCurrentTemplateId, getEditorSettings } = + select( editorStore ); + return { + templateId: getCurrentTemplateId(), + isBlockTheme: getEditorSettings().__unstableIsBlockBasedTheme, + }; + }, [] ); + + const isVisible = true; + useSelect( ( select ) => { + const postTypeSlug = select( editorStore ).getCurrentPostType(); + const postType = select( coreStore ).getPostType( postTypeSlug ); + if ( ! postType?.viewable ) { + return false; + } + + const settings = select( editorStore ).getEditorSettings(); + const hasTemplates = + !! settings.availableTemplates && + Object.keys( settings.availableTemplates ).length > 0; + if ( hasTemplates ) { + return true; + } + + if ( ! settings.supportsTemplateMode ) { + return false; + } + + const canCreateTemplates = + select( coreStore ).canUser( 'create', 'templates' ) ?? false; + return canCreateTemplates; + }, [] ); + + if ( ! isBlockTheme && isVisible ) { + return ( + <PostPanelRow label={ __( 'Template' ) }> + <ClassicThemeControl /> + </PostPanelRow> + ); + } + + if ( isBlockTheme && !! templateId ) { + return ( + <PostPanelRow label={ __( 'Template' ) }> + <BlockThemeControl id={ templateId } /> + </PostPanelRow> + ); + } + return null; +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js b/packages/editor/src/components/post-template/reset-default-template.js similarity index 69% rename from packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js rename to packages/editor/src/components/post-template/reset-default-template.js index 795477cc8fc7c6..c730f6b06dee89 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/reset-default-template.js +++ b/packages/editor/src/components/post-template/reset-default-template.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { MenuGroup, MenuItem } from '@wordpress/components'; +import { MenuItem } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; @@ -25,21 +25,19 @@ export default function ResetDefaultTemplate( { onClick } ) { return null; } return ( - <MenuGroup> - <MenuItem - onClick={ async () => { - editEntityRecord( - 'postType', - postType, - postId, - { template: '' }, - { undoIgnore: true } - ); - onClick(); - } } - > - { __( 'Use default template' ) } - </MenuItem> - </MenuGroup> + <MenuItem + onClick={ () => { + editEntityRecord( + 'postType', + postType, + postId, + { template: '' }, + { undoIgnore: true } + ); + onClick(); + } } + > + { __( 'Use default template' ) } + </MenuItem> ); } diff --git a/packages/editor/src/components/post-template/style.scss b/packages/editor/src/components/post-template/style.scss new file mode 100644 index 00000000000000..c969654a532658 --- /dev/null +++ b/packages/editor/src/components/post-template/style.scss @@ -0,0 +1,52 @@ +.editor-post-template__swap-template-modal { + z-index: z-index(".editor-post-template__swap-template-modal"); +} + +.editor-post-template__swap-template-modal-content .block-editor-block-patterns-list { + column-count: 2; + column-gap: $grid-unit-30; + + // Small top padding required to avoid cutting off the visible outline when hovering items + padding-top: $border-width-focus-fallback; + + @include break-medium() { + column-count: 3; + } + + @include break-wide() { + column-count: 4; + } + + .block-editor-block-patterns-list__list-item { + break-inside: avoid-column; + } + + .block-editor-block-patterns-list__item { + // Avoid to override the BlockPatternList component + // default hover and focus styles. + &:not(:focus):not(:hover) .block-editor-block-preview__container { + box-shadow: 0 0 0 1px $gray-300; + } + } +} + +.editor-post-template__dropdown { + .components-popover__content { + min-width: 240px; + } + .components-button.is-pressed, + .components-button.is-pressed:hover { + background: inherit; + color: inherit; + } +} + +.editor-post-template__create-form { + @include break-medium() { + width: $grid-unit * 40; + } +} + +.editor-post-template__classic-theme-dropdown { + padding: $grid-unit-10; +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js b/packages/editor/src/components/post-template/swap-template-button.js similarity index 94% rename from packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js rename to packages/editor/src/components/post-template/swap-template-button.js index 40eb1c5c4bd627..240dee42214d56 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/swap-template-button.js +++ b/packages/editor/src/components/post-template/swap-template-button.js @@ -47,10 +47,10 @@ export default function SwapTemplateButton( { onClick } ) { <Modal title={ __( 'Choose a template' ) } onRequestClose={ onClose } - overlayClassName="edit-site-swap-template-modal" + overlayClassName="editor-post-template__swap-template-modal" isFullScreen > - <div className="edit-site-page-panels__swap-template__modal-content"> + <div className="editor-post-template__swap-template-modal-content"> <TemplatesList onSelect={ onTemplateSelect } /> </div> </Modal> diff --git a/packages/editor/src/store/private-actions.js b/packages/editor/src/store/private-actions.js index 1af9ff5f6adb92..7ddeab5f35734c 100644 --- a/packages/editor/src/store/private-actions.js +++ b/packages/editor/src/store/private-actions.js @@ -1,3 +1,10 @@ +/** + * WordPress dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { __ } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; + /** * Returns an action object used to set which template is currently being used/edited. * @@ -11,3 +18,44 @@ export function setCurrentTemplateId( id ) { id, }; } + +/** + * Create a block based template. + * + * @param {Object?} template Template to create and assign. + */ +export const createTemplate = + ( template ) => + async ( { select, dispatch, registry } ) => { + const savedTemplate = await registry + .dispatch( coreStore ) + .saveEntityRecord( 'postType', 'wp_template', template ); + registry + .dispatch( coreStore ) + .editEntityRecord( + 'postType', + select.getCurrentPostType(), + select.getCurrentPostId(), + { + template: savedTemplate.slug, + } + ); + registry + .dispatch( noticesStore ) + .createSuccessNotice( + __( "Custom template created. You're in template mode now." ), + { + type: 'snackbar', + actions: [ + { + label: __( 'Go back' ), + onClick: () => + dispatch.setRenderingMode( + select.getEditorSettings() + .defaultRenderingMode + ), + }, + ], + } + ); + }; diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index ba12f58697a4a3..01e17a1a964ab0 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -17,6 +17,7 @@ @import "./components/post-schedule/style.scss"; @import "./components/post-sync-status/style.scss"; @import "./components/post-taxonomies/style.scss"; +@import "./components/post-template/style.scss"; @import "./components/post-text-editor/style.scss"; @import "./components/post-title/style.scss"; @import "./components/post-url/style.scss"; diff --git a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js index f862c32556e061..021199cc094950 100644 --- a/test/e2e/specs/editor/various/post-editor-template-mode.spec.js +++ b/test/e2e/specs/editor/various/post-editor-template-mode.spec.js @@ -131,7 +131,9 @@ class PostEditorTemplateMode { // Only match the beginning of Select template: because it contains the template name or slug afterwards. await this.editorSettingsSidebar - .locator( 'role=button[name^="Select template"i]' ) + .getByRole( 'button', { + name: 'Template options', + } ) .click(); } @@ -139,8 +141,11 @@ class PostEditorTemplateMode { await this.disableTemplateWelcomeGuide(); await this.openTemplatePopover(); - - await this.page.locator( 'role=button[name="Edit template"i]' ).click(); + await this.page + .getByRole( 'menuitem', { + name: 'Edit template', + } ) + .click(); // Check that we switched properly to edit mode. await this.page.waitForSelector( diff --git a/test/e2e/specs/site-editor/pages.spec.js b/test/e2e/specs/site-editor/pages.spec.js index 4d40223e0a99c4..6de94c3c2d6732 100644 --- a/test/e2e/specs/site-editor/pages.spec.js +++ b/test/e2e/specs/site-editor/pages.spec.js @@ -119,7 +119,7 @@ test.describe( 'Pages', () => { .getByRole( 'region', { name: 'Editor settings' } ) .getByRole( 'button', { name: 'Template options' } ) .click(); - await page.getByRole( 'button', { name: 'Edit template' } ).click(); + await page.getByRole( 'menuitem', { name: 'Edit template' } ).click(); await expect( editor.canvas.getByRole( 'document', { name: 'Block: Content', @@ -129,7 +129,7 @@ test.describe( 'Pages', () => { ); await expect( page.locator( - 'role=button[name="Dismiss this notice"i] >> text="You are editing a template."' + 'role=button[name="Dismiss this notice"i] >> text="Editing template. Changes made here affect all posts and pages that use the template."' ) ).toBeVisible(); From 40c233f929dc1668541a2fbd9eadd40d2b790bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 7 Dec 2023 10:12:08 +0100 Subject: [PATCH 068/325] DataViews: iterate on list view (#56746) Co-authored-by: James Koster <james@jameskoster.co.uk> --- package-lock.json | 2 + packages/dataviews/README.md | 5 +- packages/dataviews/package.json | 1 + packages/dataviews/src/constants.js | 20 ++-- packages/dataviews/src/dataviews.js | 12 ++- packages/dataviews/src/style.scss | 80 +++++++++++++++ packages/dataviews/src/view-grid.js | 7 +- packages/dataviews/src/view-list.js | 98 ++++++++++++++++++- packages/dataviews/src/view-table.js | 3 +- .../src/components/page-pages/index.js | 75 +++++++------- .../page-templates/dataviews-templates.js | 6 +- 11 files changed, 245 insertions(+), 64 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86ff1a4d21af92..4b27980b6b40fd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55066,6 +55066,7 @@ "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/keycodes": "file:../keycodes", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", "remove-accents": "^0.5.0" @@ -70424,6 +70425,7 @@ "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/keycodes": "file:../keycodes", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", "remove-accents": "^0.5.0" diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index 2ee1d7f42eff5b..c0d0a01cbc3e28 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -22,6 +22,7 @@ npm install @wordpress/dataviews --save fields={ fields } actions={ [ trashPostAction ] } paginationInfo={ { totalItems, totalPages } } + onSelectionChange={ ( items ) => { /* ... */ } } /> ``` @@ -75,8 +76,8 @@ Example: - `value`: the actual value selected by the user. - `hiddenFields`: the `id` of the fields that are hidden in the UI. - `layout`: config that is specific to a particular layout type. - - `mediaField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's media. - - `primaryField`: used by the `grid` layout. The `id` of the field to be used for rendering each card's title. + - `mediaField`: used by the `grid` and `list` layouts. The `id` of the field to be used for rendering each card's media. + - `primaryField`: used by the `grid` and `list` layouts. The `id` of the field to be highlighted in each card/list item. ### View <=> data diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 40a09050b94321..3d15ea435ab4f8 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -35,6 +35,7 @@ "@wordpress/element": "file:../element", "@wordpress/i18n": "file:../i18n", "@wordpress/icons": "file:../icons", + "@wordpress/keycodes": "file:../keycodes", "@wordpress/private-apis": "file:../private-apis", "classnames": "^2.3.1", "remove-accents": "^0.5.0" diff --git a/packages/dataviews/src/constants.js b/packages/dataviews/src/constants.js index c4d1d9242f08a3..387050a1dca5b6 100644 --- a/packages/dataviews/src/constants.js +++ b/packages/dataviews/src/constants.js @@ -1,8 +1,13 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; -import { blockTable, category, drawerLeft } from '@wordpress/icons'; +import { __, isRTL } from '@wordpress/i18n'; +import { + blockTable, + category, + formatListBullets, + formatListBulletsRTL, +} from '@wordpress/icons'; /** * Internal dependencies @@ -29,26 +34,17 @@ export const VIEW_LAYOUTS = [ label: __( 'Table' ), component: ViewTable, icon: blockTable, - supports: { - preview: false, - }, }, { type: LAYOUT_GRID, label: __( 'Grid' ), component: ViewGrid, icon: category, - supports: { - preview: false, - }, }, { type: LAYOUT_LIST, label: __( 'List' ), component: ViewList, - icon: drawerLeft, - supports: { - preview: true, - }, + icon: isRTL() ? formatListBulletsRTL : formatListBullets, }, ]; diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index b75155e8fddf0a..21d5b24aec345d 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -5,7 +5,7 @@ import { __experimentalVStack as VStack, __experimentalHStack as HStack, } from '@wordpress/components'; -import { useMemo } from '@wordpress/element'; +import { useMemo, useState } from '@wordpress/element'; /** * Internal dependencies @@ -28,7 +28,15 @@ export default function DataViews( { isLoading = false, paginationInfo, supportedLayouts, + onSelectionChange, } ) { + const [ selection, setSelection ] = useState( [] ); + + const onSetSelection = ( items ) => { + setSelection( items.map( ( item ) => item.id ) ); + onSelectionChange( items ); + }; + const ViewComponent = VIEW_LAYOUTS.find( ( v ) => v.type === view.type ).component; @@ -72,6 +80,8 @@ export default function DataViews( { data={ data } getItemId={ getItemId } isLoading={ isLoading } + onSelectionChange={ onSetSelection } + selection={ selection } /> <Pagination view={ view } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 5dd89f4c279707..b4f4ac0c28f956 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -127,6 +127,86 @@ } } +.dataviews-list-view { + li { + border-bottom: $border-width solid $gray-100; + margin: 0; + &:last-child { + border-bottom: 0; + } + } + + .dataviews-list-view__item { + padding: $grid-unit-15 $grid-unit-40; + cursor: default; + &:focus, + &:hover { + background-color: lighten($gray-100, 3%); + } + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + h3 { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .dataviews-list-view__item-selected, + .dataviews-list-view__item-selected:hover { + background-color: $gray-100; + + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + + .dataviews-list-view__media-wrapper { + min-width: $grid-unit-40; + height: $grid-unit-40; + border-radius: $grid-unit-05; + overflow: hidden; + position: relative; + + &::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.1); + border-radius: $grid-unit-05; + } + } + + .edit-site-page-pages__featured-image, + .dataviews-list-view__media-placeholder { + min-width: $grid-unit-40; + height: $grid-unit-40; + } + + .dataviews-list-view__media-placeholder { + background-color: $gray-200; + } + + .dataviews-list-view__fields { + color: $gray-700; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .dataviews-list-view__field { + margin-right: $grid-unit-15; + + &:last-child { + margin-right: 0; + } + } + } +} + .dataviews-action-modal { z-index: z-index(".dataviews-action-modal"); } diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 89dd7d393430d9..d28bfa055d946b 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -43,14 +43,14 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { className="dataviews-view-grid__card" > <div className="dataviews-view-grid__media"> - { mediaField?.render( { item, view } ) } + { mediaField?.render( { item } ) } </div> <HStack className="dataviews-view-grid__primary-field" justify="space-between" > <FlexBlock> - { primaryField?.render( { item, view } ) } + { primaryField?.render( { item } ) } </FlexBlock> <ItemActions item={ item } @@ -65,7 +65,6 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { { visibleFields.map( ( field ) => { const renderedValue = field.render( { item, - view, } ); if ( ! renderedValue ) { return null; @@ -80,7 +79,7 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { { field.header } </div> <div className="dataviews-view-grid__field-value"> - { field.render( { item, view } ) } + { field.render( { item } ) } </div> </VStack> ); diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index 7d781b0fb2ed69..cd9b651cabca55 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -1,9 +1,97 @@ /** - * Internal dependencies + * External dependencies */ -import ViewTable from './view-table'; +import classNames from 'classnames'; -export default function ViewList( props ) { - // To do: change to email-like preview list. - return <ViewTable { ...props } />; +/** + * WordPress dependencies + */ +import { useAsyncList } from '@wordpress/compose'; +import { + __experimentalHStack as HStack, + __experimentalVStack as VStack, +} from '@wordpress/components'; +import { ENTER, SPACE } from '@wordpress/keycodes'; + +export default function ViewList( { + view, + fields, + data, + getItemId, + onSelectionChange, + selection, +} ) { + const shownData = useAsyncList( data, { step: 3 } ); + const mediaField = fields.find( + ( field ) => field.id === view.layout.mediaField + ); + const primaryField = fields.find( + ( field ) => field.id === view.layout.primaryField + ); + const visibleFields = fields.filter( + ( field ) => + ! view.hiddenFields.includes( field.id ) && + ! [ view.layout.primaryField, view.layout.mediaField ].includes( + field.id + ) + ); + + const onEnter = ( item ) => ( event ) => { + const { keyCode } = event; + if ( [ ENTER, SPACE ].includes( keyCode ) ) { + onSelectionChange( [ item ] ); + } + }; + + return ( + <ul className="dataviews-list-view"> + { shownData.map( ( item, index ) => { + return ( + <li key={ getItemId?.( item ) || index }> + <div + role="button" + tabIndex={ 0 } + aria-pressed={ selection.includes( item.id ) } + onKeyDown={ onEnter( item ) } + className={ classNames( + 'dataviews-list-view__item', + { + 'dataviews-list-view__item-selected': + selection.includes( item.id ), + } + ) } + onClick={ () => onSelectionChange( [ item ] ) } + > + <HStack spacing={ 3 }> + <div className="dataviews-list-view__media-wrapper"> + { mediaField?.render( { item } ) || ( + <div className="dataviews-list-view__media-placeholder"></div> + ) } + </div> + <HStack> + <VStack spacing={ 1 }> + { primaryField?.render( { item } ) } + <div className="dataviews-list-view__fields"> + { visibleFields.map( ( field ) => { + return ( + <span + key={ field.id } + className="dataviews-list-view__field" + > + { field.render( { + item, + } ) } + </span> + ); + } ) } + </div> + </VStack> + </HStack> + </HStack> + </div> + </li> + ); + } ) } + </ul> + ); } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 8b6422b4be11a7..5ce9456344f695 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -344,8 +344,7 @@ function ViewTable( { const columns = useMemo( () => { const _columns = fields.map( ( field ) => { const { render, getValue, ...column } = field; - column.cell = ( props ) => - render( { item: props.row.original, view } ); + column.cell = ( props ) => render( { item: props.row.original } ); if ( getValue ) { column.accessorFn = ( item ) => getValue( { item } ); } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index bac881f2ceb218..fe6f50a4a9c070 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -12,7 +12,7 @@ import { useState, useMemo, useCallback, useEffect } from '@wordpress/element'; import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; -import { DataViews, VIEW_LAYOUTS } from '@wordpress/dataviews'; +import { DataViews } from '@wordpress/dataviews'; /** * Internal dependencies @@ -24,6 +24,7 @@ import { ENUMERATION_TYPE, LAYOUT_GRID, LAYOUT_TABLE, + LAYOUT_LIST, OPERATOR_IN, OPERATOR_NOT_IN, } from '../../utils/constants'; @@ -48,6 +49,10 @@ const defaultConfigPerViewType = { mediaField: 'featured-image', primaryField: 'title', }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'featured-image', + }, }; function useView( type ) { @@ -124,7 +129,10 @@ const DEFAULT_STATUSES = 'draft,future,pending,private,publish'; // All but 'tra export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); - const [ selection, setSelection ] = useState( [] ); + const [ pageId, setPageId ] = useState( null ); + + const onSelectionChange = ( items ) => + setPageId( items?.length === 1 ? items[ 0 ].id : null ); const queryArgs = useMemo( () => { const filters = {}; @@ -187,15 +195,15 @@ export default function PagePages() { id: 'featured-image', header: __( 'Featured Image' ), getValue: ( { item } ) => item.featured_media, - render: ( { item, view: currentView } ) => + render: ( { item } ) => !! item.featured_media ? ( <Media className="edit-site-page-pages__featured-image" id={ item.featured_media } size={ - currentView.type === 'list' - ? [ 'thumbnail', 'medium', 'large', 'full' ] - : [ 'large', 'full', 'medium', 'thumbnail' ] + view.type === LAYOUT_GRID + ? [ 'large', 'full', 'medium', 'thumbnail' ] + : [ 'thumbnail', 'medium', 'large', 'full' ] } /> ) : null, @@ -205,31 +213,29 @@ export default function PagePages() { header: __( 'Title' ), id: 'title', getValue: ( { item } ) => item.title?.rendered || item.slug, - render: ( { item, view: { type } } ) => { + render: ( { item } ) => { return ( <VStack spacing={ 1 }> - <Heading as="h3" level={ 5 }> - <Link - params={ { - postId: item.id, - postType: item.type, - canvas: 'edit', - } } - onClick={ ( event ) => { - if ( - VIEW_LAYOUTS.find( - ( v ) => v.type === type - )?.supports?.preview - ) { - event.preventDefault(); - setSelection( [ item.id ] ); - } - } } - > - { decodeEntities( + <Heading as="h3" level={ 5 } weight={ 500 }> + { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( + view.type + ) ? ( + <Link + params={ { + postId: item.id, + postType: item.type, + canvas: 'edit', + } } + > + { decodeEntities( + item.title?.rendered || item.slug + ) || __( '(no title)' ) } + </Link> + ) : ( + decodeEntities( item.title?.rendered || item.slug - ) || __( '(no title)' ) } - </Link> + ) || __( '(no title)' ) + ) } </Heading> </VStack> ); @@ -274,7 +280,7 @@ export default function PagePages() { }, }, ], - [ authors ] + [ authors, view ] ); const permanentlyDeletePostAction = usePermanentlyDeletePostAction(); @@ -324,19 +330,18 @@ export default function PagePages() { isLoading={ isLoadingPages || isLoadingAuthors } view={ view } onChangeView={ onChangeView } + onSelectionChange={ onSelectionChange } /> </Page> - { VIEW_LAYOUTS.find( ( v ) => v.type === view.type )?.supports - ?.preview && ( + { view.type === LAYOUT_LIST && ( <Page> <div className="edit-site-page-pages-preview"> - { selection.length === 1 && ( + { pageId !== null ? ( <SideEditor - postId={ selection[ 0 ] } + postId={ pageId } postType={ postType } /> - ) } - { selection.length !== 1 && ( + ) : ( <div style={ { display: 'flex', diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 15750dd85b42c9..c4014f9bd7cbc4 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -176,11 +176,11 @@ export default function DataviewsTemplates() { { header: __( 'Preview' ), id: 'preview', - render: ( { item, view: { type: viewType } } ) => { + render: ( { item } ) => { return ( <TemplatePreview content={ item.content.raw } - viewType={ viewType } + viewType={ view.type } /> ); }, @@ -229,7 +229,7 @@ export default function DataviewsTemplates() { elements: authors, }, ], - [ authors ] + [ authors, view ] ); const { shownTemplates, paginationInfo } = useMemo( () => { From 26a5d6d779b3aeaca112d72fe6cda4045ee297db Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Thu, 7 Dec 2023 12:22:46 +0200 Subject: [PATCH 069/325] DataViews: Render data async conditionally (#56851) * DataViews: Render data async conditionally * rename to `deferredRendering` --- packages/dataviews/src/dataviews.js | 2 ++ packages/dataviews/src/view-actions.js | 5 ++++- packages/dataviews/src/view-grid.js | 12 ++++++++++-- packages/dataviews/src/view-list.js | 4 +++- packages/dataviews/src/view-table.js | 6 ++++-- .../edit-site/src/components/page-pages/index.js | 1 + .../components/page-templates/dataviews-templates.js | 1 + 7 files changed, 25 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 21d5b24aec345d..13397242953b70 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -29,6 +29,7 @@ export default function DataViews( { paginationInfo, supportedLayouts, onSelectionChange, + deferredRendering, } ) { const [ selection, setSelection ] = useState( [] ); @@ -82,6 +83,7 @@ export default function DataViews( { isLoading={ isLoading } onSelectionChange={ onSetSelection } selection={ selection } + deferredRendering={ deferredRendering } /> <Pagination view={ view } diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index ff01155727e697..4d012d4e5a38ff 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -156,7 +156,10 @@ function FieldsVisibilityMenu( { view, onChangeView, fields } ) { ? view.hiddenFields.filter( ( id ) => id !== field.id ) - : [ ...view.hiddenFields, field.id ], + : [ + ...( view.hiddenFields || [] ), + field.id, + ], } ); } } > diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index d28bfa055d946b..e2c34ba6749faa 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -14,7 +14,14 @@ import { useAsyncList } from '@wordpress/compose'; */ import ItemActions from './item-actions'; -export default function ViewGrid( { data, fields, view, actions, getItemId } ) { +export default function ViewGrid( { + data, + fields, + view, + actions, + getItemId, + deferredRendering, +} ) { const mediaField = fields.find( ( field ) => field.id === view.layout.mediaField ); @@ -29,6 +36,7 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { ) ); const shownData = useAsyncList( data, { step: 3 } ); + const usedData = deferredRendering ? shownData : data; return ( <Grid gap={ 8 } @@ -36,7 +44,7 @@ export default function ViewGrid( { data, fields, view, actions, getItemId } ) { alignment="top" className="dataviews-grid-view" > - { shownData.map( ( item, index ) => ( + { usedData.map( ( item, index ) => ( <VStack spacing={ 3 } key={ getItemId?.( item ) || index } diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index cd9b651cabca55..516264946d1f67 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -20,8 +20,10 @@ export default function ViewList( { getItemId, onSelectionChange, selection, + deferredRendering, } ) { const shownData = useAsyncList( data, { step: 3 } ); + const usedData = deferredRendering ? shownData : data; const mediaField = fields.find( ( field ) => field.id === view.layout.mediaField ); @@ -45,7 +47,7 @@ export default function ViewList( { return ( <ul className="dataviews-list-view"> - { shownData.map( ( item, index ) => { + { usedData.map( ( item, index ) => { return ( <li key={ getItemId?.( item ) || index }> <div diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 5ce9456344f695..dece8a55107c03 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -340,6 +340,7 @@ function ViewTable( { getItemId, isLoading = false, paginationInfo, + deferredRendering, } ) { const columns = useMemo( () => { const _columns = fields.map( ( field ) => { @@ -367,7 +368,7 @@ function ViewTable( { } return _columns; - }, [ fields, actions, view ] ); + }, [ fields, actions ] ); const columnVisibility = useMemo( () => { if ( ! view.hiddenFields?.length ) { @@ -435,8 +436,9 @@ function ViewTable( { } ); const shownData = useAsyncList( data ); + const usedData = deferredRendering ? shownData : data; const dataView = useReactTable( { - data: shownData, + data: usedData, columns, manualSorting: true, manualFiltering: true, diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index fe6f50a4a9c070..c92ce35ebe46dc 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -331,6 +331,7 @@ export default function PagePages() { view={ view } onChangeView={ onChangeView } onSelectionChange={ onSelectionChange } + deferredRendering={ false } /> </Page> { view.type === LAYOUT_LIST && ( diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index c4014f9bd7cbc4..e51f9ba970c4cd 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -353,6 +353,7 @@ export default function DataviewsTemplates() { view={ view } onChangeView={ onChangeView } supportedLayouts={ [ LAYOUT_TABLE, LAYOUT_GRID ] } + deferredRendering={ ! view.hiddenFields?.includes( 'preview' ) } /> </Page> ); From f76dcf3186cbabcfee332dfdf613b303f3db698e Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:24:25 +0200 Subject: [PATCH 070/325] useBlockProps: combine store subscriptions (#56847) --- .../block-list/use-block-props/index.js | 92 ++++++++++++++----- .../use-block-props/use-block-class-names.js | 66 ------------- .../use-block-custom-class-name.js | 44 --------- .../use-block-default-class-name.js | 35 ------- .../use-focus-first-element.js | 35 +------ .../use-block-props/use-is-hovered.js | 15 +-- .../use-selected-block-event-handlers.js | 6 +- .../components/rich-text/use-format-types.js | 80 ++++++++-------- .../src/navigation/edit/index.js | 9 +- .../provider/use-block-editor-settings.js | 15 ++- 10 files changed, 128 insertions(+), 269 deletions(-) delete mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js delete mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js delete mode 100644 packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index d5f4e7608255db..cd7c66174c3ff9 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -11,6 +11,8 @@ import { __, sprintf } from '@wordpress/i18n'; import { __unstableGetBlockProps as getBlockProps, getBlockType, + isReusableBlock, + getBlockDefaultClassName, store as blocksStore, } from '@wordpress/blocks'; import { useMergeRefs, useDisabled } from '@wordpress/compose'; @@ -25,17 +27,12 @@ import { BlockListBlockContext } from '../block-list-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; import { useBlockEditContext } from '../../block-edit/context'; -import { useBlockClassNames } from './use-block-class-names'; -import { useBlockDefaultClassName } from './use-block-default-class-name'; -import { useBlockCustomClassName } from './use-block-custom-class-name'; -import { useBlockMovingModeClassNames } from './use-block-moving-mode-class-names'; import { useFocusHandler } from './use-focus-handler'; import { useEventHandlers } from './use-selected-block-event-handlers'; import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; import { store as blockEditorStore } from '../../../store'; -import useBlockOverlayActive from '../../block-content-overlay'; import { unlock } from '../../../lock-unlock'; /** @@ -99,10 +96,15 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { name, blockApiVersion, blockTitle, + isSelected, isPartOfSelection, adjustScrolling, enableAnimation, isSubtreeDisabled, + isOutlineEnabled, + hasOverlay, + initialPosition, + classNames, } = useSelect( ( select ) => { const { @@ -117,9 +119,21 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isAncestorMultiSelected, isFirstMultiSelectedBlock, isBlockSubtreeDisabled, + getSettings, + isBlockHighlighted, + __unstableIsFullySelected, + __unstableSelectionHasUnmergeableBlock, + isBlockBeingDragged, + hasSelectedInnerBlock, + hasBlockMovingClientId, + canInsertBlockType, + getBlockRootClientId, + __unstableHasActiveBlockOverlayActive, + __unstableGetEditorMode, + getSelectedBlocksInitialCaretPosition, } = unlock( select( blockEditorStore ) ); const { getActiveBlockVariation } = select( blocksStore ); - const isSelected = isBlockSelected( clientId ); + const _isSelected = isBlockSelected( clientId ); const isPartOfMultiSelection = isBlockMultiSelected( clientId ) || isAncestorMultiSelected( clientId ); @@ -127,6 +141,16 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { const blockType = getBlockType( blockName ); const attributes = getBlockAttributes( clientId ); const match = getActiveBlockVariation( blockName, attributes ); + const { outlineMode } = getSettings(); + const isMultiSelected = isBlockMultiSelected( clientId ); + const checkDeep = true; + const isAncestorOfSelectedBlock = hasSelectedInnerBlock( + clientId, + checkDeep + ); + const typing = isTyping(); + const hasLightBlockWrapper = blockType?.apiVersion > 1; + const movingClientId = hasBlockMovingClientId(); return { index: getBlockIndex( clientId ), @@ -134,31 +158,59 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { name: blockName, blockApiVersion: blockType?.apiVersion || 1, blockTitle: match?.title || blockType?.title, - isPartOfSelection: isSelected || isPartOfMultiSelection, + isSelected: _isSelected, + isPartOfSelection: _isSelected || isPartOfMultiSelection, adjustScrolling: - isSelected || isFirstMultiSelectedBlock( clientId ), + _isSelected || isFirstMultiSelectedBlock( clientId ), enableAnimation: - ! isTyping() && + ! typing && getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), + isOutlineEnabled: outlineMode, + hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), + initialPosition: + _isSelected && __unstableGetEditorMode() === 'edit' + ? getSelectedBlocksInitialCaretPosition() + : undefined, + classNames: classnames( { + 'is-selected': _isSelected, + 'is-highlighted': isBlockHighlighted( clientId ), + 'is-multi-selected': isMultiSelected, + 'is-partially-selected': + isMultiSelected && + ! __unstableIsFullySelected() && + ! __unstableSelectionHasUnmergeableBlock(), + 'is-reusable': isReusableBlock( blockType ), + 'is-dragging': isBlockBeingDragged( clientId ), + 'has-child-selected': isAncestorOfSelectedBlock, + 'remove-outline': _isSelected && outlineMode && typing, + 'is-block-moving-mode': !! movingClientId, + 'can-insert-moving-block': + movingClientId && + canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + [ attributes.className ]: hasLightBlockWrapper, + [ getBlockDefaultClassName( blockName ) ]: + hasLightBlockWrapper, + } ), }; }, [ clientId ] ); - const hasOverlay = useBlockOverlayActive( clientId ); - // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); const htmlSuffix = mode === 'html' && ! __unstableIsHtml ? '-visual' : ''; const mergedRefs = useMergeRefs( [ props.ref, - useFocusFirstElement( clientId ), + useFocusFirstElement( { clientId, initialPosition } ), useBlockRefProvider( clientId ), useFocusHandler( clientId ), - useEventHandlers( clientId ), + useEventHandlers( { clientId, isSelected } ), useNavModeExit( clientId ), - useIsHovered(), + useIsHovered( { isEnabled: isOutlineEnabled } ), useIntersectionObserver(), useMovingAnimation( { isSelected: isPartOfSelection, @@ -190,18 +242,16 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { 'data-title': blockTitle, inert: isSubtreeDisabled ? 'true' : undefined, className: classnames( - // The wp-block className is important for editor styles. - classnames( 'block-editor-block-list__block', { + 'block-editor-block-list__block', + { + // The wp-block className is important for editor styles. 'wp-block': ! isAligned, 'has-block-overlay': hasOverlay, - } ), + }, className, props.className, wrapperProps.className, - useBlockClassNames( clientId ), - useBlockDefaultClassName( clientId ), - useBlockCustomClassName( clientId ), - useBlockMovingModeClassNames( clientId ) + classNames ), style: { ...wrapperProps.style, ...props.style }, }; diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js deleted file mode 100644 index fce94b85f91190..00000000000000 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-class-names.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { isReusableBlock, getBlockType } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../../store'; - -/** - * Returns the class names used for the different states of the block. - * - * @param {string} clientId The block client ID. - * - * @return {string} The class names. - */ -export function useBlockClassNames( clientId ) { - return useSelect( - ( select ) => { - const { - isBlockBeingDragged, - isBlockHighlighted, - isBlockSelected, - isBlockMultiSelected, - getBlockName, - getSettings, - hasSelectedInnerBlock, - isTyping, - __unstableIsFullySelected, - __unstableSelectionHasUnmergeableBlock, - } = select( blockEditorStore ); - const { outlineMode } = getSettings(); - const isDragging = isBlockBeingDragged( clientId ); - const isSelected = isBlockSelected( clientId ); - const name = getBlockName( clientId ); - const checkDeep = true; - // "ancestor" is the more appropriate label due to "deep" check. - const isAncestorOfSelectedBlock = hasSelectedInnerBlock( - clientId, - checkDeep - ); - const isMultiSelected = isBlockMultiSelected( clientId ); - return classnames( { - 'is-selected': isSelected, - 'is-highlighted': isBlockHighlighted( clientId ), - 'is-multi-selected': isMultiSelected, - 'is-partially-selected': - isMultiSelected && - ! __unstableIsFullySelected() && - ! __unstableSelectionHasUnmergeableBlock(), - 'is-reusable': isReusableBlock( getBlockType( name ) ), - 'is-dragging': isDragging, - 'has-child-selected': isAncestorOfSelectedBlock, - 'remove-outline': isSelected && outlineMode && isTyping(), - } ); - }, - [ clientId ] - ); -} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js deleted file mode 100644 index 50372c09a042ca..00000000000000 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-custom-class-name.js +++ /dev/null @@ -1,44 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { getBlockType } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../../store'; - -/** - * Returns the custom class name if the block is a light block. - * - * @param {string} clientId The block client ID. - * - * @return {string} The custom class name. - */ -export function useBlockCustomClassName( clientId ) { - // It's good for this to be a separate selector because it will be executed - // on every attribute change, while the other selectors are not re-evaluated - // as much. - return useSelect( - ( select ) => { - const { getBlockName, getBlockAttributes } = - select( blockEditorStore ); - const attributes = getBlockAttributes( clientId ); - - if ( ! attributes?.className ) { - return; - } - - const blockType = getBlockType( getBlockName( clientId ) ); - const hasLightBlockWrapper = blockType?.apiVersion > 1; - - if ( ! hasLightBlockWrapper ) { - return; - } - - return attributes.className; - }, - [ clientId ] - ); -} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js b/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js deleted file mode 100644 index 7877ceb96490ca..00000000000000 --- a/packages/block-editor/src/components/block-list/use-block-props/use-block-default-class-name.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { getBlockType, getBlockDefaultClassName } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../../store'; - -/** - * Returns the default class name if the block is a light block and it supports - * `className`. - * - * @param {string} clientId The block client ID. - * - * @return {string} The class name, e.g. `wp-block-paragraph`. - */ -export function useBlockDefaultClassName( clientId ) { - return useSelect( - ( select ) => { - const name = select( blockEditorStore ).getBlockName( clientId ); - const blockType = getBlockType( name ); - const hasLightBlockWrapper = blockType?.apiVersion > 1; - - if ( ! hasLightBlockWrapper ) { - return; - } - - return getBlockDefaultClassName( name ); - }, - [ clientId ] - ); -} diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js index 21515d74b2ecdb..b3f844cea5429a 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-focus-first-element.js @@ -18,38 +18,6 @@ import { store as blockEditorStore } from '../../../store'; /** @typedef {import('@wordpress/element').RefObject} RefObject */ -/** - * Returns the initial position if the block needs to be focussed, `undefined` - * otherwise. The initial position is either 0 (start) or -1 (end). - * - * @param {string} clientId Block client ID. - * - * @return {number} The initial position, either 0 (start) or -1 (end). - */ -function useInitialPosition( clientId ) { - return useSelect( - ( select ) => { - const { - getSelectedBlocksInitialCaretPosition, - __unstableGetEditorMode, - isBlockSelected, - } = select( blockEditorStore ); - - if ( ! isBlockSelected( clientId ) ) { - return; - } - - if ( __unstableGetEditorMode() !== 'edit' ) { - return; - } - - // If there's no initial position, return 0 to focus the start. - return getSelectedBlocksInitialCaretPosition(); - }, - [ clientId ] - ); -} - /** * Transitions focus to the block or inner tabbable when the block becomes * selected and an initial position is set. @@ -58,9 +26,8 @@ function useInitialPosition( clientId ) { * * @return {RefObject} React ref with the block element. */ -export function useFocusFirstElement( clientId ) { +export function useFocusFirstElement( { clientId, initialPosition } ) { const ref = useRef(); - const initialPosition = useInitialPosition( clientId ); const { isBlockSelected, isMultiSelecting } = useSelect( blockEditorStore ); useEffect( () => { diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js b/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js index 653ef3049b1242..08c42eb1fdb085 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-is-hovered.js @@ -1,14 +1,8 @@ /** * WordPress dependencies */ -import { useSelect } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../../../store'; - function listener( event ) { if ( event.defaultPrevented ) { return; @@ -20,16 +14,11 @@ function listener( event ) { event.currentTarget.classList[ action ]( 'is-hovered' ); } -/** +/* * Adds `is-hovered` class when the block is hovered and in navigation or * outline mode. */ -export function useIsHovered() { - const isEnabled = useSelect( ( select ) => { - const { getSettings } = select( blockEditorStore ); - return getSettings().outlineMode; - }, [] ); - +export function useIsHovered( { isEnabled } ) { return useRefEffect( ( node ) => { if ( isEnabled ) { diff --git a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js index bd6566112263f1..bf4fc55879448a 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js +++ b/packages/block-editor/src/components/block-list/use-block-props/use-selected-block-event-handlers.js @@ -19,11 +19,7 @@ import { store as blockEditorStore } from '../../../store'; * * @param {string} clientId Block client ID. */ -export function useEventHandlers( clientId ) { - const isSelected = useSelect( - ( select ) => select( blockEditorStore ).isBlockSelected( clientId ), - [ clientId ] - ); +export function useEventHandlers( { clientId, isSelected } ) { const { getBlockRootClientId, getBlockIndex } = useSelect( blockEditorStore ); const { insertDefaultBlock, removeBlock } = useDispatch( blockEditorStore ); diff --git a/packages/block-editor/src/components/rich-text/use-format-types.js b/packages/block-editor/src/components/rich-text/use-format-types.js index 9d26d619432496..e30880422634c7 100644 --- a/packages/block-editor/src/components/rich-text/use-format-types.js +++ b/packages/block-editor/src/components/rich-text/use-format-types.js @@ -1,14 +1,9 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as richTextStore } from '@wordpress/rich-text'; -function formatTypesSelector( select ) { - return select( richTextStore ).getFormatTypes(); -} - /** * Set of all interactive content tags. * @@ -64,45 +59,50 @@ export function useFormatTypes( { withoutInteractiveFormatting, allowedFormats, } ) { - const allFormatTypes = useSelect( formatTypesSelector, [] ); - const formatTypes = useMemo( () => { - return allFormatTypes.filter( ( { name, interactive, tagName } ) => { - if ( allowedFormats && ! allowedFormats.includes( name ) ) { - return false; - } + const { formatTypes, ...keyedSelected } = useSelect( + ( select ) => { + const _formatTypes = select( richTextStore ) + .getFormatTypes() + .filter( ( { name, interactive, tagName } ) => { + if ( allowedFormats && ! allowedFormats.includes( name ) ) { + return false; + } - if ( - withoutInteractiveFormatting && - ( interactive || interactiveContentTags.has( tagName ) ) - ) { - return false; - } + if ( + withoutInteractiveFormatting && + ( interactive || interactiveContentTags.has( tagName ) ) + ) { + return false; + } - return true; - } ); - }, [ allFormatTypes, allowedFormats, withoutInteractiveFormatting ] ); - const keyedSelected = useSelect( - ( select ) => - formatTypes.reduce( ( accumulator, type ) => { - if ( ! type.__experimentalGetPropsForEditableTreePreparation ) { - return accumulator; - } + return true; + } ); + return _formatTypes.reduce( + ( accumulator, type ) => { + if ( + ! type.__experimentalGetPropsForEditableTreePreparation + ) { + return accumulator; + } - return { - ...accumulator, - ...prefixSelectKeys( - type.__experimentalGetPropsForEditableTreePreparation( - select, - { - richTextIdentifier: identifier, - blockClientId: clientId, - } + return { + ...accumulator, + ...prefixSelectKeys( + type.__experimentalGetPropsForEditableTreePreparation( + select, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } + ), + type.name ), - type.name - ), - }; - }, {} ), - [ formatTypes, clientId, identifier ] + }; + }, + { formatTypes: _formatTypes } + ); + }, + [ clientId, identifier, allowedFormats, withoutInteractiveFormatting ] ); const dispatch = useDispatch(); const prepareHandlers = []; diff --git a/packages/block-library/src/navigation/edit/index.js b/packages/block-library/src/navigation/edit/index.js index f12d83e2fe2eae..2e94cddcc9bc24 100644 --- a/packages/block-library/src/navigation/edit/index.js +++ b/packages/block-library/src/navigation/edit/index.js @@ -24,7 +24,6 @@ import { getColorClassName, Warning, __experimentalColorGradientSettingsDropdown as ColorGradientSettingsDropdown, - __experimentalUseBlockOverlayActive as useBlockOverlayActive, __experimentalUseMultipleOriginColorsAndGradients as useMultipleOriginColorsAndGradients, useBlockEditingMode, } from '@wordpress/block-editor'; @@ -290,7 +289,13 @@ function Navigation( { const textDecoration = attributes.style?.typography?.textDecoration; - const hasBlockOverlay = useBlockOverlayActive( clientId ); + const hasBlockOverlay = useSelect( + ( select ) => + select( blockEditorStore ).__unstableHasActiveBlockOverlayActive( + clientId + ), + [ clientId ] + ); const isResponsive = 'never' !== overlayMenu; const blockProps = useBlockProps( { ref: navRef, diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index e2cbba7e6a7571..de5d9cf43437d4 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -96,6 +96,8 @@ function useBlockEditorSettings( settings, postType, postId ) { pageOnFront, pageForPosts, userPatternCategories, + restBlockPatterns, + restBlockPatternCategories, } = useSelect( ( select ) => { const isWeb = Platform.OS === 'web'; @@ -105,6 +107,8 @@ function useBlockEditorSettings( settings, postType, postId ) { getEntityRecord, getUserPatternCategories, getEntityRecords, + getBlockPatterns, + getBlockPatternCategories, } = select( coreStore ); const siteSettings = canUser( 'read', 'settings' ) @@ -127,6 +131,8 @@ function useBlockEditorSettings( settings, postType, postId ) { pageOnFront: siteSettings?.page_on_front, pageForPosts: siteSettings?.page_for_posts, userPatternCategories: getUserPatternCategories(), + restBlockPatterns: getBlockPatterns(), + restBlockPatternCategories: getBlockPatternCategories(), }; }, [ postType, postId ] @@ -139,15 +145,6 @@ function useBlockEditorSettings( settings, postType, postId ) { settings.__experimentalAdditionalBlockPatternCategories ?? // WP 6.0 settings.__experimentalBlockPatternCategories; // WP 5.9 - const { restBlockPatterns, restBlockPatternCategories } = useSelect( - ( select ) => ( { - restBlockPatterns: select( coreStore ).getBlockPatterns(), - restBlockPatternCategories: - select( coreStore ).getBlockPatternCategories(), - } ), - [] - ); - const blockPatterns = useMemo( () => [ From d295da8e683919cdb042977da81b70df9ed79669 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 7 Dec 2023 12:28:55 +0100 Subject: [PATCH 071/325] Editor: Move the BlockCanvas component within the EditorCanvas component (#56850) --- .../src/components/visual-editor/index.js | 32 ++-------- .../components/block-editor/editor-canvas.js | 60 ++++++++--------- .../src/components/editor-canvas/index.js | 64 +++++++++++++++---- 3 files changed, 87 insertions(+), 69 deletions(-) diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 655549138d3ebe..9b975414510c52 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -12,14 +12,11 @@ import { } from '@wordpress/editor'; import { BlockTools, - __unstableUseTypewriter as useTypewriter, __experimentalUseResizeCanvas as useResizeCanvas, - privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { useRef, useMemo } from '@wordpress/element'; import { __unstableMotion as motion } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; -import { useMergeRefs } from '@wordpress/compose'; import { store as blocksStore } from '@wordpress/blocks'; /** @@ -28,9 +25,6 @@ import { store as blocksStore } from '@wordpress/blocks'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { ExperimentalBlockCanvas: BlockCanvas } = unlock( - blockEditorPrivateApis -); const { EditorCanvas } = unlock( editorPrivateApis ); const isGutenbergPlugin = process.env.IS_GUTENBERG_PLUGIN ? true : false; @@ -104,7 +98,6 @@ export default function VisualEditor( { styles } ) { } const ref = useRef(); - const contentRef = useMergeRefs( [ ref, useTypewriter() ] ); styles = useMemo( () => [ @@ -146,25 +139,14 @@ export default function VisualEditor( { styles } ) { initial={ desktopCanvasStyles } className={ previewMode } > - <BlockCanvas - shouldIframe={ isToBeIframed } - contentRef={ contentRef } + <EditorCanvas + ref={ ref } + disableIframe={ ! isToBeIframed } styles={ styles } - height="100%" - > - <EditorCanvas - dropZoneElement={ - // When iframed, pass in the html element of the iframe to - // ensure the drop zone extends to the edges of the iframe. - isToBeIframed - ? ref.current?.parentNode - : ref.current - } - // We should auto-focus the canvas (title) on load. - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={ ! isWelcomeGuideVisible } - /> - </BlockCanvas> + // We should auto-focus the canvas (title) on load. + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={ ! isWelcomeGuideVisible } + /> </motion.div> </motion.div> </BlockTools> diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index 15d638aa329e12..fa20a4abae1ad2 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -8,12 +8,11 @@ import classnames from 'classnames'; */ import { __experimentalUseResizeCanvas as useResizeCanvas, - privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; -import { useState, useEffect } from '@wordpress/element'; +import { useState, useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; @@ -27,9 +26,6 @@ import { NAVIGATION_POST_TYPE, } from '../../utils/constants'; -const { ExperimentalBlockCanvas: BlockCanvas } = unlock( - blockEditorPrivateApis -); const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); function EditorCanvas( { @@ -101,9 +97,35 @@ function EditorCanvas( { ? false : undefined; + const styles = useMemo( + () => [ + ...settings.styles, + { + // Forming a "block formatting context" to prevent margin collapsing. + // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context + + css: `.is-root-container{display:flow-root;${ + // Some themes will have `min-height: 100vh` for the root container, + // which isn't a requirement in auto resize mode. + enableResizing ? 'min-height:0!important;' : '' + }}body{position:relative; ${ + canvasMode === 'view' + ? 'cursor: pointer; min-height: 100vh;' + : '' + }}}`, + }, + ], + [ settings.styles, enableResizing, canvasMode ] + ); + return ( - <BlockCanvas - height="100%" + <EditorCanvasRoot + ref={ contentRef } + className={ classnames( 'edit-site-editor-canvas__block-list', { + 'is-navigation-block': isTemplateTypeNavigation, + } ) } + renderAppender={ showBlockAppender } + styles={ styles } iframeProps={ { expand: isZoomOutMode, scale: isZoomOutMode ? 0.45 : undefined, @@ -118,31 +140,9 @@ function EditorCanvas( { ...props, ...( canvasMode === 'view' ? viewModeProps : {} ), } } - styles={ settings.styles } - contentRef={ contentRef } > - <style>{ - // Forming a "block formatting context" to prevent margin collapsing. - // @see https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Block_formatting_context - `.is-root-container{display:flow-root;${ - // Some themes will have `min-height: 100vh` for the root container, - // which isn't a requirement in auto resize mode. - enableResizing ? 'min-height:0!important;' : '' - }}body{position:relative; ${ - canvasMode === 'view' - ? 'cursor: pointer; min-height: 100vh;' - : '' - }}}` - }</style> - <EditorCanvasRoot - dropZoneElement={ contentRef.current?.parentNode } - className={ classnames( 'edit-site-editor-canvas__block-list', { - 'is-navigation-block': isTemplateTypeNavigation, - } ) } - renderAppender={ showBlockAppender } - /> { children } - </BlockCanvas> + </EditorCanvasRoot> ); } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index 906eb6b272fc78..86df470087862b 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -9,15 +9,17 @@ import classnames from 'classnames'; import { BlockList, store as blockEditorStore, + __unstableUseTypewriter as useTypewriter, __unstableUseTypingObserver as useTypingObserver, useSettings, __experimentalRecursionProvider as RecursionProvider, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; -import { useEffect, useRef, useMemo } from '@wordpress/element'; +import { useEffect, useRef, useMemo, forwardRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; +import { useMergeRefs } from '@wordpress/compose'; /** * Internal dependencies @@ -26,9 +28,12 @@ import PostTitle from '../post-title'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { LayoutStyle, useLayoutClasses, useLayoutStyles } = unlock( - blockEditorPrivateApis -); +const { + LayoutStyle, + useLayoutClasses, + useLayoutStyles, + ExperimentalBlockCanvas: BlockCanvas, +} = unlock( blockEditorPrivateApis ); /** * Given an array of nested blocks, find the first Post Content @@ -65,13 +70,19 @@ function checkForPostContentAtRootLevel( blocks ) { return false; } -export default function EditorCanvas( { - // Ideally as we unify post and site editors, we won't need these props. - autoFocus, - dropZoneElement, - className, - renderAppender, -} ) { +function EditorCanvas( + { + // Ideally as we unify post and site editors, we won't need these props. + autoFocus, + className, + renderAppender, + styles, + disableIframe = false, + iframeProps, + children, + }, + ref +) { const { renderingMode, postContentAttributes, @@ -268,8 +279,24 @@ export default function EditorCanvas( { .is-root-container.alignfull { max-width: none; margin-left: auto; margin-right: auto;} .is-root-container.alignfull:where(.is-layout-flow) > :not(.alignleft):not(.alignright) { max-width: none;}`; + const localRef = useRef(); + const typewriterRef = useTypewriter(); + const contentRef = useMergeRefs( + [ + ref, + localRef, + renderingMode === 'post-only' ? typewriterRef : undefined, + ].filter( ( r ) => !! r ) + ); + return ( - <> + <BlockCanvas + shouldIframe={ ! disableIframe } + contentRef={ contentRef } + styles={ styles } + height="100%" + iframeProps={ iframeProps } + > { themeSupportsLayout && ! themeHasDisabledLayoutStyles && renderingMode === 'post-only' && ( @@ -325,10 +352,19 @@ export default function EditorCanvas( { : `${ blockListLayoutClass } wp-block-post-content` // Ensure root level blocks receive default/flow blockGap styling rules. ) } layout={ blockListLayout } - dropZoneElement={ dropZoneElement } + dropZoneElement={ + // When iframed, pass in the html element of the iframe to + // ensure the drop zone extends to the edges of the iframe. + disableIframe + ? localRef.current + : localRef.current?.parentNode + } renderAppender={ renderAppender } /> </RecursionProvider> - </> + { children } + </BlockCanvas> ); } + +export default forwardRef( EditorCanvas ); From 7bdc190a396fd7b4f03ae4871df4bbb413a3ee69 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:50:33 +0200 Subject: [PATCH 072/325] Block editor: hooks: share block settings (#56852) --- packages/block-editor/src/hooks/border.js | 9 +-- packages/block-editor/src/hooks/color.js | 4 +- packages/block-editor/src/hooks/dimensions.js | 10 +-- packages/block-editor/src/hooks/style.js | 61 +++++++++---------- packages/block-editor/src/hooks/typography.js | 10 +-- 5 files changed, 37 insertions(+), 57 deletions(-) diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index 735d91e76538e5..d8905b29a29617 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -19,11 +19,7 @@ import { useSelect } from '@wordpress/data'; import { getColorClassName } from '../components/colors'; import InspectorControls from '../components/inspector-controls'; import useMultipleOriginColorsAndGradients from '../components/colors-gradients/use-multiple-origin-colors-and-gradients'; -import { - cleanEmptyObject, - shouldSkipSerialization, - useBlockSettings, -} from './utils'; +import { cleanEmptyObject, shouldSkipSerialization } from './utils'; import { useHasBorderPanel, BorderPanel as StylesBorderPanel, @@ -137,8 +133,7 @@ function BordersInspectorControl( { children, resetAllFilter } ) { ); } -function BorderPanelPure( { clientId, name, setAttributes } ) { - const settings = useBlockSettings( name ); +function BorderPanelPure( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasBorderPanel( settings ); function selector( select ) { const { style, borderColor } = diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 6addd94d93ee58..d5cb21e5dcf9a2 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -24,7 +24,6 @@ import { cleanEmptyObject, transformStyles, shouldSkipSerialization, - useBlockSettings, } from './utils'; import { useSettings } from '../components/use-settings'; import InspectorControls from '../components/inspector-controls'; @@ -291,8 +290,7 @@ function ColorInspectorControl( { children, resetAllFilter } ) { ); } -function ColorEditPure( { clientId, name, setAttributes } ) { - const settings = useBlockSettings( name ); +function ColorEditPure( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasColorPanel( settings ); function selector( select ) { const { style, textColor, backgroundColor, gradient } = diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index c6d64d4ef785f3..4dcba5c4abef68 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -20,7 +20,7 @@ import { PaddingVisualizer } from './padding'; import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; -import { cleanEmptyObject, useBlockSettings } from './utils'; +import { cleanEmptyObject } from './utils'; export const DIMENSIONS_SUPPORT_KEY = 'dimensions'; export const SPACING_SUPPORT_KEY = 'spacing'; @@ -66,13 +66,7 @@ function DimensionsInspectorControl( { children, resetAllFilter } ) { ); } -function DimensionsPanelPure( { - clientId, - name, - setAttributes, - __unstableParentLayout, -} ) { - const settings = useBlockSettings( name, __unstableParentLayout ); +function DimensionsPanelPure( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasDimensionsPanel( settings ); const value = useSelect( ( select ) => diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 8b3a475e1babe5..1acb2cda3ac017 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -33,7 +33,11 @@ import { DimensionsPanel, } from './dimensions'; import useDisplayBlockControls from '../components/use-display-block-controls'; -import { shouldSkipSerialization, useStyleOverride } from './utils'; +import { + shouldSkipSerialization, + useStyleOverride, + useBlockSettings, +} from './utils'; import { scopeSelector } from '../components/global-styles/utils'; import { useBlockEditingMode } from '../components/block-editing-mode'; @@ -345,6 +349,30 @@ export function addEditProps( settings ) { return settings; } +function BlockStyleControls( { + clientId, + name, + setAttributes, + __unstableParentLayout, +} ) { + const settings = useBlockSettings( name, __unstableParentLayout ); + const passedProps = { + clientId, + name, + setAttributes, + settings, + }; + return ( + <> + <ColorEdit { ...passedProps } /> + <BackgroundImagePanel { ...passedProps } /> + <TypographyPanel { ...passedProps } /> + <BorderPanel { ...passedProps } /> + <DimensionsPanel { ...passedProps } /> + </> + ); +} + /** * Override the default edit UI to include new inspector controls for * all the custom styles configs. @@ -361,40 +389,11 @@ export const withBlockStyleControls = createHigherOrderComponent( const shouldDisplayControls = useDisplayBlockControls(); const blockEditingMode = useBlockEditingMode(); - const { clientId, name, setAttributes, __unstableParentLayout } = props; return ( <> { shouldDisplayControls && blockEditingMode === 'default' && ( - <> - <ColorEdit - clientId={ clientId } - name={ name } - setAttributes={ setAttributes } - /> - <BackgroundImagePanel - clientId={ clientId } - name={ name } - setAttributes={ setAttributes } - /> - <TypographyPanel - clientId={ clientId } - name={ name } - setAttributes={ setAttributes } - __unstableParentLayout={ __unstableParentLayout } - /> - <BorderPanel - clientId={ clientId } - name={ name } - setAttributes={ setAttributes } - /> - <DimensionsPanel - clientId={ clientId } - name={ name } - setAttributes={ setAttributes } - __unstableParentLayout={ __unstableParentLayout } - /> - </> + <BlockStyleControls { ...props } /> ) } <BlockEdit key="edit" { ...props } /> </> diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index d5bf9ec42ad040..7b2fdc9ca28fb2 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -18,7 +18,7 @@ import { import { LINE_HEIGHT_SUPPORT_KEY } from './line-height'; import { FONT_FAMILY_SUPPORT_KEY } from './font-family'; import { FONT_SIZE_SUPPORT_KEY } from './font-size'; -import { cleanEmptyObject, useBlockSettings } from './utils'; +import { cleanEmptyObject } from './utils'; import { store as blockEditorStore } from '../store'; function omit( object, keys ) { @@ -109,19 +109,13 @@ function TypographyInspectorControl( { children, resetAllFilter } ) { ); } -function TypographyPanelPure( { - clientId, - name, - setAttributes, - __unstableParentLayout, -} ) { +function TypographyPanelPure( { clientId, name, setAttributes, settings } ) { function selector( select ) { const { style, fontFamily, fontSize } = select( blockEditorStore ).getBlockAttributes( clientId ) || {}; return { style, fontFamily, fontSize }; } const { style, fontFamily, fontSize } = useSelect( selector, [ clientId ] ); - const settings = useBlockSettings( name, __unstableParentLayout ); const isEnabled = useHasTypographyPanel( settings ); const value = useMemo( () => attributesToStyle( { style, fontFamily, fontSize } ), From 3514f11e8932997f845d1b55f4b00265c514319d Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 7 Dec 2023 13:08:02 +0100 Subject: [PATCH 073/325] Navigation editor: Fix content mode (#56856) --- .../editor/src/components/provider/index.js | 38 ++----------------- .../provider/navigation-block-editing-mode.js | 37 ++++++++++++++++++ 2 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 packages/editor/src/components/provider/navigation-block-editing-mode.js diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 3e32c7f80f48e1..714e05e5385bc2 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -9,7 +9,6 @@ import { BlockEditorProvider, BlockContextProvider, privateApis as blockEditorPrivateApis, - store as blockEditorStore, } from '@wordpress/block-editor'; import { store as noticesStore } from '@wordpress/notices'; import { privateApis as editPatternsPrivateApis } from '@wordpress/patterns'; @@ -23,42 +22,13 @@ import { store as editorStore } from '../../store'; import useBlockEditorSettings from './use-block-editor-settings'; import { unlock } from '../../lock-unlock'; import DisableNonPageContentBlocks from './disable-non-page-content-blocks'; +import NavigationBlockEditingMode from './navigation-block-editing-mode'; const { ExperimentalBlockEditorProvider } = unlock( blockEditorPrivateApis ); const { PatternsMenuItems } = unlock( editPatternsPrivateApis ); const noop = () => {}; -/** - * For the Navigation block editor, we need to force the block editor to contentOnly for that block. - * - * Set block editing mode to contentOnly when entering Navigation focus mode. - * this ensures that non-content controls on the block will be hidden and thus - * the user can focus on editing the Navigation Menu content only. - * - * @param {string} navigationBlockClientId ClientId. - */ -function useForceFocusModeForNavigation( navigationBlockClientId ) { - const { setBlockEditingMode, unsetBlockEditingMode } = - useDispatch( blockEditorStore ); - - useEffect( () => { - if ( ! navigationBlockClientId ) { - return; - } - - setBlockEditingMode( navigationBlockClientId, 'contentOnly' ); - - return () => { - unsetBlockEditingMode( navigationBlockClientId ); - }; - }, [ - navigationBlockClientId, - unsetBlockEditingMode, - setBlockEditingMode, - ] ); -} - /** * Depending on the post, template and template mode, * returns the appropriate blocks and change handlers for the block editor provider. @@ -114,9 +84,6 @@ function useBlockEditorProps( post, template, mode ) { const disableRootLevelChanges = ( !! template && mode === 'template-locked' ) || post.type === 'wp_navigation'; - const navigationBlockClientId = - post.type === 'wp_navigation' && blocks && blocks[ 0 ]?.clientId; - useForceFocusModeForNavigation( navigationBlockClientId ); if ( disableRootLevelChanges ) { return [ blocks, noop, noop ]; } @@ -274,6 +241,9 @@ export const ExperimentalEditorProvider = withRegistryProvider( { mode === 'template-locked' && ( <DisableNonPageContentBlocks /> ) } + { type === 'wp_navigation' && ( + <NavigationBlockEditingMode /> + ) } </BlockEditorProviderComponent> </BlockContextProvider> </EntityProvider> diff --git a/packages/editor/src/components/provider/navigation-block-editing-mode.js b/packages/editor/src/components/provider/navigation-block-editing-mode.js new file mode 100644 index 00000000000000..f45de7c149d826 --- /dev/null +++ b/packages/editor/src/components/provider/navigation-block-editing-mode.js @@ -0,0 +1,37 @@ +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +/** + * For the Navigation block editor, we need to force the block editor to contentOnly for that block. + * + * Set block editing mode to contentOnly when entering Navigation focus mode. + * this ensures that non-content controls on the block will be hidden and thus + * the user can focus on editing the Navigation Menu content only. + */ + +export default function NavigationBlockEditingMode() { + // In the navigation block editor, + // the navigation block is the only root block. + const blockClientId = useSelect( + ( select ) => select( blockEditorStore ).getBlockOrder()?.[ 0 ], + [] + ); + const { setBlockEditingMode, unsetBlockEditingMode } = + useDispatch( blockEditorStore ); + + useEffect( () => { + if ( ! blockClientId ) { + return; + } + + setBlockEditingMode( blockClientId, 'contentOnly' ); + + return () => { + unsetBlockEditingMode( blockClientId ); + }; + }, [ blockClientId, unsetBlockEditingMode, setBlockEditingMode ] ); +} From e63ef38ef7585d33e4b67810bb368dbae04ee992 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 14:28:19 +0200 Subject: [PATCH 074/325] Revert format types hook refactor (#56859) --- .../components/rich-text/use-format-types.js | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/use-format-types.js b/packages/block-editor/src/components/rich-text/use-format-types.js index e30880422634c7..9d26d619432496 100644 --- a/packages/block-editor/src/components/rich-text/use-format-types.js +++ b/packages/block-editor/src/components/rich-text/use-format-types.js @@ -1,9 +1,14 @@ /** * WordPress dependencies */ +import { useMemo } from '@wordpress/element'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as richTextStore } from '@wordpress/rich-text'; +function formatTypesSelector( select ) { + return select( richTextStore ).getFormatTypes(); +} + /** * Set of all interactive content tags. * @@ -59,50 +64,45 @@ export function useFormatTypes( { withoutInteractiveFormatting, allowedFormats, } ) { - const { formatTypes, ...keyedSelected } = useSelect( - ( select ) => { - const _formatTypes = select( richTextStore ) - .getFormatTypes() - .filter( ( { name, interactive, tagName } ) => { - if ( allowedFormats && ! allowedFormats.includes( name ) ) { - return false; - } + const allFormatTypes = useSelect( formatTypesSelector, [] ); + const formatTypes = useMemo( () => { + return allFormatTypes.filter( ( { name, interactive, tagName } ) => { + if ( allowedFormats && ! allowedFormats.includes( name ) ) { + return false; + } - if ( - withoutInteractiveFormatting && - ( interactive || interactiveContentTags.has( tagName ) ) - ) { - return false; - } + if ( + withoutInteractiveFormatting && + ( interactive || interactiveContentTags.has( tagName ) ) + ) { + return false; + } - return true; - } ); - return _formatTypes.reduce( - ( accumulator, type ) => { - if ( - ! type.__experimentalGetPropsForEditableTreePreparation - ) { - return accumulator; - } + return true; + } ); + }, [ allFormatTypes, allowedFormats, withoutInteractiveFormatting ] ); + const keyedSelected = useSelect( + ( select ) => + formatTypes.reduce( ( accumulator, type ) => { + if ( ! type.__experimentalGetPropsForEditableTreePreparation ) { + return accumulator; + } - return { - ...accumulator, - ...prefixSelectKeys( - type.__experimentalGetPropsForEditableTreePreparation( - select, - { - richTextIdentifier: identifier, - blockClientId: clientId, - } - ), - type.name + return { + ...accumulator, + ...prefixSelectKeys( + type.__experimentalGetPropsForEditableTreePreparation( + select, + { + richTextIdentifier: identifier, + blockClientId: clientId, + } ), - }; - }, - { formatTypes: _formatTypes } - ); - }, - [ clientId, identifier, allowedFormats, withoutInteractiveFormatting ] + type.name + ), + }; + }, {} ), + [ formatTypes, clientId, identifier ] ); const dispatch = useDispatch(); const prepareHandlers = []; From 5067361dc000cb3ab2b0a180b6a4eae19cd7e7a2 Mon Sep 17 00:00:00 2001 From: Andrew Hayward <andrew.hayward@automattic.com> Date: Thu, 7 Dec 2023 12:42:36 +0000 Subject: [PATCH 075/325] Adding `aria-sort` to table view headers (#56860) Ensures sorting semantics are not just communicated visually --- packages/dataviews/src/view-table.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index dece8a55107c03..0e51bd85cdee48 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -545,6 +545,9 @@ function ViewTable( { // TODO:Add spinner or progress bar.. return <h3>{ __( 'Loading' ) }</h3>; } + + const sortValues = { asc: 'ascending', desc: 'descending' }; + return ( <div className="dataviews-table-view-wrapper"> { hasRows && ( @@ -568,6 +571,11 @@ function ViewTable( { .maxWidth || undefined, } } data-field-id={ header.id } + aria-sort={ + sortValues[ + header.column.getIsSorted() + ] + } > <HeaderMenu dataView={ dataView } From 23c0de4e162d0d5eb02420e00ac40aabb2c4d1a5 Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Thu, 7 Dec 2023 13:04:56 +0000 Subject: [PATCH 076/325] Update data view layout (#56786) Co-authored-by: ntsekouras <ntsekouras@outlook.com> --- packages/dataviews/src/dataviews.js | 7 ++- packages/dataviews/src/style.scss | 46 ++++++++++++++++--- packages/dataviews/src/view-table.js | 13 +++++- .../src/components/page-pages/style.scss | 4 +- .../edit-site/src/components/page/header.js | 3 +- .../edit-site/src/components/page/style.scss | 4 +- 6 files changed, 62 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 13397242953b70..9e7b45d04ef87f 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -49,8 +49,11 @@ export default function DataViews( { }, [ fields ] ); return ( <div className="dataviews-wrapper"> - <VStack spacing={ 4 } justify="flex-start"> - <HStack alignment="flex-start"> + <VStack spacing={ 0 } justify="flex-start"> + <HStack + alignment="flex-start" + className="dataviews__filters-view-actions" + > <HStack justify="start" wrap> { search && ( <Search diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index b4f4ac0c28f956..ab6c052e3a8980 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -1,8 +1,7 @@ .dataviews-wrapper { width: 100%; - height: calc(100% - #{$grid-unit-40} * 2); + height: 100%; overflow: auto; - padding: $grid-unit-40 $grid-unit-40 0; box-sizing: border-box; > div { @@ -10,13 +9,18 @@ } } +.dataviews__filters-view-actions { + padding: $grid-unit-15 $grid-unit-40; +} + .dataviews-pagination { margin-top: auto; position: sticky; bottom: 0; background-color: $white; - padding: $grid-unit-20 0; - border-top: $border-width solid $gray-200; + padding: $grid-unit-15 $grid-unit-40; + border-top: $border-width solid $gray-100; + color: $gray-700; } .dataviews-filters-options { @@ -29,9 +33,12 @@ border-color: inherit; border-collapse: collapse; position: relative; + color: $gray-700; a { text-decoration: none; + color: $gray-900; + font-weight: 500; } th { text-align: left; @@ -42,6 +49,7 @@ td, th { padding: $grid-unit-15; + min-width: 160px; &[data-field-id="actions"] { text-align: right; } @@ -49,6 +57,16 @@ tr { border-bottom: 1px solid $gray-100; + td:first-child, + th:first-child { + padding-left: $grid-unit-40; + } + + td:last-child, + th:last-child { + padding-right: $grid-unit-40; + } + &:last-child { border-bottom: 0; } @@ -59,9 +77,12 @@ } th { position: sticky; - top: - #{$grid-unit-40}; // Offset the container padding - background-color: $white; - box-shadow: inset 0 -#{$border-width} 0 $gray-200; + top: -1px; + background-color: lighten($gray-100, 4%); + box-shadow: inset 0 -#{$border-width} 0 $gray-100; + border-top: 1px solid $gray-100; + padding-top: $grid-unit-05; + padding-bottom: $grid-unit-05; } } } @@ -69,6 +90,7 @@ .dataviews-grid-view { margin-bottom: $grid-unit-30; grid-template-columns: repeat(2, minmax(0, 1fr)) !important; + padding: 0 $grid-unit-40; @include break-xlarge() { grid-template-columns: repeat(3, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency @@ -128,9 +150,14 @@ } .dataviews-list-view { + margin: 0; + li { border-bottom: $border-width solid $gray-100; margin: 0; + &:first-child { + border-top: $border-width solid $gray-100; + } &:last-child { border-bottom: 0; } @@ -210,3 +237,8 @@ .dataviews-action-modal { z-index: z-index(".dataviews-action-modal"); } + +.dataviews-no-results, +.dataviews-loading { + padding: 0 $grid-unit-40; +} diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 0e51bd85cdee48..209b9e443dc2a2 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -122,6 +122,7 @@ function HeaderMenu( { dataView, header } ) { iconPosition="right" text={ text } style={ { padding: 0 } } + size="compact" /> } > @@ -543,7 +544,11 @@ function ViewTable( { const hasRows = !! rows?.length; if ( isLoading ) { // TODO:Add spinner or progress bar.. - return <h3>{ __( 'Loading' ) }</h3>; + return ( + <div className="dataviews-loading"> + <h3>{ __( 'Loading' ) }</h3> + </div> + ); } const sortValues = { asc: 'ascending', desc: 'descending' }; @@ -615,7 +620,11 @@ function ViewTable( { </tbody> </table> ) } - { ! hasRows && <p>{ __( 'no results' ) }</p> } + { ! hasRows && ( + <div className="dataviews-no-results"> + <p>{ __( 'No results' ) }</p> + </div> + ) } </div> ); } diff --git a/packages/edit-site/src/components/page-pages/style.scss b/packages/edit-site/src/components/page-pages/style.scss index fde960ca1a72ca..933fdadb8d070e 100644 --- a/packages/edit-site/src/components/page-pages/style.scss +++ b/packages/edit-site/src/components/page-pages/style.scss @@ -1,3 +1,5 @@ .edit-site-page-pages__featured-image { - border-radius: $radius-block-ui; + border-radius: $grid-unit-05; + width: $grid-unit-40; + height: $grid-unit-40; } diff --git a/packages/edit-site/src/components/page/header.js b/packages/edit-site/src/components/page/header.js index 06de80c25685bd..274fd395a16f1d 100644 --- a/packages/edit-site/src/components/page/header.js +++ b/packages/edit-site/src/components/page/header.js @@ -19,7 +19,8 @@ export default function Header( { title, subTitle, actions } ) { <FlexBlock className="edit-site-page-header__page-title"> <Heading as="h2" - level={ 4 } + level={ 3 } + weight={ 500 } className="edit-site-page-header__title" > { title } diff --git a/packages/edit-site/src/components/page/style.scss b/packages/edit-site/src/components/page/style.scss index 8da7df8e0385b8..72ecbb4e2b7d77 100644 --- a/packages/edit-site/src/components/page/style.scss +++ b/packages/edit-site/src/components/page/style.scss @@ -12,8 +12,8 @@ } .edit-site-page-header { - padding: 0 $grid-unit-40; - min-height: $header-height; + padding: $grid-unit-20 $grid-unit-40; + min-height: $grid-unit * 9; border-bottom: 1px solid $gray-100; background: $white; position: sticky; From c97e586e1025a68b026552e8b2fff7ed476351f3 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 7 Dec 2023 15:32:29 +0100 Subject: [PATCH 077/325] Editor: Move the device type state to the editor package (#56866) --- .../reference-guides/data/data-core-editor.md | 24 +++++++++++++++++++ .../src/components/preview-options/README.md | 2 +- .../components/use-resize-canvas/README.md | 4 ++-- .../src/components/device-preview/index.js | 8 +++---- .../src/components/visual-editor/index.js | 8 +++---- packages/edit-post/src/editor.native.js | 13 ++++------ packages/edit-post/src/store/actions.js | 21 +++++++++++----- packages/edit-post/src/store/reducer.js | 18 -------------- packages/edit-post/src/store/selectors.js | 18 +++++++++++--- .../components/block-editor/editor-canvas.js | 16 +++++++------ .../header-edit-mode/document-tools/index.js | 22 +++++++---------- .../src/components/header-edit-mode/index.js | 20 ++++++---------- .../src/components/site-hub/index.js | 9 ++++--- packages/edit-site/src/store/actions.js | 20 ++++++++++++---- packages/edit-site/src/store/reducer.js | 18 -------------- packages/edit-site/src/store/selectors.js | 18 +++++++++++--- packages/editor/src/store/actions.js | 14 +++++++++++ packages/editor/src/store/reducer.js | 18 ++++++++++++++ packages/editor/src/store/selectors.js | 11 +++++++++ 19 files changed, 171 insertions(+), 111 deletions(-) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index fae5b8a78e2cfc..7b5a6dbeeb909c 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -268,6 +268,18 @@ _Returns_ - `string?`: Template ID. +### getDeviceType + +Returns the current editing canvas device type. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `string`: Device type. + ### getEditedPostAttribute Returns a single attribute of the post being edited, preferring the unsaved edit if one exists, but falling back to the attribute for the last known saved state of the post. @@ -1261,6 +1273,18 @@ _Related_ - selectBlock in core/block-editor store. +### setDeviceType + +Action that changes the width of the editing canvas. + +_Parameters_ + +- _deviceType_ `string`: + +_Returns_ + +- `Object`: Action object. + ### setRenderingMode Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: diff --git a/packages/block-editor/src/components/preview-options/README.md b/packages/block-editor/src/components/preview-options/README.md index baf886c71bd65b..80182f18d243d7 100644 --- a/packages/block-editor/src/components/preview-options/README.md +++ b/packages/block-editor/src/components/preview-options/README.md @@ -27,7 +27,7 @@ const MyPreviewOptions = () => ( isEnabled={ true } className="edit-post-post-preview-dropdown" deviceType={ deviceType } - setDeviceType={ setPreviewDeviceType } + setDeviceType={ setDeviceType } > { ( { onClose } ) => ( <MenuGroup> <div className="edit-post-header-preview__grouping-external"> diff --git a/packages/block-editor/src/components/use-resize-canvas/README.md b/packages/block-editor/src/components/use-resize-canvas/README.md index 18d28df7b3e3af..51e583f8def474 100644 --- a/packages/block-editor/src/components/use-resize-canvas/README.md +++ b/packages/block-editor/src/components/use-resize-canvas/README.md @@ -14,14 +14,14 @@ Note that this is currently experimental, and is available as `__experimentalUse ### Usage -The hook returns a style object which can be applied to a container. It is passed the current device type, which can be obtained from `__experimentalGetPreviewDeviceType`. +The hook returns a style object which can be applied to a container. It is passed the current device type, which can be obtained from `getDeviceType`. ```jsx import { __experimentalUseResizeCanvas as useResizeCanvas } from '@wordpress/block-editor'; function ResizedContainer() { const deviceType = useSelect( ( select ) => { - return select( 'core/edit-post' ).__experimentalGetPreviewDeviceType(); + return select( 'core/editor' ).getDeviceType(); }, [] ); const inlineStyles = useResizeCanvas( deviceType ); diff --git a/packages/edit-post/src/components/device-preview/index.js b/packages/edit-post/src/components/device-preview/index.js index a10688d1850237..9fc95b943609d5 100644 --- a/packages/edit-post/src/components/device-preview/index.js +++ b/packages/edit-post/src/components/device-preview/index.js @@ -30,21 +30,19 @@ export default function DevicePreview() { hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), isPostSaveable: select( editorStore ).isEditedPostSaveable(), isViewable: postType?.viewable ?? false, - deviceType: - select( editPostStore ).__experimentalGetPreviewDeviceType(), + deviceType: select( editorStore ).getDeviceType(), showIconLabels: select( editPostStore ).isFeatureActive( 'showIconLabels' ), }; }, [] ); - const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = - useDispatch( editPostStore ); + const { setDeviceType } = useDispatch( editorStore ); return ( <PreviewOptions isEnabled={ isPostSaveable } className="edit-post-post-preview-dropdown" deviceType={ deviceType } - setDeviceType={ setPreviewDeviceType } + setDeviceType={ setDeviceType } label={ __( 'Preview' ) } showIconLabels={ showIconLabels } > diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 9b975414510c52..5d309947a37c2c 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -37,14 +37,14 @@ export default function VisualEditor( { styles } ) { isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { - const { isFeatureActive, __experimentalGetPreviewDeviceType } = - select( editPostStore ); - const { getEditorSettings, getRenderingMode } = select( editorStore ); + const { isFeatureActive } = select( editPostStore ); + const { getEditorSettings, getRenderingMode, getDeviceType } = + select( editorStore ); const { getBlockTypes } = select( blocksStore ); const editorSettings = getEditorSettings(); return { - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), renderingMode: getRenderingMode(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index b031601186c72f..49b6022d6c05b1 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -9,7 +9,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { EditorProvider } from '@wordpress/editor'; +import { EditorProvider, store as editorStore } from '@wordpress/editor'; import { parse, serialize, store as blocksStore } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -192,18 +192,15 @@ class Editor extends Component { export default compose( [ withSelect( ( select ) => { - const { - isFeatureActive, - getEditorMode, - __experimentalGetPreviewDeviceType, - getHiddenBlockTypes, - } = select( editPostStore ); + const { isFeatureActive, getEditorMode, getHiddenBlockTypes } = + select( editPostStore ); const { getBlockTypes } = select( blocksStore ); + const { getDeviceType } = select( editorStore ); return { hasFixedToolbar: isFeatureActive( 'fixedToolbar' ) || - __experimentalGetPreviewDeviceType() !== 'Desktop', + getDeviceType() !== 'Desktop', focusMode: isFeatureActive( 'focusMode' ), mode: getEditorMode(), hiddenBlockTypes: getHiddenBlockTypes(), diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 2659b7ad333981..eae1030fad0248 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -456,18 +456,27 @@ export function metaBoxUpdatesFailure() { } /** - * Returns an action object used to toggle the width of the editing canvas. + * Action that changes the width of the editing canvas. + * + * @deprecated * * @param {string} deviceType * * @return {Object} Action object. */ -export function __experimentalSetPreviewDeviceType( deviceType ) { - return { - type: 'SET_PREVIEW_DEVICE_TYPE', - deviceType, +export const __experimentalSetPreviewDeviceType = + ( deviceType ) => + ( { registry } ) => { + deprecated( + "dispatch( 'core/edit-post' ).__experimentalSetPreviewDeviceType", + { + since: '6.5', + version: '6.7', + hint: 'registry.dispatch( editorStore ).setDeviceType', + } + ); + registry.dispatch( editorStore ).setDeviceType( deviceType ); }; -} /** * Returns an action object used to open/close the inserter. diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 4748c4fff49723..1072919d388db4 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -98,23 +98,6 @@ export function metaBoxLocations( state = {}, action ) { return state; } -/** - * Reducer returning the editing canvas device type. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function deviceType( state = 'Desktop', action ) { - switch ( action.type ) { - case 'SET_PREVIEW_DEVICE_TYPE': - return action.deviceType; - } - - return state; -} - /** * Reducer to set the block inserter panel open or closed. * @@ -179,7 +162,6 @@ export default combineReducers( { metaBoxes, publishSidebarActive, removedPanels, - deviceType, blockInserterPanel, listViewPanel, } ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index f7b8f91d380dc0..115dcd9bcd78e7 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -450,13 +450,25 @@ export function isSavingMetaBoxes( state ) { /** * Returns the current editing canvas device type. * + * @deprecated + * * @param {Object} state Global application state. * * @return {string} Device type. */ -export function __experimentalGetPreviewDeviceType( state ) { - return state.deviceType; -} +export const __experimentalGetPreviewDeviceType = createRegistrySelector( + ( select ) => () => { + deprecated( + `select( 'core/edit-site' ).__experimentalGetPreviewDeviceType`, + { + since: '6.5', + version: '6.7', + alternative: `select( 'core/editor' ).getDeviceType`, + } + ); + return select( editorStore ).getDeviceType(); + } +); /** * Returns true if the inserter is opened. diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index fa20a4abae1ad2..bf40c655b4477d 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -14,7 +14,10 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { useState, useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { privateApis as editorPrivateApis } from '@wordpress/editor'; +import { + privateApis as editorPrivateApis, + store as editorStore, +} from '@wordpress/editor'; /** * Internal dependencies @@ -45,17 +48,16 @@ function EditorCanvas( { } = useSelect( ( select ) => { const { getBlockCount, __unstableGetEditorMode } = select( blockEditorStore ); - const { - getEditedPostType, - __experimentalGetPreviewDeviceType, - getCanvasMode, - } = unlock( select( editSiteStore ) ); + const { getEditedPostType, getCanvasMode } = unlock( + select( editSiteStore ) + ); + const { getDeviceType } = select( editorStore ); const _templateType = getEditedPostType(); return { templateType: _templateType, isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', canvasMode: getCanvasMode(), hasBlocks: !! getBlockCount(), diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index f0231e1e621169..eefc6668b5b95d 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -14,6 +14,7 @@ import { _x, __ } from '@wordpress/i18n'; import { listView, plus, chevronUpDown } from '@wordpress/icons'; import { Button, ToolbarItem } from '@wordpress/components'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -39,18 +40,15 @@ export default function DocumentTools( { const inserterButton = useRef(); const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } = useSelect( ( select ) => { - const { - __experimentalGetPreviewDeviceType, - isInserterOpened, - isListViewOpened, - getEditorMode, - } = select( editSiteStore ); + const { isInserterOpened, isListViewOpened, getEditorMode } = + select( editSiteStore ); + const { getDeviceType } = select( editorStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); return { - deviceType: __experimentalGetPreviewDeviceType(), + deviceType: getDeviceType(), isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( @@ -60,12 +58,10 @@ export default function DocumentTools( { }; }, [] ); - const { - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - setIsInserterOpened, - setIsListViewOpened, - } = useDispatch( editSiteStore ); + const { setIsInserterOpened, setIsListViewOpened } = + useDispatch( editSiteStore ); const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); + const { setDeviceType } = useDispatch( editorStore ); const isLargeViewport = useViewportMatch( 'medium' ); @@ -189,7 +185,7 @@ export default function DocumentTools( { /* translators: button label text should, if possible, be under 16 characters. */ label={ __( 'Zoom-out View' ) } onClick={ () => { - setPreviewDeviceType( 'Desktop' ); + setDeviceType( 'Desktop' ); __unstableSetEditorMode( isZoomedOutView ? 'edit' diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index f8a9d9d4e892bd..c6dbe4b6b91449 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -27,7 +27,7 @@ import { VisuallyHidden, } from '@wordpress/components'; import { store as preferencesStore } from '@wordpress/preferences'; -import { DocumentBar } from '@wordpress/editor'; +import { DocumentBar, store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -58,22 +58,18 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { hasFixedToolbar, isZoomOutMode, } = useSelect( ( select ) => { - const { __experimentalGetPreviewDeviceType, getEditedPostType } = - select( editSiteStore ); + const { getEditedPostType } = select( editSiteStore ); const { getBlockSelectionStart, __unstableGetEditorMode } = select( blockEditorStore ); - - const postType = getEditedPostType(); - const { getUnstableBase, // Site index. } = select( coreStore ); - const { get: getPreference } = select( preferencesStore ); + const { getDeviceType } = select( editorStore ); return { - deviceType: __experimentalGetPreviewDeviceType(), - templateType: postType, + deviceType: getDeviceType(), + templateType: getEditedPostType(), blockEditorMode: __unstableGetEditorMode(), blockSelectionStart: getBlockSelectionStart(), homeUrl: getUnstableBase()?.home, @@ -99,9 +95,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { const isLargeViewport = useViewportMatch( 'medium' ); const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; const blockToolbarRef = useRef(); - - const { __experimentalSetPreviewDeviceType: setPreviewDeviceType } = - useDispatch( editSiteStore ); + const { setDeviceType } = useDispatch( editorStore ); const disableMotion = useReducedMotion(); const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); @@ -225,7 +219,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { > <PreviewOptions deviceType={ deviceType } - setDeviceType={ setPreviewDeviceType } + setDeviceType={ setDeviceType } label={ __( 'View' ) } isEnabled={ ! isFocusMode && hasDefaultEditorCanvasView diff --git a/packages/edit-site/src/components/site-hub/index.js b/packages/edit-site/src/components/site-hub/index.js index 9d63001c185c32..7af0d53090c578 100644 --- a/packages/edit-site/src/components/site-hub/index.js +++ b/packages/edit-site/src/components/site-hub/index.js @@ -17,6 +17,7 @@ import { useReducedMotion } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; +import { store as editorStore } from '@wordpress/editor'; import { decodeEntities } from '@wordpress/html-entities'; import { memo } from '@wordpress/element'; import { search, external } from '@wordpress/icons'; @@ -57,11 +58,9 @@ const SiteHub = memo( ( { isTransparent, className } ) => { const { open: openCommandCenter } = useDispatch( commandsStore ); const disableMotion = useReducedMotion(); - const { - setCanvasMode, - __experimentalSetPreviewDeviceType: setPreviewDeviceType, - } = unlock( useDispatch( editSiteStore ) ); + const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); const { clearSelectedBlock } = useDispatch( blockEditorStore ); + const { setDeviceType } = useDispatch( editorStore ); const isBackToDashboardButton = canvasMode === 'view'; const siteIconButtonProps = isBackToDashboardButton ? { @@ -76,7 +75,7 @@ const SiteHub = memo( ( { isTransparent, className } ) => { event.preventDefault(); if ( canvasMode === 'edit' ) { clearSelectedBlock(); - setPreviewDeviceType( 'Desktop' ); + setDeviceType( 'Desktop' ); setCanvasMode( 'view' ); } }, diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 2dd7aacd384014..6397a31af120b3 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -10,6 +10,7 @@ import { store as noticesStore } from '@wordpress/notices'; import { store as coreStore } from '@wordpress/core-data'; import { store as interfaceStore } from '@wordpress/interface'; import { store as blockEditorStore } from '@wordpress/block-editor'; +import { store as editorStore } from '@wordpress/editor'; import { speak } from '@wordpress/a11y'; import { store as preferencesStore } from '@wordpress/preferences'; import { decodeEntities } from '@wordpress/html-entities'; @@ -49,16 +50,25 @@ export function toggleFeature( featureName ) { /** * Action that changes the width of the editing canvas. * + * @deprecated + * * @param {string} deviceType * * @return {Object} Action object. */ -export function __experimentalSetPreviewDeviceType( deviceType ) { - return { - type: 'SET_PREVIEW_DEVICE_TYPE', - deviceType, +export const __experimentalSetPreviewDeviceType = + ( deviceType ) => + ( { registry } ) => { + deprecated( + "dispatch( 'core/edit-site' ).__experimentalSetPreviewDeviceType", + { + since: '6.5', + version: '6.7', + hint: 'registry.dispatch( editorStore ).setDeviceType', + } + ); + registry.dispatch( editorStore ).setDeviceType( deviceType ); }; -} /** * Action that sets a template, optionally fetching it from REST API. diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index a46d215f905074..b55acbffd626e6 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -3,23 +3,6 @@ */ import { combineReducers } from '@wordpress/data'; -/** - * Reducer returning the editing canvas device type. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - * - * @return {Object} Updated state. - */ -export function deviceType( state = 'Desktop', action ) { - switch ( action.type ) { - case 'SET_PREVIEW_DEVICE_TYPE': - return action.deviceType; - } - - return state; -} - /** * Reducer returning the settings. * @@ -158,7 +141,6 @@ function editorCanvasContainerView( state = undefined, action ) { } export default combineReducers( { - deviceType, settings, editedPost, blockInserterPanel, diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index 9d00e141270c40..ebaee12dfdc5e4 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -44,13 +44,25 @@ export const isFeatureActive = createRegistrySelector( /** * Returns the current editing canvas device type. * + * @deprecated + * * @param {Object} state Global application state. * * @return {string} Device type. */ -export function __experimentalGetPreviewDeviceType( state ) { - return state.deviceType; -} +export const __experimentalGetPreviewDeviceType = createRegistrySelector( + ( select ) => () => { + deprecated( + `select( 'core/edit-site' ).__experimentalGetPreviewDeviceType`, + { + since: '6.5', + version: '6.7', + alternative: `select( 'core/editor' ).getDeviceType`, + } + ); + return select( editorStore ).getDeviceType(); + } +); /** * Returns whether the current user can create media or not. diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 4c1170b064202f..5d8335382b5dbc 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -573,6 +573,20 @@ export const setRenderingMode = } ); }; +/** + * Action that changes the width of the editing canvas. + * + * @param {string} deviceType + * + * @return {Object} Action object. + */ +export function setDeviceType( deviceType ) { + return { + type: 'SET_DEVICE_TYPE', + deviceType, + }; +} + /** * Backward compatibility */ diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 7821baf5cdc062..23fa84edba0320 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -297,6 +297,23 @@ export function renderingMode( state = 'all', action ) { return state; } +/** + * Reducer returning the editing canvas device type. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export function deviceType( state = 'Desktop', action ) { + switch ( action.type ) { + case 'SET_DEVICE_TYPE': + return action.deviceType; + } + + return state; +} + export default combineReducers( { postId, postType, @@ -310,4 +327,5 @@ export default combineReducers( { editorSettings, postAutosavingLock, renderingMode, + deviceType, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index c47a80e96735f5..0b82bb797584eb 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1210,6 +1210,17 @@ export function getRenderingMode( state ) { return state.renderingMode; } +/** + * Returns the current editing canvas device type. + * + * @param {Object} state Global application state. + * + * @return {string} Device type. + */ +export function getDeviceType( state ) { + return state.deviceType; +} + /* * Backward compatibility */ From be7d5189651d9b966e925f98277a46190943b059 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Thu, 7 Dec 2023 15:42:31 +0000 Subject: [PATCH 078/325] Update changelog files --- packages/create-block-interactive-template/CHANGELOG.md | 2 ++ packages/create-block-interactive-template/package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index fbe3a8c8c857c8..72ed6677e0b4ed 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.10.1 (2023-12-07) + - Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) ## 1.10.0 (2023-11-29) diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 48723b3b756d12..7da853a5328525 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.10.0", + "version": "1.10.1-prerelease", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From eae6a91b16363f0db48938c54bf1555ff7dbe40c Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Thu, 7 Dec 2023 15:44:15 +0000 Subject: [PATCH 079/325] chore(release): publish - @wordpress/block-directory@4.24.1 - @wordpress/block-library@8.24.1 - @wordpress/create-block-interactive-template@1.10.1 - @wordpress/customize-widgets@4.24.1 - @wordpress/e2e-tests@7.18.1 - @wordpress/edit-post@7.24.1 - @wordpress/edit-site@5.24.1 - @wordpress/edit-widgets@5.24.1 - @wordpress/editor@13.24.1 - @wordpress/interactivity@3.0.1 --- package-lock.json | 18 +++++++++--------- packages/block-directory/package.json | 2 +- packages/block-library/package.json | 2 +- .../package.json | 2 +- packages/customize-widgets/package.json | 2 +- packages/e2e-tests/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/edit-site/package.json | 2 +- packages/edit-widgets/package.json | 2 +- packages/editor/package.json | 2 +- packages/interactivity/package.json | 2 +- 11 files changed, 19 insertions(+), 19 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b27980b6b40fd..e59b7b849ebd92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54440,7 +54440,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.24.0", + "version": "4.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54572,7 +54572,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.24.0", + "version": "8.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54973,7 +54973,7 @@ }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.24.0", + "version": "4.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55208,7 +55208,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.18.0", + "version": "7.18.1", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55247,7 +55247,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.24.0", + "version": "7.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55295,7 +55295,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.24.0", + "version": "5.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55360,7 +55360,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.24.0", + "version": "5.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55402,7 +55402,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.24.0", + "version": "13.24.1", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55739,7 +55739,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "3.0.0", + "version": "3.0.1", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.1.3", diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 147f58dd719e13..04bcd1c50d8b33 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.24.0", + "version": "4.24.1", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index f989e586ec7b83..bcb7a843b3232d 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.24.0", + "version": "8.24.1", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 7da853a5328525..3bc6b1f646c265 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.10.1-prerelease", + "version": "1.10.1", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 65a64d7826a7f7..14aff02afb0167 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.24.0", + "version": "4.24.1", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 7327336b19b9b3..103daf0498b539 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "7.18.0", + "version": "7.18.1", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 0bc4376cedec94..eea3306a2665ff 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.24.0", + "version": "7.24.1", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 92e9fd8bebe59a..eba0a06012da78 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.24.0", + "version": "5.24.1", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index eb318c962c1c3f..a983c1893ed127 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.24.0", + "version": "5.24.1", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 81d24961a775ec..70344e9dc3e72d 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.24.0", + "version": "13.24.1", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index da118762e3c02a..bf8576fd67ae73 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "3.0.0", + "version": "3.0.1", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From ee9baf2baa00d7e762c5baab863e5436ebd3e2ec Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Thu, 7 Dec 2023 11:01:19 -0500 Subject: [PATCH 080/325] Tabs: Implement `ariakit/test` in unit tests (#56835) --- packages/components/src/tabs/test/index.tsx | 127 +++++++------------- 1 file changed, 46 insertions(+), 81 deletions(-) diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index fac8127c4cc0d8..f923dc455fd7bd 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -2,7 +2,7 @@ * External dependencies */ import { render, screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import { press, click } from '@ariakit/test'; /** * WordPress dependencies @@ -184,26 +184,22 @@ describe( 'Tabs', () => { } ); describe( 'Focus Behavior', () => { it( 'should focus on the related TabPanel when pressing the Tab key', async () => { - const user = userEvent.setup(); - render( <UncontrolledTabs tabs={ TABS } /> ); const selectedTabPanel = await screen.findByRole( 'tabpanel' ); // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); // By default the tabpanel should receive focus - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( selectedTabPanel ).toHaveFocus(); } ); it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { - const user = userEvent.setup(); - const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => tabObj.id === 'alpha' ? { @@ -229,13 +225,13 @@ describe( 'Tabs', () => { // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); // Because the alpha tabpanel is set to `focusable: false`, pressing // the Tab key should focus the button, not the tabpanel - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( alphaButton ).toHaveFocus(); } ); } ); @@ -258,7 +254,6 @@ describe( 'Tabs', () => { describe( 'Tab Activation', () => { it( 'defaults to automatic tab activation (pointer clicks)', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -273,7 +268,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); // Click on Beta, make sure beta is the selected tab - await user.click( screen.getByRole( 'tab', { name: 'Beta' } ) ); + await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( @@ -282,7 +277,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); // Click on Alpha, make sure beta is the selected tab - await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( @@ -292,7 +287,6 @@ describe( 'Tabs', () => { } ); it( 'defaults to automatic tab activation (arrow keys)', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -307,12 +301,12 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Navigate forward with arrow keys and make sure the Beta tab is // selected automatically. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -320,7 +314,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -328,7 +322,6 @@ describe( 'Tabs', () => { } ); it( 'wraps around the last/first tab when using arrow keys', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -341,12 +334,12 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure Alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Navigate backwards with arrow keys and make sure that the Gamma tab // (the last tab) is selected automatically. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -354,7 +347,7 @@ describe( 'Tabs', () => { // Navigate forward with arrow keys. Make sure alpha (the first tab) is // selected automatically. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -362,7 +355,6 @@ describe( 'Tabs', () => { } ); it( 'should not move tab selection when pressing the up/down arrow keys, unless the orientation is changed to `vertical`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -377,18 +369,18 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Press the arrow up key, nothing happens. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); // Press the arrow down key, nothing happens - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); @@ -415,7 +407,7 @@ describe( 'Tabs', () => { // Navigate forward with arrow keys and make sure the Beta tab is // selected automatically. - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); @@ -423,7 +415,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -431,7 +423,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowUp]' ); + await press.ArrowUp(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 4 ); @@ -439,7 +431,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. Make sure alpha is // selected automatically. - await user.keyboard( '[ArrowDown]' ); + await press.ArrowDown(); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 5 ); @@ -447,7 +439,6 @@ describe( 'Tabs', () => { } ); it( 'should move focus on a tab even if disabled with arrow key, but not with pointer clicks', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => @@ -477,7 +468,7 @@ describe( 'Tabs', () => { // Tab to focus the tablist. Make sure Alpha is focused. expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( await getSelectedTab() ).not.toHaveFocus(); - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await getSelectedTab() ).toHaveFocus(); // Confirm onSelect has not been re-called expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); @@ -487,7 +478,9 @@ describe( 'Tabs', () => { // it was the tab that was last selected before delta. Therefore, the // `mockOnSelect` function gets called only twice (and not three times) // - it will receive focus, when using arrow keys - await user.keyboard( '[ArrowRight][ArrowRight][ArrowRight]' ); + await press.ArrowRight(); + await press.ArrowRight(); + await press.ArrowRight(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( screen.getByRole( 'tab', { name: 'Delta' } ) @@ -498,7 +491,7 @@ describe( 'Tabs', () => { // Navigate backwards with arrow keys. The gamma tab receives focus. // The `mockOnSelect` callback doesn't fire, since the gamma tab was // already selected. - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); @@ -506,37 +499,26 @@ describe( 'Tabs', () => { // Click on the disabled tab. Compared to using arrow keys to move the // focus, disabled tabs ignore pointer clicks — and therefore, they don't // receive focus, nor they cause the `mockOnSelect` function to fire. - await user.click( screen.getByRole( 'tab', { name: 'Delta' } ) ); + await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); expect( await getSelectedTab() ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); } ); it( 'should not focus the next tab when the Tab key is pressed', async () => { - const user = userEvent.setup(); - render( <UncontrolledTabs tabs={ TABS } /> ); // Tab should initially focus the first tab in the tablist, which // is Alpha. - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); - // This assertion ensures the component has had time to fully - // render, preventing flakiness. - // see https://github.com/WordPress/gutenberg/pull/55950 - await waitFor( () => - expect( - screen.getByRole( 'tab', { name: 'Beta' } ) - ).toHaveAttribute( 'tabindex', '-1' ) - ); - // Because all other tabs should have `tabindex=-1`, pressing Tab // should NOT move the focus to the next tab, which is Beta. // Instead, focus should go to the currently selected tabpanel (alpha). - await user.keyboard( '[Tab]' ); + await press.Tab(); expect( await screen.findByRole( 'tabpanel', { name: 'Alpha', @@ -545,7 +527,6 @@ describe( 'Tabs', () => { } ); it( 'switches to manual tab activation when the `selectOnMove` prop is set to `false`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); render( @@ -563,7 +544,7 @@ describe( 'Tabs', () => { // Click on Alpha and make sure it is selected. // onSelect shouldn't fire since the selected tab didn't change. - await user.click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await screen.findByRole( 'tab', { name: 'Alpha' } ) ).toHaveFocus(); @@ -574,13 +555,13 @@ describe( 'Tabs', () => { // that the tab selection happens only when pressing the spacebar // or enter key. onSelect shouldn't fire since the selected tab // didn't change. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await screen.findByRole( 'tab', { name: 'Beta' } ) ).toHaveFocus(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); - await user.keyboard( '[Enter]' ); + await press.Enter(); expect( mockOnSelect ).toHaveBeenCalledTimes( 2 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'beta' ); @@ -588,7 +569,7 @@ describe( 'Tabs', () => { // focused, but that tab selection happens only when pressing the // spacebar or enter key. onSelect shouldn't fire since the selected // tab didn't change. - await user.keyboard( '[ArrowRight]' ); + await press.ArrowRight(); expect( await screen.findByRole( 'tab', { name: 'Gamma' } ) ).toHaveFocus(); @@ -597,7 +578,7 @@ describe( 'Tabs', () => { screen.getByRole( 'tab', { name: 'Gamma' } ) ).toHaveFocus(); - await user.keyboard( '[Space]' ); + await press.Space(); expect( mockOnSelect ).toHaveBeenCalledTimes( 3 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'gamma' ); } ); @@ -700,7 +681,6 @@ describe( 'Tabs', () => { } ); it( 'should fall back to the tab associated to `initialTabId` if the currently active tab is removed', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -713,9 +693,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - await user.click( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); @@ -731,7 +709,6 @@ describe( 'Tabs', () => { } ); it( 'should fall back to the tab associated to `initialTabId` if the currently active tab becomes disabled', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const { rerender } = render( @@ -744,9 +721,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); - await user.click( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ); + await click( screen.getByRole( 'tab', { name: 'Alpha' } ) ); expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); @@ -822,7 +797,6 @@ describe( 'Tabs', () => { describe( 'Disabled tab', () => { it( 'should disable the tab when `disabled` is `true`', async () => { - const user = userEvent.setup(); const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( @@ -853,20 +827,15 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); + // Move focus to the tablist, make sure alpha is focused. + await press.Tab(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + // onSelect should not be called since the disabled tab is // highlighted, but not selected. - await user.keyboard( '[Tab]' ); - - // This assertion ensures focus has time to move to the first - // tab before the test proceeds, preventing flakiness. - // see https://github.com/WordPress/gutenberg/pull/55950 - await waitFor( () => - expect( - screen.getByRole( 'tab', { name: 'Alpha' } ) - ).toHaveFocus() - ); - - await user.keyboard( '[ArrowLeft]' ); + await press.ArrowLeft(); expect( mockOnSelect ).toHaveBeenCalledTimes( 1 ); // Delta (which is disabled) has focus @@ -1067,14 +1036,10 @@ describe( 'Tabs', () => { /> ); - // No tab should be selected i.e. it doesn't fall back to first tab. - // `waitFor` is needed here to prevent testing library from - // throwing a 'not wrapped in `act()`' error. - await waitFor( () => - expect( - screen.queryByRole( 'tab', { selected: true } ) - ).not.toBeInTheDocument() - ); + expect( + screen.queryByRole( 'tab', { selected: true } ) + ).not.toBeInTheDocument(); + // No tabpanel should be rendered either expect( screen.queryByRole( 'tabpanel' ) ).not.toBeInTheDocument(); } ); From 9d13cf09f06dc8e4dadbcdb31265a0f49bd42f0e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 7 Dec 2023 17:40:07 +0100 Subject: [PATCH 081/325] Site Editor: Fix active edited post (#56863) --- .../reference-guides/data/data-core-editor.md | 21 +++++++--- .../src/components/document-bar/index.js | 8 ++-- .../editor/src/components/provider/index.js | 13 ++++--- packages/editor/src/store/actions.js | 38 +++++++++++++++---- packages/editor/src/store/reducer.js | 31 ++------------- packages/editor/src/store/reducer.native.js | 2 - packages/editor/src/store/selectors.js | 2 +- 7 files changed, 63 insertions(+), 52 deletions(-) diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 7b5a6dbeeb909c..f6086090f9b541 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -1285,6 +1285,19 @@ _Returns_ - `Object`: Action object. +### setEditedPost + +Returns an action that sets the current post Type and post ID. + +_Parameters_ + +- _postType_ `string`: Post Type. +- _postId_ `string`: Post ID. + +_Returns_ + +- `Object`: Action object. + ### setRenderingMode Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: @@ -1316,16 +1329,14 @@ _Parameters_ ### setupEditorState -Returns an action object used to setup the editor state when first opening an editor. +> **Deprecated** + +Setup the editor state. _Parameters_ - _post_ `Object`: Post object. -_Returns_ - -- `Object`: Action object. - ### showInsertionPoint _Related_ diff --git a/packages/editor/src/components/document-bar/index.js b/packages/editor/src/components/document-bar/index.js index ffb2be33074563..da43533bfa5bc2 100644 --- a/packages/editor/src/components/document-bar/index.js +++ b/packages/editor/src/components/document-bar/index.js @@ -88,7 +88,7 @@ export default function DocumentBar() { function BaseDocumentActions( { postType, postId, onBack } ) { const { open: openCommandCenter } = useDispatch( commandsStore ); - const { editedRecord: document, isResolving } = useEntityRecord( + const { editedRecord: doc, isResolving } = useEntityRecord( 'postType', postType, postId @@ -96,13 +96,13 @@ function BaseDocumentActions( { postType, postId, onBack } ) { const { templateIcon, templateTitle } = useSelect( ( select ) => { const { __experimentalGetTemplateInfo: getTemplateInfo } = select( editorStore ); - const templateInfo = getTemplateInfo( document ); + const templateInfo = getTemplateInfo( doc ); return { templateIcon: templateInfo.icon, templateTitle: templateInfo.title, }; } ); - const isNotFound = ! document && ! isResolving; + const isNotFound = ! doc && ! isResolving; const icon = icons[ postType ] ?? pageIcon; const [ isAnimated, setIsAnimated ] = useState( false ); const isMounting = useRef( true ); @@ -123,7 +123,7 @@ function BaseDocumentActions( { postType, postId, onBack } ) { isMounting.current = false; }, [ postType, postId ] ); - const title = isTemplate ? templateTitle : document.title; + const title = isTemplate ? templateTitle : doc.title; return ( <div diff --git a/packages/editor/src/components/provider/index.js b/packages/editor/src/components/provider/index.js index 714e05e5385bc2..fc49339c2c13ff 100644 --- a/packages/editor/src/components/provider/index.js +++ b/packages/editor/src/components/provider/index.js @@ -164,13 +164,12 @@ export const ExperimentalEditorProvider = withRegistryProvider( updatePostLock, setupEditor, updateEditorSettings, - __experimentalTearDownEditor, setCurrentTemplateId, + setEditedPost, setRenderingMode, } = unlock( useDispatch( editorStore ) ); const { createWarningNotice } = useDispatch( noticesStore ); - // Initialize and tear down the editor. // Ideally this should be synced on each change and not just something you do once. useLayoutEffect( () => { // Assume that we don't need to initialize in the case of an error recovery. @@ -196,17 +195,19 @@ export const ExperimentalEditorProvider = withRegistryProvider( } ); } - - return () => { - __experimentalTearDownEditor(); - }; }, [] ); + // Synchronizes the active post with the state + useEffect( () => { + setEditedPost( post.type, post.id ); + }, [ post.type, post.id ] ); + // Synchronize the editor settings as they change. useEffect( () => { updateEditorSettings( settings ); }, [ settings, updateEditorSettings ] ); + // Synchronizes the active template with the state. useEffect( () => { setCurrentTemplateId( template?.id ); }, [ template?.id, setCurrentTemplateId ] ); diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 5d8335382b5dbc..8fe0822e6a016c 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -36,7 +36,7 @@ import { export const setupEditor = ( post, edits, template ) => ( { dispatch } ) => { - dispatch.setupEditorState( post ); + dispatch.setEditedPost( post.type, post.id ); // Apply a template for new posts only, if exists. const isNewPost = post.status === 'auto-draft'; if ( isNewPost && template ) { @@ -70,10 +70,18 @@ export const setupEditor = * Returns an action object signalling that the editor is being destroyed and * that any necessary state or side-effect cleanup should occur. * + * @deprecated + * * @return {Object} Action object. */ export function __experimentalTearDownEditor() { - return { type: 'TEAR_DOWN_EDITOR' }; + deprecated( + "wp.data.dispatch( 'core/editor' ).__experimentalTearDownEditor", + { + since: '6.5', + } + ); + return { type: 'DO_NOTHING' }; } /** @@ -109,17 +117,33 @@ export function updatePost() { } /** - * Returns an action object used to setup the editor state when first opening - * an editor. + * Setup the editor state. + * + * @deprecated * * @param {Object} post Post object. + */ +export function setupEditorState( post ) { + deprecated( "wp.data.dispatch( 'core/editor' ).setupEditorState", { + since: '6.5', + alternative: "wp.data.dispatch( 'core/editor' ).setEditedPost", + } ); + return setEditedPost( post.type, post.id ); +} + +/** + * Returns an action that sets the current post Type and post ID. + * + * @param {string} postType Post Type. + * @param {string} postId Post ID. * * @return {Object} Action object. */ -export function setupEditorState( post ) { +export function setEditedPost( postType, postId ) { return { - type: 'SETUP_EDITOR_STATE', - post, + type: 'SET_EDITED_POST', + postType, + postId, }; } diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 23fa84edba0320..a4323b59679561 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -83,8 +83,8 @@ export function shouldOverwriteState( action, previousAction ) { export function postId( state = null, action ) { switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return action.post.id; + case 'SET_EDITED_POST': + return action.postId; } return state; @@ -101,8 +101,8 @@ export function templateId( state = null, action ) { export function postType( state = null, action ) { switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return action.post.type; + case 'SET_EDITED_POST': + return action.postType; } return state; @@ -246,28 +246,6 @@ export function postAutosavingLock( state = {}, action ) { return state; } -/** - * Reducer returning whether the editor is ready to be rendered. - * The editor is considered ready to be rendered once - * the post object is loaded properly and the initial blocks parsed. - * - * @param {boolean} state - * @param {Object} action - * - * @return {boolean} Updated state. - */ -export function isReady( state = false, action ) { - switch ( action.type ) { - case 'SETUP_EDITOR_STATE': - return true; - - case 'TEAR_DOWN_EDITOR': - return false; - } - - return state; -} - /** * Reducer returning the post editor setting. * @@ -323,7 +301,6 @@ export default combineReducers( { postLock, template, postSavingLock, - isReady, editorSettings, postAutosavingLock, renderingMode, diff --git a/packages/editor/src/store/reducer.native.js b/packages/editor/src/store/reducer.native.js index 991addd88620b7..7566dfc5dfd038 100644 --- a/packages/editor/src/store/reducer.native.js +++ b/packages/editor/src/store/reducer.native.js @@ -13,7 +13,6 @@ import { postLock, postSavingLock, template, - isReady, editorSettings, } from './reducer.js'; @@ -87,7 +86,6 @@ export default combineReducers( { postLock, postSavingLock, template, - isReady, editorSettings, clipboard, notices, diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 0b82bb797584eb..3b3f3158124300 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1185,7 +1185,7 @@ export function getEditorSelection( state ) { * @return {boolean} is Ready. */ export function __unstableIsEditorReady( state ) { - return state.isReady; + return !! state.postId; } /** From 0fa652ac4f078ef11a83f3383fce8e3288f2a798 Mon Sep 17 00:00:00 2001 From: Damon Cook <colorful-tones@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:59:47 -0500 Subject: [PATCH 082/325] Update InnerBlocks defaultblock doc usage (#56728) Co-authored-by: Riad Benguella <benguella@gmail.com> --- .../src/components/inner-blocks/README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/inner-blocks/README.md b/packages/block-editor/src/components/inner-blocks/README.md index 0f5d303b8c7917..92d4fdb5739cec 100644 --- a/packages/block-editor/src/components/inner-blocks/README.md +++ b/packages/block-editor/src/components/inner-blocks/README.md @@ -188,8 +188,19 @@ For example, a button block, deeply nested in several levels of block `X` that u ### `defaultBlock` -- **Type:** `Array` -- **Default:** - `undefined`. Determines which block type should be inserted by default and any attributes that should be set by default when the block is inserted. Takes an array in the form of `[ blockname, {blockAttributes} ]`. +- **Type:** `Object` +- **Default:** - `undefined` + +Determines which block type should be inserted by default and any attributes that should be set by default when the block is inserted. Takes an object in the form of `{ name: blockname, attributes: {blockAttributes} }`. + +```jsx +const DEFAULT_BLOCK = { name: 'core/paragraph', attributes: { content: 'Lorem ipsum...' } }; +... +<InnerBlocks + defaultBlock={ DEFAULT_BLOCK } + directInsert={ true } +/> +``` ### `directInsert` From a9cbc06d5e55ebf1b5255edd832809bd684b2c9f Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 20:44:24 +0200 Subject: [PATCH 083/325] One hook to rule them all: preparation for a block supports API (#56862) --- .../src/components/block-controls/hook.js | 30 +------ .../components/block-info-slot-fill/index.js | 2 +- .../src/components/inspector-controls/fill.js | 2 +- .../inspector-controls/fill.native.js | 2 +- .../use-display-block-controls/index.js | 28 ++++--- .../index.native.js | 6 +- packages/block-editor/src/hooks/align.js | 54 +++---------- .../block-editor/src/hooks/align.native.js | 1 + packages/block-editor/src/hooks/anchor.js | 51 +++--------- .../block-editor/src/hooks/block-hooks.js | 78 ++++++------------- .../block-editor/src/hooks/block-renaming.js | 45 ++--------- .../src/hooks/custom-class-name.js | 51 ++---------- .../block-editor/src/hooks/custom-fields.js | 60 +++----------- packages/block-editor/src/hooks/duotone.js | 61 +++------------ packages/block-editor/src/hooks/index.js | 36 ++++++--- .../block-editor/src/hooks/index.native.js | 9 ++- packages/block-editor/src/hooks/layout.js | 50 +++--------- packages/block-editor/src/hooks/position.js | 58 +++----------- packages/block-editor/src/hooks/style.js | 43 ++-------- packages/block-editor/src/hooks/test/align.js | 72 +---------------- packages/block-editor/src/hooks/utils.js | 68 ++++++++++++++++ 21 files changed, 229 insertions(+), 578 deletions(-) diff --git a/packages/block-editor/src/components/block-controls/hook.js b/packages/block-editor/src/components/block-controls/hook.js index 18a38e245e58ab..e3f69c8bec3b25 100644 --- a/packages/block-editor/src/components/block-controls/hook.js +++ b/packages/block-editor/src/components/block-controls/hook.js @@ -1,45 +1,19 @@ /** * WordPress dependencies */ -import { store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import groups from './groups'; -import { store as blockEditorStore } from '../../store'; -import { useBlockEditContext } from '../block-edit/context'; import useDisplayBlockControls from '../use-display-block-controls'; export default function useBlockControlsFill( group, shareWithChildBlocks ) { - const isDisplayed = useDisplayBlockControls(); - const { clientId } = useBlockEditContext(); - const isParentDisplayed = useSelect( - ( select ) => { - if ( ! shareWithChildBlocks ) { - return false; - } - - const { getBlockName, hasSelectedInnerBlock } = - select( blockEditorStore ); - const { hasBlockSupport } = select( blocksStore ); - - return ( - hasBlockSupport( - getBlockName( clientId ), - '__experimentalExposeControlsToChildren', - false - ) && hasSelectedInnerBlock( clientId ) - ); - }, - [ shareWithChildBlocks, clientId ] - ); - + const { isDisplayed, isParentDisplayed } = useDisplayBlockControls(); if ( isDisplayed ) { return groups[ group ]?.Fill; } - if ( isParentDisplayed ) { + if ( isParentDisplayed && shareWithChildBlocks ) { return groups.parent.Fill; } return null; diff --git a/packages/block-editor/src/components/block-info-slot-fill/index.js b/packages/block-editor/src/components/block-info-slot-fill/index.js index db7919b6ef5eab..8e16757f3ebbc1 100644 --- a/packages/block-editor/src/components/block-info-slot-fill/index.js +++ b/packages/block-editor/src/components/block-info-slot-fill/index.js @@ -13,7 +13,7 @@ const { createPrivateSlotFill } = unlock( componentsPrivateApis ); const { Fill, Slot } = createPrivateSlotFill( 'BlockInformation' ); const BlockInfo = ( props ) => { - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); if ( ! isDisplayed ) { return null; } diff --git a/packages/block-editor/src/components/inspector-controls/fill.js b/packages/block-editor/src/components/inspector-controls/fill.js index f0640a9d31ddc5..fdb0d44f0eccb6 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.js +++ b/packages/block-editor/src/components/inspector-controls/fill.js @@ -33,7 +33,7 @@ export default function InspectorControlsFill( { group = __experimentalGroup; } - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { warning( `Unknown InspectorControls group "${ group }" provided.` ); diff --git a/packages/block-editor/src/components/inspector-controls/fill.native.js b/packages/block-editor/src/components/inspector-controls/fill.native.js index d38d865cd15cc0..f1ee5a14cd18e1 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.native.js +++ b/packages/block-editor/src/components/inspector-controls/fill.native.js @@ -35,7 +35,7 @@ export default function InspectorControlsFill( { ); group = __experimentalGroup; } - const isDisplayed = useDisplayBlockControls(); + const { isDisplayed } = useDisplayBlockControls(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { diff --git a/packages/block-editor/src/components/use-display-block-controls/index.js b/packages/block-editor/src/components/use-display-block-controls/index.js index 605556f295b968..ef27479593a736 100644 --- a/packages/block-editor/src/components/use-display-block-controls/index.js +++ b/packages/block-editor/src/components/use-display-block-controls/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { useSelect } from '@wordpress/data'; +import { store as blocksStore } from '@wordpress/blocks'; /** * Internal dependencies @@ -13,23 +14,28 @@ export default function useDisplayBlockControls() { const { isSelected, clientId, name } = useBlockEditContext(); return useSelect( ( select ) => { - if ( isSelected ) { - return true; - } - const { getBlockName, isFirstMultiSelectedBlock, getMultiSelectedBlockClientIds, + hasSelectedInnerBlock, } = select( blockEditorStore ); + const { hasBlockSupport } = select( blocksStore ); - if ( isFirstMultiSelectedBlock( clientId ) ) { - return getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ); - } - - return false; + return { + isDisplayed: + isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === name + ) ), + isParentDisplayed: + hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && hasSelectedInnerBlock( clientId ), + }; }, [ clientId, isSelected, name ] ); diff --git a/packages/block-editor/src/components/use-display-block-controls/index.native.js b/packages/block-editor/src/components/use-display-block-controls/index.native.js index e8a198e1592e82..d865ed6d9d7b26 100644 --- a/packages/block-editor/src/components/use-display-block-controls/index.native.js +++ b/packages/block-editor/src/components/use-display-block-controls/index.native.js @@ -26,11 +26,7 @@ export default function useDisplayBlockControls() { false ); - if ( ! hideControls && isSelected ) { - return true; - } - - return false; + return { isDisplayed: ! hideControls && isSelected }; }, [ clientId, isSelected, name ] ); diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 3b916d9577f1a7..2019228cf2d3e8 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -109,7 +109,7 @@ export function addAttribute( settings ) { } function BlockEditAlignmentToolbarControlsPure( { - blockName, + name: blockName, align, setAttributes, } ) { @@ -152,45 +152,14 @@ function BlockEditAlignmentToolbarControlsPure( { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockEditAlignmentToolbarControls = pure( - BlockEditAlignmentToolbarControlsPure -); - -/** - * Override the default edit UI to include new toolbar controls for block - * alignment, if block defines support. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withAlignmentControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const hasAlignmentSupport = hasBlockSupport( - props.name, - 'align', - false - ); - - return ( - <> - { hasAlignmentSupport && ( - <BlockEditAlignmentToolbarControls - blockName={ props.name } - // This component is pure, so only pass needed props! - align={ props.attributes.align } - setAttributes={ props.setAttributes } - /> - ) } - <BlockEdit key="edit" { ...props } /> - </> - ); +export default { + shareWithChildBlocks: true, + edit: BlockEditAlignmentToolbarControlsPure, + attributeKeys: [ 'align' ], + hasSupport( name ) { + return hasBlockSupport( name, 'align', false ); }, - 'withAlignmentControls' -); +}; function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { const { name, attributes } = props; @@ -273,11 +242,6 @@ addFilter( 'core/editor/align/with-data-align', withDataAlign ); -addFilter( - 'editor.BlockEdit', - 'core/editor/align/with-toolbar-controls', - withAlignmentControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/align/addAssignedAlign', diff --git a/packages/block-editor/src/hooks/align.native.js b/packages/block-editor/src/hooks/align.native.js index 1bf375b654ad40..7a229e79870a80 100644 --- a/packages/block-editor/src/hooks/align.native.js +++ b/packages/block-editor/src/hooks/align.native.js @@ -8,6 +8,7 @@ import { WIDE_ALIGNMENTS } from '@wordpress/components'; const ALIGNMENTS = [ 'left', 'center', 'right' ]; export * from './align.js'; +export { default } from './align.js'; // Used to filter out blocks that don't support wide/full alignment on mobile addFilter( diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 9902ed479531c6..882820678aa870 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl, ExternalLink } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { Platform } from '@wordpress/element'; /** @@ -52,7 +51,11 @@ export function addAttribute( settings ) { return settings; } -function BlockEditAnchorControlPure( { blockName, anchor, setAttributes } ) { +function BlockEditAnchorControlPure( { + name: blockName, + anchor, + setAttributes, +} ) { const blockEditingMode = useBlockEditingMode(); const isWeb = Platform.OS === 'web'; @@ -116,38 +119,13 @@ function BlockEditAnchorControlPure( { blockName, anchor, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockEditAnchorControl = pure( BlockEditAnchorControlPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning the anchor ID, if block supports anchor. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -export const withAnchorControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - return ( - <> - <BlockEdit { ...props } /> - { props.isSelected && - hasBlockSupport( props.name, 'anchor' ) && ( - <BlockEditAnchorControl - blockName={ props.name } - // This component is pure, so only pass needed - // props! - anchor={ props.attributes.anchor } - setAttributes={ props.setAttributes } - /> - ) } - </> - ); - }; -}, 'withAnchorControls' ); +export default { + edit: BlockEditAnchorControlPure, + attributeKeys: [ 'anchor' ], + hasSupport( name ) { + return hasBlockSupport( name, 'anchor' ); + }, +}; /** * Override props assigned to save component to inject anchor ID, if block @@ -169,11 +147,6 @@ export function addSaveProps( extraProps, blockType, attributes ) { } addFilter( 'blocks.registerBlockType', 'core/anchor/attribute', addAttribute ); -addFilter( - 'editor.BlockEdit', - 'core/editor/anchor/with-inspector-controls', - withAnchorControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/anchor/save-props', diff --git a/packages/block-editor/src/hooks/block-hooks.js b/packages/block-editor/src/hooks/block-hooks.js index 59c0e3c85486f0..a1640c72f4b2b4 100644 --- a/packages/block-editor/src/hooks/block-hooks.js +++ b/packages/block-editor/src/hooks/block-hooks.js @@ -2,14 +2,12 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { addFilter } from '@wordpress/hooks'; import { Fragment, useMemo } from '@wordpress/element'; import { __experimentalHStack as HStack, PanelBody, ToggleControl, } from '@wordpress/components'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { createBlock, store as blocksStore } from '@wordpress/blocks'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -21,7 +19,7 @@ import { store as blockEditorStore } from '../store'; const EMPTY_OBJECT = {}; -function BlockHooksControlPure( props ) { +function BlockHooksControlPure( { name, clientId } ) { const blockTypes = useSelect( ( select ) => select( blocksStore ).getBlockTypes(), [] @@ -30,10 +28,9 @@ function BlockHooksControlPure( props ) { const hookedBlocksForCurrentBlock = useMemo( () => blockTypes?.filter( - ( { blockHooks } ) => - blockHooks && props.blockName in blockHooks + ( { blockHooks } ) => blockHooks && name in blockHooks ), - [ blockTypes, props.blockName ] + [ blockTypes, name ] ); const { blockIndex, rootClientId, innerBlocksLength } = useSelect( @@ -42,13 +39,12 @@ function BlockHooksControlPure( props ) { select( blockEditorStore ); return { - blockIndex: getBlockIndex( props.clientId ), - innerBlocksLength: getBlock( props.clientId )?.innerBlocks - ?.length, - rootClientId: getBlockRootClientId( props.clientId ), + blockIndex: getBlockIndex( clientId ), + innerBlocksLength: getBlock( clientId )?.innerBlocks?.length, + rootClientId: getBlockRootClientId( clientId ), }; }, - [ props.clientId ] + [ clientId ] ); const hookedBlockClientIds = useSelect( @@ -65,8 +61,7 @@ function BlockHooksControlPure( props ) { return clientIds; } - const relativePosition = - block?.blockHooks?.[ props.blockName ]; + const relativePosition = block?.blockHooks?.[ name ]; let candidates; switch ( relativePosition ) { @@ -83,12 +78,12 @@ function BlockHooksControlPure( props ) { // Any of the current block's child blocks (with the right block type) qualifies // as a hooked first or last child block, as the block might've been automatically // inserted and then moved around a bit by the user. - candidates = getBlock( props.clientId ).innerBlocks; + candidates = getBlock( clientId ).innerBlocks; break; } const hookedBlock = candidates?.find( - ( { name } ) => name === block.name + ( candidate ) => name === candidate.name ); // If the block exists in the designated location, we consider it hooked @@ -118,12 +113,7 @@ function BlockHooksControlPure( props ) { return EMPTY_OBJECT; }, - [ - hookedBlocksForCurrentBlock, - props.blockName, - props.clientId, - rootClientId, - ] + [ hookedBlocksForCurrentBlock, name, clientId, rootClientId ] ); const { insertBlock, removeBlock } = useDispatch( blockEditorStore ); @@ -169,7 +159,7 @@ function BlockHooksControlPure( props ) { block, // TODO: It'd be great if insertBlock() would accept negative indices for insertion. relativePosition === 'first_child' ? 0 : innerBlocksLength, - props.clientId, // Insert as a child of the current block. + clientId, // Insert as a child of the current block. false ); break; @@ -207,9 +197,7 @@ function BlockHooksControlPure( props ) { if ( ! checked ) { // Create and insert block. const relativePosition = - block.blockHooks[ - props.blockName - ]; + block.blockHooks[ name ]; insertBlockIntoDesignatedLocation( createBlock( block.name ), relativePosition @@ -218,11 +206,12 @@ function BlockHooksControlPure( props ) { } // Remove block. - const clientId = + removeBlock( hookedBlockClientIds[ block.name - ]; - removeBlock( clientId, false ); + ], + false + ); } } /> ); @@ -235,32 +224,9 @@ function BlockHooksControlPure( props ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockHooksControl = pure( BlockHooksControlPure ); - -export const withBlockHooksControls = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - return ( - <> - <BlockEdit key="edit" { ...props } /> - { props.isSelected && ( - <BlockHooksControl - blockName={ props.name } - clientId={ props.clientId } - /> - ) } - </> - ); - }; +export default { + edit: BlockHooksControlPure, + hasSupport() { + return true; }, - 'withBlockHooksControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/editor/block-hooks/with-inspector-controls', - withBlockHooksControls -); +}; diff --git a/packages/block-editor/src/hooks/block-renaming.js b/packages/block-editor/src/hooks/block-renaming.js index 452be6e686dbf4..26ada6ba732819 100644 --- a/packages/block-editor/src/hooks/block-renaming.js +++ b/packages/block-editor/src/hooks/block-renaming.js @@ -3,7 +3,6 @@ */ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { TextControl } from '@wordpress/components'; @@ -11,7 +10,6 @@ import { TextControl } from '@wordpress/components'; * Internal dependencies */ import { InspectorControls } from '../components'; -import { useBlockRename } from '../components/block-rename'; /** * Filters registered block settings, adding an `__experimentalLabel` callback if one does not already exist. @@ -47,13 +45,7 @@ export function addLabelCallback( settings ) { return settings; } -function BlockRenameControlPure( { name, metadata, setAttributes } ) { - const { canRename } = useBlockRename( name ); - - if ( ! canRename ) { - return null; - } - +function BlockRenameControlPure( { metadata, setAttributes } ) { return ( <InspectorControls group="advanced"> <TextControl @@ -70,36 +62,13 @@ function BlockRenameControlPure( { name, metadata, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const BlockRenameControl = pure( BlockRenameControlPure ); - -export const withBlockRenameControl = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { name, attributes, setAttributes, isSelected } = props; - return ( - <> - { isSelected && ( - <BlockRenameControl - name={ name } - // This component is pure, so only pass needed props! - metadata={ attributes.metadata } - setAttributes={ setAttributes } - /> - ) } - <BlockEdit key="edit" { ...props } /> - </> - ); +export default { + edit: BlockRenameControlPure, + attributeKeys: [ 'metadata' ], + hasSupport( name ) { + return hasBlockSupport( name, 'renaming', true ); }, - 'withToolbarControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/block-rename-ui/with-block-rename-control', - withBlockRenameControl -); +}; addFilter( 'blocks.registerBlockType', diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index 8c0f58ddda682d..331edd9ef214a2 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -10,7 +10,6 @@ import { addFilter } from '@wordpress/hooks'; import { TextControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -64,46 +63,13 @@ function CustomClassNameControlsPure( { className, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const CustomClassNameControls = pure( CustomClassNameControlsPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning the custom class name, if block supports custom class name. - * The control is displayed within the Advanced panel in the block inspector. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -export const withCustomClassNameControls = createHigherOrderComponent( - ( BlockEdit ) => { - return ( props ) => { - const hasCustomClassName = hasBlockSupport( - props.name, - 'customClassName', - true - ); - - return ( - <> - <BlockEdit { ...props } /> - { hasCustomClassName && props.isSelected && ( - <CustomClassNameControls - // This component is pure, so only pass needed - // props! - className={ props.attributes.className } - setAttributes={ props.setAttributes } - /> - ) } - </> - ); - }; +export default { + edit: CustomClassNameControlsPure, + attributeKeys: [ 'className' ], + hasSupport( name ) { + return hasBlockSupport( name, 'customClassName', true ); }, - 'withCustomClassNameControls' -); +}; /** * Override props assigned to save component to inject the className, if block @@ -174,11 +140,6 @@ addFilter( 'core/editor/custom-class-name/attribute', addAttribute ); -addFilter( - 'editor.BlockEdit', - 'core/editor/custom-class-name/with-inspector-controls', - withCustomClassNameControls -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/custom-class-name/save-props', diff --git a/packages/block-editor/src/hooks/custom-fields.js b/packages/block-editor/src/hooks/custom-fields.js index 19729d00ad61a6..9b677933adc138 100644 --- a/packages/block-editor/src/hooks/custom-fields.js +++ b/packages/block-editor/src/hooks/custom-fields.js @@ -5,7 +5,6 @@ import { addFilter } from '@wordpress/hooks'; import { PanelBody, TextControl } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { hasBlockSupport } from '@wordpress/blocks'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; /** * Internal dependencies @@ -91,50 +90,18 @@ function CustomFieldsControlPure( { name, connections, setAttributes } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const CustomFieldsControl = pure( CustomFieldsControlPure ); - -/** - * Override the default edit UI to include a new block inspector control for - * assigning a connection to blocks that has support for connections. - * Currently, only the `core/paragraph` block is supported and there is only a relation - * between paragraph content and a custom field. - * - * @param {Component} BlockEdit Original component. - * - * @return {Component} Wrapped component. - */ -const withCustomFieldsControls = createHigherOrderComponent( ( BlockEdit ) => { - return ( props ) => { - const hasCustomFieldsSupport = hasBlockSupport( - props.name, - '__experimentalConnections', - false - ); - - // Check if the current block is a paragraph or image block. - // Currently, only these two blocks are supported. - if ( ! [ 'core/paragraph', 'core/image' ].includes( props.name ) ) { - return <BlockEdit key="edit" { ...props } />; - } - +export default { + edit: CustomFieldsControlPure, + attributeKeys: [ 'connections' ], + hasSupport( name ) { return ( - <> - <BlockEdit key="edit" { ...props } /> - { hasCustomFieldsSupport && props.isSelected && ( - <CustomFieldsControl - name={ props.name } - // This component is pure, so only pass needed props! - connections={ props.attributes.connections } - setAttributes={ props.setAttributes } - /> - ) } - </> + hasBlockSupport( name, '__experimentalConnections', false ) && + // Check if the current block is a paragraph or image block. + // Currently, only these two blocks are supported. + [ 'core/paragraph', 'core/image' ].includes( name ) ); - }; -}, 'withCustomFieldsControls' ); + }, +}; if ( window.__experimentalConnections || @@ -146,10 +113,3 @@ if ( addAttribute ); } -if ( window.__experimentalConnections ) { - addFilter( - 'editor.BlockEdit', - 'core/editor/connections/with-inspector-controls', - withCustomFieldsControls - ); -} diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index 6e18b44cef1633..c0b76d12cb3707 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -13,11 +13,7 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { - createHigherOrderComponent, - useInstanceId, - pure, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; @@ -179,10 +175,14 @@ function DuotonePanelPure( { style, setAttributes, name } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const DuotonePanel = pure( DuotonePanelPure ); +export default { + shareWithChildBlocks: true, + edit: DuotonePanelPure, + attributeKeys: [ 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, 'filter.duotone' ); + }, +}; /** * Filters registered block settings, extending attributes to include @@ -212,44 +212,6 @@ function addDuotoneAttributes( settings ) { return settings; } -/** - * Override the default edit UI to include toolbar controls for duotone if the - * block supports duotone. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -const withDuotoneControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - // Previous `color.__experimentalDuotone` support flag is migrated via - // block_type_metadata_settings filter in `lib/block-supports/duotone.php`. - const hasDuotoneSupport = hasBlockSupport( - props.name, - 'filter.duotone' - ); - - // CAUTION: code added before this line will be executed - // for all blocks, not just those that support duotone. Code added - // above this line should be carefully evaluated for its impact on - // performance. - return ( - <> - { hasDuotoneSupport && ( - <DuotonePanel - // This component is pure, so only pass needed props! - style={ props.attributes.style } - setAttributes={ props.setAttributes } - name={ props.name } - /> - ) } - <BlockEdit { ...props } /> - </> - ); - }, - 'withDuotoneControls' -); - function DuotoneStyles( { clientId, id: filterId, @@ -438,11 +400,6 @@ addFilter( 'core/editor/duotone/add-attributes', addDuotoneAttributes ); -addFilter( - 'editor.BlockEdit', - 'core/editor/duotone/with-editor-controls', - withDuotoneControls -); addFilter( 'editor.BlockListBlock', 'core/editor/duotone/with-styles', diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index c088216c0645cb..6ae589dd672bf1 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,27 +1,43 @@ /** * Internal dependencies */ +import { createBlockEditFilter } from './utils'; import './compat'; -import './align'; +import align from './align'; import './lock'; -import './anchor'; +import anchor from './anchor'; import './aria-label'; -import './custom-class-name'; +import customClassName from './custom-class-name'; import './generated-class-name'; -import './style'; +import style from './style'; import './settings'; import './color'; -import './duotone'; +import duotone from './duotone'; import './font-family'; import './font-size'; import './border'; -import './position'; -import './layout'; +import position from './position'; +import layout from './layout'; import './content-lock-ui'; import './metadata'; -import './custom-fields'; -import './block-hooks'; -import './block-renaming'; +import customFields from './custom-fields'; +import blockHooks from './block-hooks'; +import blockRenaming from './block-renaming'; + +createBlockEditFilter( + [ + align, + anchor, + customClassName, + style, + duotone, + position, + layout, + window.__experimentalConnections ? customFields : null, + blockHooks, + blockRenaming, + ].filter( Boolean ) +); export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index 42bda25bfe1ccf..3f1a4473c13891 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -1,16 +1,19 @@ /** * Internal dependencies */ +import { createBlockEditFilter } from './utils'; import './compat'; -import './align'; -import './anchor'; +import align from './align'; +import anchor from './anchor'; import './custom-class-name'; import './generated-class-name'; -import './style'; +import style from './style'; import './color'; import './font-size'; import './layout'; +createBlockEditFilter( [ align, anchor, style ] ); + export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; export { getSpacingClassesAndStyles } from './use-spacing-props'; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 3ea5c56da8e776..46239e1de07037 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -6,11 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - createHigherOrderComponent, - pure, - useInstanceId, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; @@ -290,10 +286,14 @@ function LayoutPanelPure( { layout, setAttributes, name: blockName } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const LayoutPanel = pure( LayoutPanelPure ); +export default { + shareWithChildBlocks: true, + edit: LayoutPanelPure, + attributeKeys: [ 'layout' ], + hasSupport( name ) { + return hasLayoutBlockSupport( name ); + }, +}; function LayoutTypeSwitcher( { type, onChange } ) { return ( @@ -336,33 +336,6 @@ export function addAttribute( settings ) { return settings; } -/** - * Override the default edit UI to include layout controls - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withLayoutControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const supportLayout = hasLayoutBlockSupport( props.name ); - - return [ - supportLayout && ( - <LayoutPanel - key="layout" - // This component is pure, so only pass needed props! - layout={ props.attributes.layout } - setAttributes={ props.setAttributes } - name={ props.name } - /> - ), - <BlockEdit key="edit" { ...props } />, - ]; - }, - 'withLayoutControls' -); - function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { const { name, attributes } = props; const id = useInstanceId( BlockListBlock ); @@ -516,8 +489,3 @@ addFilter( 'core/editor/layout/with-child-layout-styles', withChildLayoutStyles ); -addFilter( - 'editor.BlockEdit', - 'core/editor/layout/with-inspector-controls', - withLayoutControls -); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index 32d4f6582969ee..cdeb90822f0ac4 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -12,11 +12,7 @@ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { - createHigherOrderComponent, - pure, - useInstanceId, -} from '@wordpress/compose'; +import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo, Platform } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; @@ -318,44 +314,19 @@ export function PositionPanelPure( { } ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const PositionPanel = pure( PositionPanelPure ); - -/** - * Override the default edit UI to include position controls. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withPositionControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - const { name: blockName } = props; - const positionSupport = hasBlockSupport( - blockName, - POSITION_SUPPORT_KEY - ); +export default { + edit: function Edit( props ) { const isPositionDisabled = useIsPositionDisabled( props ); - const showPositionControls = positionSupport && ! isPositionDisabled; - - return [ - showPositionControls && ( - <PositionPanel - key="position" - // This component is pure, so only pass needed props! - style={ props.attributes.style } - name={ blockName } - setAttributes={ props.setAttributes } - clientId={ props.clientId } - /> - ), - <BlockEdit key="edit" { ...props } />, - ]; + if ( isPositionDisabled ) { + return null; + } + return <PositionPanelPure { ...props } />; }, - 'withPositionControls' -); + attributeKeys: [ 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, POSITION_SUPPORT_KEY ); + }, +}; /** * Override the default block element to add the position styles. @@ -411,8 +382,3 @@ addFilter( 'core/editor/position/with-position-styles', withPositionStyles ); -addFilter( - 'editor.BlockEdit', - 'core/editor/position/with-inspector-controls', - withPositionControls -); diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 1acb2cda3ac017..40363423168874 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -32,7 +32,6 @@ import { SPACING_SUPPORT_KEY, DimensionsPanel, } from './dimensions'; -import useDisplayBlockControls from '../components/use-display-block-controls'; import { shouldSkipSerialization, useStyleOverride, @@ -356,12 +355,16 @@ function BlockStyleControls( { __unstableParentLayout, } ) { const settings = useBlockSettings( name, __unstableParentLayout ); + const blockEditingMode = useBlockEditingMode(); const passedProps = { clientId, name, setAttributes, settings, }; + if ( blockEditingMode !== 'default' ) { + return null; + } return ( <> <ColorEdit { ...passedProps } /> @@ -373,34 +376,10 @@ function BlockStyleControls( { ); } -/** - * Override the default edit UI to include new inspector controls for - * all the custom styles configs. - * - * @param {Function} BlockEdit Original component. - * - * @return {Function} Wrapped component. - */ -export const withBlockStyleControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - if ( ! hasStyleSupport( props.name ) ) { - return <BlockEdit key="edit" { ...props } />; - } - - const shouldDisplayControls = useDisplayBlockControls(); - const blockEditingMode = useBlockEditingMode(); - - return ( - <> - { shouldDisplayControls && blockEditingMode === 'default' && ( - <BlockStyleControls { ...props } /> - ) } - <BlockEdit key="edit" { ...props } /> - </> - ); - }, - 'withBlockStyleControls' -); +export default { + edit: BlockStyleControls, + hasSupport: hasStyleSupport, +}; // Defines which element types are supported, including their hover styles or // any other elements that have been included under a single element type @@ -542,12 +521,6 @@ addFilter( addEditProps ); -addFilter( - 'editor.BlockEdit', - 'core/style/with-block-controls', - withBlockStyleControls -); - addFilter( 'editor.BlockListBlock', 'core/editor/with-elements-styles', diff --git a/packages/block-editor/src/hooks/test/align.js b/packages/block-editor/src/hooks/test/align.js index b928e6eafc8b2d..c695399e993b0e 100644 --- a/packages/block-editor/src/hooks/test/align.js +++ b/packages/block-editor/src/hooks/test/align.js @@ -12,20 +12,12 @@ import { registerBlockType, unregisterBlockType, } from '@wordpress/blocks'; -import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies */ -import BlockControls from '../../components/block-controls'; -import BlockEdit from '../../components/block-edit'; import BlockEditorProvider from '../../components/provider'; -import { - getValidAlignments, - withAlignmentControls, - withDataAlign, - addAssignedAlign, -} from '../align'; +import { getValidAlignments, withDataAlign, addAssignedAlign } from '../align'; const noop = () => {}; @@ -157,68 +149,6 @@ describe( 'align', () => { } ); } ); - describe( 'withAlignControls', () => { - const componentProps = { - name: 'core/foo', - attributes: {}, - isSelected: true, - }; - - it( 'should do nothing if no valid alignments', () => { - registerBlockType( 'core/foo', blockSettings ); - - const EnhancedComponent = withAlignmentControls( - ( { wrapperProps } ) => <div { ...wrapperProps } /> - ); - - render( - <SlotFillProvider> - <BlockEdit { ...componentProps }> - <EnhancedComponent { ...componentProps } /> - </BlockEdit> - <BlockControls.Slot group="block" /> - </SlotFillProvider> - ); - - expect( - screen.queryByRole( 'button', { - name: 'Align', - expanded: false, - } ) - ).not.toBeInTheDocument(); - } ); - - it( 'should render toolbar controls if valid alignments', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: false, - }, - } ); - - const EnhancedComponent = withAlignmentControls( - ( { wrapperProps } ) => <div { ...wrapperProps } /> - ); - - render( - <SlotFillProvider> - <BlockEdit { ...componentProps }> - <EnhancedComponent { ...componentProps } /> - </BlockEdit> - <BlockControls.Slot group="block" /> - </SlotFillProvider> - ); - - expect( - screen.getAllByRole( 'button', { - name: 'Align', - expanded: false, - } ) - ).toHaveLength( 2 ); - } ); - } ); - describe( 'withDataAlign', () => { it( 'should render with wrapper props', () => { registerBlockType( 'core/foo', { diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 5985b821d00a86..98638ae5dabf54 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -4,10 +4,13 @@ import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, useEffect, useId } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; +import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ +import useDisplayBlockControls from '../components/use-display-block-controls'; import { useSettings } from '../components'; import { useSettingsForBlockElement } from '../components/global-styles/hooks'; import { getValueFromObjectPath, setImmutably } from '../utils/object'; @@ -362,3 +365,68 @@ export function useBlockSettings( name, parentLayout ) { return useSettingsForBlockElement( rawSettings, name ); } + +export function createBlockEditFilter( features ) { + // We don't want block controls to re-render when typing inside a block. + // `pure` will prevent re-renders unless props change, so only pass the + // needed props and not the whole attributes object. + features = features.map( ( settings ) => { + return { ...settings, Edit: pure( settings.edit ) }; + } ); + const withBlockEditHooks = createHigherOrderComponent( + ( OriginalBlockEdit ) => ( props ) => { + const { isDisplayed, isParentDisplayed } = + useDisplayBlockControls(); + // CAUTION: code added before this line will be executed for all + // blocks, not just those that support the feature! Code added + // above this line should be carefully evaluated for its impact on + // performance. + return [ + ...features.map( ( feature, i ) => { + const { + Edit, + hasSupport, + attributeKeys = [], + shareWithChildBlocks, + } = feature; + const shouldDisplayControls = + isDisplayed || + ( isParentDisplayed && shareWithChildBlocks ); + + if ( + ! shouldDisplayControls || + ! hasSupport( props.name ) + ) { + return null; + } + + const neededProps = {}; + for ( const key of attributeKeys ) { + if ( props.attributes[ key ] ) { + neededProps[ key ] = props.attributes[ key ]; + } + } + return ( + <Edit + // We can use the index because the array length + // is fixed per page load right now. + key={ i } + name={ props.name } + clientId={ props.clientId } + setAttributes={ props.setAttributes } + __unstableParentLayout={ + props.__unstableParentLayout + } + // This component is pure, so only pass needed + // props!!! + { ...neededProps } + /> + ); + } ), + <OriginalBlockEdit key="edit" { ...props } />, + ]; + }, + 'withBlockEditHooks' + ); + addFilter( 'editor.BlockEdit', 'core/editor/hooks', withBlockEditHooks ); +} From 6a42225124e69276a2deec4597a855bb504b37cc Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 7 Dec 2023 22:41:11 +0200 Subject: [PATCH 084/325] RichText: pass value to store (#43204) --- package-lock.json | 2 + .../src/components/rich-text/content.js | 47 ++++--- .../rich-text/get-rich-text-values.js | 7 +- .../components/rich-text/use-input-rules.js | 7 +- packages/block-editor/src/utils/selection.js | 11 +- packages/block-library/src/audio/block.json | 4 +- packages/block-library/src/button/block.json | 4 +- packages/block-library/src/code/block.json | 4 +- packages/block-library/src/code/save.js | 5 +- packages/block-library/src/details/block.json | 4 +- packages/block-library/src/embed/block.json | 4 +- packages/block-library/src/file/block.json | 8 +- packages/block-library/src/file/save.js | 6 +- .../block-library/src/form-input/block.json | 4 +- packages/block-library/src/gallery/block.json | 8 +- packages/block-library/src/heading/block.json | 5 +- packages/block-library/src/image/block.json | 4 +- .../block-library/src/list-item/block.json | 5 +- .../block-library/src/paragraph/block.json | 5 +- .../block-library/src/preformatted/block.json | 5 +- .../block-library/src/pullquote/block.json | 9 +- packages/block-library/src/quote/block.json | 5 +- packages/block-library/src/table/block.json | 19 ++- .../src/utils/remove-anchor-tag.js | 3 +- packages/block-library/src/verse/block.json | 5 +- packages/block-library/src/video/block.json | 4 +- packages/blocks/package.json | 1 + packages/blocks/src/api/matchers.js | 12 ++ .../src/api/parser/get-block-attributes.js | 25 +++- .../api/raw-handling/test/paste-handler.js | 4 +- packages/blocks/src/api/utils.js | 42 +++++- .../src/footnotes/get-footnotes-order.js | 17 +-- packages/core-data/src/footnotes/index.js | 22 +-- packages/rich-text/README.md | 13 ++ packages/rich-text/src/component/index.js | 38 ++++-- packages/rich-text/src/create.js | 129 +++++++++++++++++- packages/rich-text/src/index.ts | 2 +- schemas/json/block.json | 2 + .../blocks/core__gallery__deprecated-1.json | 2 + .../documents/ms-word-online-out.html | 14 +- .../non-matched-tags-handling.test.js | 8 +- 41 files changed, 380 insertions(+), 145 deletions(-) diff --git a/package-lock.json b/package-lock.json index e59b7b849ebd92..86cf832a8e41dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54674,6 +54674,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", @@ -70141,6 +70142,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", diff --git a/packages/block-editor/src/components/rich-text/content.js b/packages/block-editor/src/components/rich-text/content.js index 9762582f86f141..92e150fb174edb 100644 --- a/packages/block-editor/src/components/rich-text/content.js +++ b/packages/block-editor/src/components/rich-text/content.js @@ -5,36 +5,43 @@ import { RawHTML } from '@wordpress/element'; import { children as childrenSource } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; +/** + * Internal dependencies + */ +import RichText from './'; + /** * Internal dependencies */ import { getMultilineTag } from './utils'; -export const Content = ( { value, tagName: Tag, multiline, ...props } ) => { - // Handle deprecated `children` and `node` sources. - if ( Array.isArray( value ) ) { +export function Content( { + value, + tagName: Tag, + multiline, + format, + ...props +} ) { + if ( RichText.isEmpty( value ) ) { + const MultilineTag = getMultilineTag( multiline ); + value = MultilineTag ? <MultilineTag /> : null; + } else if ( Array.isArray( value ) ) { deprecated( 'wp.blockEditor.RichText value prop as children type', { since: '6.1', version: '6.3', alternative: 'value prop as string', link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', } ); - - value = childrenSource.toHTML( value ); - } - - const MultilineTag = getMultilineTag( multiline ); - - if ( ! value && MultilineTag ) { - value = `<${ MultilineTag }></${ MultilineTag }>`; - } - - const content = <RawHTML>{ value }</RawHTML>; - - if ( Tag ) { - const { format, ...restProps } = props; - return <Tag { ...restProps }>{ content }</Tag>; + value = <RawHTML>{ childrenSource.toHTML( value ) }</RawHTML>; + } else if ( typeof value === 'string' ) { + // To do: deprecate. + value = <RawHTML>{ value }</RawHTML>; + } else { + // To do: create a toReactComponent method on RichTextData, which we + // might in the future also use for the editable tree. See + // https://github.com/WordPress/gutenberg/pull/41655. + value = <RawHTML>{ value.toHTMLString() }</RawHTML>; } - return content; -}; + return Tag ? <Tag { ...props }>{ value }</Tag> : value; +} diff --git a/packages/block-editor/src/components/rich-text/get-rich-text-values.js b/packages/block-editor/src/components/rich-text/get-rich-text-values.js index bd1c62ea5e6f61..ee2bc638269308 100644 --- a/packages/block-editor/src/components/rich-text/get-rich-text-values.js +++ b/packages/block-editor/src/components/rich-text/get-rich-text-values.js @@ -6,6 +6,7 @@ import { getSaveElement, __unstableGetBlockProps as getBlockProps, } from '@wordpress/blocks'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -95,5 +96,9 @@ export function getRichTextValues( blocks = [] ) { const values = []; addValuesForBlocks( values, blocks ); getBlockProps.skipFilters = false; - return values; + return values.map( ( value ) => + value instanceof RichTextData + ? value + : RichTextData.fromHTMLString( value ) + ); } diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 5aa47e7c7b4d74..5640a85f5f2695 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -28,7 +28,12 @@ function findSelection( blocks ) { if ( attributeKey ) { blocks[ i ].attributes[ attributeKey ] = blocks[ i ].attributes[ attributeKey - ].replace( START_OF_SELECTED_AREA, '' ); + ] + // To do: refactor this to use rich text's selection instead, so + // we no longer have to use on this hack inserting a special + // character. + .toString() + .replace( START_OF_SELECTED_AREA, '' ); return [ blocks[ i ].clientId, attributeKey, 0, 0 ]; } diff --git a/packages/block-editor/src/utils/selection.js b/packages/block-editor/src/utils/selection.js index 68c634d591c5e9..4e971485838791 100644 --- a/packages/block-editor/src/utils/selection.js +++ b/packages/block-editor/src/utils/selection.js @@ -1,3 +1,8 @@ +/** + * WordPress dependencies + */ +import { RichTextData } from '@wordpress/rich-text'; + /** * A robust way to retain selection position through various * transforms is to insert a special character at the position and @@ -19,8 +24,10 @@ export function retrieveSelectedAttribute( blockAttributes ) { return Object.keys( blockAttributes ).find( ( name ) => { const value = blockAttributes[ name ]; return ( - typeof value === 'string' && - value.indexOf( START_OF_SELECTED_AREA ) !== -1 + ( typeof value === 'string' || value instanceof RichTextData ) && + // To do: refactor this to use rich text's selection instead, so we + // no longer have to use on this hack inserting a special character. + value.toString().indexOf( START_OF_SELECTED_AREA ) !== -1 ); } ); } diff --git a/packages/block-library/src/audio/block.json b/packages/block-library/src/audio/block.json index a4740e304451ce..04df268a74a630 100644 --- a/packages/block-library/src/audio/block.json +++ b/packages/block-library/src/audio/block.json @@ -16,8 +16,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/button/block.json b/packages/block-library/src/button/block.json index eec327b4ca48e4..3c232700a876e6 100644 --- a/packages/block-library/src/button/block.json +++ b/packages/block-library/src/button/block.json @@ -36,8 +36,8 @@ "__experimentalRole": "content" }, "text": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a,button", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/code/block.json b/packages/block-library/src/code/block.json index 80df74b5062b56..bd5db3c918b963 100644 --- a/packages/block-library/src/code/block.json +++ b/packages/block-library/src/code/block.json @@ -8,8 +8,8 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "code", "__unstablePreserveWhiteSpace": true } diff --git a/packages/block-library/src/code/save.js b/packages/block-library/src/code/save.js index 7dd355f3855a86..5bb9f68767b5ec 100644 --- a/packages/block-library/src/code/save.js +++ b/packages/block-library/src/code/save.js @@ -13,7 +13,10 @@ export default function save( { attributes } ) { <pre { ...useBlockProps.save() }> <RichText.Content tagName="code" - value={ escape( attributes.content ) } + // To do: `escape` encodes characters in shortcodes and URLs to + // prevent embedding in PHP. Ideally checks for the code block, + // or pre/code tags, should be made on the PHP side? + value={ escape( attributes.content.toString() ) } /> </pre> ); diff --git a/packages/block-library/src/details/block.json b/packages/block-library/src/details/block.json index d449d42e1e10c4..a71d3af2a5ed39 100644 --- a/packages/block-library/src/details/block.json +++ b/packages/block-library/src/details/block.json @@ -13,8 +13,8 @@ "default": false }, "summary": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "summary" } }, diff --git a/packages/block-library/src/embed/block.json b/packages/block-library/src/embed/block.json index 9ca54db871db19..5aac8bbd6b8cab 100644 --- a/packages/block-library/src/embed/block.json +++ b/packages/block-library/src/embed/block.json @@ -12,8 +12,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/file/block.json b/packages/block-library/src/file/block.json index 0cc20b3f501e9b..9dc6677e4adce3 100644 --- a/packages/block-library/src/file/block.json +++ b/packages/block-library/src/file/block.json @@ -21,8 +21,8 @@ "attribute": "id" }, "fileName": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a:not([download])" }, "textLinkHref": { @@ -42,8 +42,8 @@ "default": true }, "downloadButtonText": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "a[download]" }, "displayPreview": { diff --git a/packages/block-library/src/file/save.js b/packages/block-library/src/file/save.js index 6d0684ac76b8ef..f5eb1ce3c2b14e 100644 --- a/packages/block-library/src/file/save.js +++ b/packages/block-library/src/file/save.js @@ -25,7 +25,11 @@ export default function save( { attributes } ) { previewHeight, } = attributes; - const pdfEmbedLabel = RichText.isEmpty( fileName ) ? 'PDF embed' : fileName; + const pdfEmbedLabel = RichText.isEmpty( fileName ) + ? 'PDF embed' + : // To do: use toPlainText, but we need ensure it's RichTextData. See + // https://github.com/WordPress/gutenberg/pull/56710. + fileName.toString(); const hasFilename = ! RichText.isEmpty( fileName ); diff --git a/packages/block-library/src/form-input/block.json b/packages/block-library/src/form-input/block.json index 067b7ac69430c4..53aa0be6744cb9 100644 --- a/packages/block-library/src/form-input/block.json +++ b/packages/block-library/src/form-input/block.json @@ -19,10 +19,10 @@ "type": "string" }, "label": { - "type": "string", + "type": "rich-text", "default": "Label", "selector": ".wp-block-form-input__label-content", - "source": "html", + "source": "rich-text", "__experimentalRole": "content" }, "inlineLabel": { diff --git a/packages/block-library/src/gallery/block.json b/packages/block-library/src/gallery/block.json index 0867989af4ec7e..fad92aed59bf77 100644 --- a/packages/block-library/src/gallery/block.json +++ b/packages/block-library/src/gallery/block.json @@ -46,8 +46,8 @@ "attribute": "data-id" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": ".blocks-gallery-item__caption" } } @@ -72,8 +72,8 @@ "maximum": 8 }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": ".blocks-gallery-caption" }, "imageCrop": { diff --git a/packages/block-library/src/heading/block.json b/packages/block-library/src/heading/block.json index dfd5bb72b63314..72cc67caddd9ea 100644 --- a/packages/block-library/src/heading/block.json +++ b/packages/block-library/src/heading/block.json @@ -12,10 +12,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "h1,h2,h3,h4,h5,h6", - "default": "", "__experimentalRole": "content" }, "level": { diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json index cfe91a71ff4f97..c5191e3dd86543 100644 --- a/packages/block-library/src/image/block.json +++ b/packages/block-library/src/image/block.json @@ -25,8 +25,8 @@ "__experimentalRole": "content" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/block-library/src/list-item/block.json b/packages/block-library/src/list-item/block.json index 07797be8623a51..06997c2ac23f8e 100644 --- a/packages/block-library/src/list-item/block.json +++ b/packages/block-library/src/list-item/block.json @@ -12,10 +12,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "li", - "default": "", "__experimentalRole": "content" } }, diff --git a/packages/block-library/src/paragraph/block.json b/packages/block-library/src/paragraph/block.json index 85f56f4a838f50..3fe4fbb34e1029 100644 --- a/packages/block-library/src/paragraph/block.json +++ b/packages/block-library/src/paragraph/block.json @@ -13,10 +13,9 @@ "type": "string" }, "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "p", - "default": "", "__experimentalRole": "content" }, "dropCap": { diff --git a/packages/block-library/src/preformatted/block.json b/packages/block-library/src/preformatted/block.json index ec6ea839385eb2..def870e7ad2fb7 100644 --- a/packages/block-library/src/preformatted/block.json +++ b/packages/block-library/src/preformatted/block.json @@ -8,10 +8,9 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "pre", - "default": "", "__unstablePreserveWhiteSpace": true, "__experimentalRole": "content" } diff --git a/packages/block-library/src/pullquote/block.json b/packages/block-library/src/pullquote/block.json index 1d6c74dbc4ae04..7fc81d5683bd19 100644 --- a/packages/block-library/src/pullquote/block.json +++ b/packages/block-library/src/pullquote/block.json @@ -8,16 +8,15 @@ "textdomain": "default", "attributes": { "value": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "p", "__experimentalRole": "content" }, "citation": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "cite", - "default": "", "__experimentalRole": "content" }, "textAlign": { diff --git a/packages/block-library/src/quote/block.json b/packages/block-library/src/quote/block.json index 7ed406c0d20965..9deca000efe06b 100644 --- a/packages/block-library/src/quote/block.json +++ b/packages/block-library/src/quote/block.json @@ -17,10 +17,9 @@ "__experimentalRole": "content" }, "citation": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "cite", - "default": "", "__experimentalRole": "content" }, "align": { diff --git a/packages/block-library/src/table/block.json b/packages/block-library/src/table/block.json index d1139d6c55addf..470886a1247fe1 100644 --- a/packages/block-library/src/table/block.json +++ b/packages/block-library/src/table/block.json @@ -12,10 +12,9 @@ "default": false }, "caption": { - "type": "string", - "source": "html", - "selector": "figcaption", - "default": "" + "type": "rich-text", + "source": "rich-text", + "selector": "figcaption" }, "head": { "type": "array", @@ -30,8 +29,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", @@ -75,8 +74,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", @@ -120,8 +119,8 @@ "selector": "td,th", "query": { "content": { - "type": "string", - "source": "html" + "type": "rich-text", + "source": "rich-text" }, "tag": { "type": "string", diff --git a/packages/block-library/src/utils/remove-anchor-tag.js b/packages/block-library/src/utils/remove-anchor-tag.js index 31d1877082f50d..82e7b03423648d 100644 --- a/packages/block-library/src/utils/remove-anchor-tag.js +++ b/packages/block-library/src/utils/remove-anchor-tag.js @@ -6,5 +6,6 @@ * @return {string} The value with anchor tags removed. */ export default function removeAnchorTag( value ) { - return value.replace( /<\/?a[^>]*>/g, '' ); + // To do: Refactor this to use rich text's removeFormat instead. + return value.toString().replace( /<\/?a[^>]*>/g, '' ); } diff --git a/packages/block-library/src/verse/block.json b/packages/block-library/src/verse/block.json index fa0bc30798212e..846a1dc99caafc 100644 --- a/packages/block-library/src/verse/block.json +++ b/packages/block-library/src/verse/block.json @@ -9,10 +9,9 @@ "textdomain": "default", "attributes": { "content": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "pre", - "default": "", "__unstablePreserveWhiteSpace": true, "__experimentalRole": "content" }, diff --git a/packages/block-library/src/video/block.json b/packages/block-library/src/video/block.json index debe6f20fe53f7..5d4680f39e79a8 100644 --- a/packages/block-library/src/video/block.json +++ b/packages/block-library/src/video/block.json @@ -15,8 +15,8 @@ "attribute": "autoplay" }, "caption": { - "type": "string", - "source": "html", + "type": "rich-text", + "source": "rich-text", "selector": "figcaption", "__experimentalRole": "content" }, diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 961cb338d73378..928d9d94740b4f 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -42,6 +42,7 @@ "@wordpress/i18n": "file:../i18n", "@wordpress/is-shallow-equal": "file:../is-shallow-equal", "@wordpress/private-apis": "file:../private-apis", + "@wordpress/rich-text": "file:../rich-text", "@wordpress/shortcode": "file:../shortcode", "change-case": "^4.1.2", "colord": "^2.7.0", diff --git a/packages/blocks/src/api/matchers.js b/packages/blocks/src/api/matchers.js index 7a6ac84891658a..950f1539440a0b 100644 --- a/packages/blocks/src/api/matchers.js +++ b/packages/blocks/src/api/matchers.js @@ -3,6 +3,11 @@ */ export { attr, prop, text, query } from 'hpq'; +/** + * WordPress dependencies + */ +import { RichTextData } from '@wordpress/rich-text'; + /** * Internal dependencies */ @@ -41,3 +46,10 @@ export function html( selector, multilineTag ) { return match.innerHTML; }; } + +export const richText = ( selector, preserveWhiteSpace ) => ( el ) => { + const target = selector ? el.querySelector( selector ) : el; + return target + ? RichTextData.fromHTMLElement( target, { preserveWhiteSpace } ) + : RichTextData.empty(); +}; diff --git a/packages/blocks/src/api/parser/get-block-attributes.js b/packages/blocks/src/api/parser/get-block-attributes.js index cc81c108005529..24faae73704636 100644 --- a/packages/blocks/src/api/parser/get-block-attributes.js +++ b/packages/blocks/src/api/parser/get-block-attributes.js @@ -9,12 +9,22 @@ import memoize from 'memize'; */ import { pipe } from '@wordpress/compose'; import { applyFilters } from '@wordpress/hooks'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies */ -import { attr, html, text, query, node, children, prop } from '../matchers'; -import { normalizeBlockType } from '../utils'; +import { + attr, + html, + text, + query, + node, + children, + prop, + richText, +} from '../matchers'; +import { normalizeBlockType, getDefault } from '../utils'; /** * Higher-order hpq matcher which enhances an attribute matcher to return true @@ -58,6 +68,9 @@ export const toBooleanAttributeMatcher = ( matcher ) => */ export function isOfType( value, type ) { switch ( type ) { + case 'rich-text': + return value instanceof RichTextData; + case 'string': return typeof value === 'string'; @@ -134,6 +147,7 @@ export function getBlockAttribute( case 'property': case 'html': case 'text': + case 'rich-text': case 'children': case 'node': case 'query': @@ -152,7 +166,7 @@ export function getBlockAttribute( } if ( value === undefined ) { - value = attributeSchema.default; + value = getDefault( attributeSchema ); } return value; @@ -211,6 +225,11 @@ export const matcherFromSource = memoize( ( sourceConfig ) => { return html( sourceConfig.selector, sourceConfig.multiline ); case 'text': return text( sourceConfig.selector ); + case 'rich-text': + return richText( + sourceConfig.selector, + sourceConfig.__unstablePreserveWhiteSpace + ); case 'children': return children( sourceConfig.selector ); case 'node': diff --git a/packages/blocks/src/api/raw-handling/test/paste-handler.js b/packages/blocks/src/api/raw-handling/test/paste-handler.js index 6938ad0d9c4081..9b3dad39a0a5b8 100644 --- a/packages/blocks/src/api/raw-handling/test/paste-handler.js +++ b/packages/blocks/src/api/raw-handling/test/paste-handler.js @@ -73,9 +73,9 @@ describe( 'pasteHandler', () => { expect( console ).toHaveLogged(); + delete result.attributes.caption; expect( result.attributes ).toEqual( { hasFixedLayout: false, - caption: '', head: [ { cells: [ @@ -113,9 +113,9 @@ describe( 'pasteHandler', () => { expect( console ).toHaveLogged(); + delete result.attributes.caption; expect( result.attributes ).toEqual( { hasFixedLayout: false, - caption: '', head: [ { cells: [ diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index c43445c6272264..60a94117b36e23 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -11,6 +11,7 @@ import a11yPlugin from 'colord/plugins/a11y'; import { Component, isValidElement } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -47,8 +48,12 @@ export function isUnmodifiedBlock( block ) { const newBlock = isUnmodifiedBlock[ block.name ]; const blockType = getBlockType( block.name ); - return Object.keys( blockType?.attributes ?? {} ).every( - ( key ) => newBlock.attributes[ key ] === block.attributes[ key ] + function isEqual( a, b ) { + return ( a?.valueOf() ?? a ) === ( b?.valueOf() ?? b ); + } + + return Object.keys( blockType?.attributes ?? {} ).every( ( key ) => + isEqual( newBlock.attributes[ key ], block.attributes[ key ] ) ); } @@ -243,6 +248,16 @@ export function getAccessibleBlockLabel( ); } +export function getDefault( attributeSchema ) { + if ( attributeSchema.default !== undefined ) { + return attributeSchema.default; + } + + if ( attributeSchema.type === 'rich-text' ) { + return new RichTextData(); + } +} + /** * Ensure attributes contains only values defined by block type, and merge * default values for missing attributes. @@ -264,9 +279,26 @@ export function __experimentalSanitizeBlockAttributes( name, attributes ) { const value = attributes[ key ]; if ( undefined !== value ) { - accumulator[ key ] = value; - } else if ( schema.hasOwnProperty( 'default' ) ) { - accumulator[ key ] = schema.default; + if ( schema.type === 'rich-text' ) { + if ( value instanceof RichTextData ) { + accumulator[ key ] = value; + } else if ( typeof value === 'string' ) { + accumulator[ key ] = + RichTextData.fromHTMLString( value ); + } + } else if ( + schema.type === 'string' && + value instanceof RichTextData + ) { + accumulator[ key ] = value.toHTMLString(); + } else { + accumulator[ key ] = value; + } + } else { + const _default = getDefault( schema ); + if ( undefined !== _default ) { + accumulator[ key ] = _default; + } } if ( [ 'node', 'children' ].indexOf( schema.source ) !== -1 ) { diff --git a/packages/core-data/src/footnotes/get-footnotes-order.js b/packages/core-data/src/footnotes/get-footnotes-order.js index 42adeed7621e8f..fcaeae660ec1aa 100644 --- a/packages/core-data/src/footnotes/get-footnotes-order.js +++ b/packages/core-data/src/footnotes/get-footnotes-order.js @@ -1,8 +1,3 @@ -/** - * WordPress dependencies - */ -import { create } from '@wordpress/rich-text'; - /** * Internal dependencies */ @@ -14,18 +9,16 @@ function getBlockFootnotesOrder( block ) { if ( ! cache.has( block ) ) { const order = []; for ( const value of getRichTextValuesCached( block ) ) { - if ( ! value || ! value.includes( 'data-fn' ) ) { + if ( ! value ) { continue; } // replacements is a sparse array, use forEach to skip empty slots. - create( { html: value } ).replacements.forEach( - ( { type, attributes } ) => { - if ( type === 'core/footnote' ) { - order.push( attributes[ 'data-fn' ] ); - } + value.replacements.forEach( ( { type, attributes } ) => { + if ( type === 'core/footnote' ) { + order.push( attributes[ 'data-fn' ] ); } - ); + } ); } cache.set( block, order ); } diff --git a/packages/core-data/src/footnotes/index.js b/packages/core-data/src/footnotes/index.js index fa1c5fad5c7e7d..9458290f9cb40b 100644 --- a/packages/core-data/src/footnotes/index.js +++ b/packages/core-data/src/footnotes/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { create, toHTMLString } from '@wordpress/rich-text'; +import { RichTextData, create, toHTMLString } from '@wordpress/rich-text'; /** * Internal dependencies @@ -53,15 +53,18 @@ export function updateFootnotesFromMeta( blocks, meta ) { continue; } - if ( typeof value !== 'string' ) { + // To do, remove support for string values? + if ( + typeof value !== 'string' && + ! ( value instanceof RichTextData ) + ) { continue; } - if ( value.indexOf( 'data-fn' ) === -1 ) { - continue; - } - - const richTextValue = create( { html: value } ); + const richTextValue = + typeof value === 'string' + ? RichTextData.fromHTMLString( value ) + : value; richTextValue.replacements.forEach( ( replacement ) => { if ( replacement.type === 'core/footnote' ) { @@ -78,7 +81,10 @@ export function updateFootnotesFromMeta( blocks, meta ) { } } ); - attributes[ key ] = toHTMLString( { value: richTextValue } ); + attributes[ key ] = + typeof value === 'string' + ? richTextValue.toHTMLString() + : richTextValue; } return attributes; diff --git a/packages/rich-text/README.md b/packages/rich-text/README.md index 90726ff238c1bd..90fd15e1c905c5 100644 --- a/packages/rich-text/README.md +++ b/packages/rich-text/README.md @@ -355,6 +355,19 @@ _Returns_ - `RichTextValue`: A new value with replacements applied. +### RichTextData + +The RichTextData class is used to instantiate a wrapper around rich text values, with methods that can be used to transform or manipulate the data. + +- Create an emtpy instance: `new RichTextData()`. +- Create one from an html string: `RichTextData.fromHTMLString( +'<em>hello</em>' )`. +- Create one from a wrapper HTMLElement: `RichTextData.fromHTMLElement( +document.querySelector( 'p' ) )`. +- Create one from plain text: `RichTextData.fromPlainText( '1\n2' )`. +- Create one from a rich text value: `new RichTextData( { text: '...', +formats: [ ... ] } )`. + ### RichTextValue An object which represents a formatted string. See main `@wordpress/rich-text` documentation for more information. diff --git a/packages/rich-text/src/component/index.js b/packages/rich-text/src/component/index.js index 0e9291b7a5e03d..a2b5734d5c2048 100644 --- a/packages/rich-text/src/component/index.js +++ b/packages/rich-text/src/component/index.js @@ -8,7 +8,7 @@ import { useRegistry } from '@wordpress/data'; /** * Internal dependencies */ -import { collapseWhiteSpace, create } from '../create'; +import { create, RichTextData } from '../create'; import { apply } from '../to-dom'; import { toHTMLString } from '../to-html-string'; import { useDefaultStyle } from './use-default-style'; @@ -70,11 +70,18 @@ export function useRichText( { function setRecordFromProps() { _value.current = value; - record.current = create( { - html: preserveWhiteSpace - ? value - : collapseWhiteSpace( typeof value === 'string' ? value : '' ), - } ); + record.current = value; + if ( ! ( value instanceof RichTextData ) ) { + record.current = value + ? RichTextData.fromHTMLString( value, { preserveWhiteSpace } ) + : RichTextData.empty(); + } + // To do: make rich text internally work with RichTextData. + record.current = { + text: record.current.text, + formats: record.current.formats, + replacements: record.current.replacements, + }; if ( disableFormats ) { record.current.formats = Array( value.length ); record.current.replacements = Array( value.length ); @@ -117,17 +124,18 @@ export function useRichText( { if ( disableFormats ) { _value.current = newRecord.text; } else { - _value.current = toHTMLString( { - value: __unstableBeforeSerialize - ? { - ...newRecord, - formats: __unstableBeforeSerialize( newRecord ), - } - : newRecord, - } ); + const newFormats = __unstableBeforeSerialize + ? __unstableBeforeSerialize( newRecord ) + : newRecord.formats; + newRecord = { ...newRecord, formats: newFormats }; + if ( typeof value === 'string' ) { + _value.current = toHTMLString( { value: newRecord } ); + } else { + _value.current = new RichTextData( newRecord ); + } } - const { start, end, formats, text } = newRecord; + const { start, end, formats, text } = record.current; // Selection must be updated first, so it is recorded in history when // the content change happens. diff --git a/packages/rich-text/src/create.js b/packages/rich-text/src/create.js index a23baf70078bc9..a35fabbd4e2fad 100644 --- a/packages/rich-text/src/create.js +++ b/packages/rich-text/src/create.js @@ -10,6 +10,8 @@ import { store as richTextStore } from './store'; import { createElement } from './create-element'; import { mergePair } from './concat'; import { OBJECT_REPLACEMENT_CHARACTER, ZWNBSP } from './special-characters'; +import { toHTMLString } from './to-html-string'; +import { getTextContent } from './get-text-content'; /** @typedef {import('./types').RichTextValue} RichTextValue */ @@ -96,6 +98,86 @@ function toFormat( { tagName, attributes } ) { }; } +// Ideally we use a private property. +const RichTextInternalData = Symbol( 'RichTextInternalData' ); + +/** + * The RichTextData class is used to instantiate a wrapper around rich text + * values, with methods that can be used to transform or manipulate the data. + * + * - Create an emtpy instance: `new RichTextData()`. + * - Create one from an html string: `RichTextData.fromHTMLString( + * '<em>hello</em>' )`. + * - Create one from a wrapper HTMLElement: `RichTextData.fromHTMLElement( + * document.querySelector( 'p' ) )`. + * - Create one from plain text: `RichTextData.fromPlainText( '1\n2' )`. + * - Create one from a rich text value: `new RichTextData( { text: '...', + * formats: [ ... ] } )`. + * + * @todo Add methods to manipulate the data, such as applyFormat, slice etc. + */ +export class RichTextData { + static empty() { + return new RichTextData(); + } + static fromPlainText( text ) { + return new RichTextData( create( { text } ) ); + } + static fromHTMLString( html ) { + return new RichTextData( create( { html } ) ); + } + static fromHTMLElement( htmlElement, options = {} ) { + const { preserveWhiteSpace = false } = options; + const element = preserveWhiteSpace + ? htmlElement + : collapseWhiteSpace( htmlElement ); + const richTextData = new RichTextData( create( { element } ) ); + Object.defineProperty( richTextData, 'originalHTML', { + value: htmlElement.innerHTML, + } ); + return richTextData; + } + constructor( init = createEmptyValue() ) { + // Setting text, formats, and replacements as enumerable properties + // unfortunately visualises these in the e2e tests. As long as the class + // instance doesn't have any enumerable properties, it will be + // visualised as a string. + Object.defineProperty( this, RichTextInternalData, { value: init } ); + } + toPlainText() { + return getTextContent( this[ RichTextInternalData ] ); + } + // We could expose `toHTMLElement` at some point as well, but we'd only use + // it internally. + toHTMLString() { + return ( + this.originalHTML || + toHTMLString( { value: this[ RichTextInternalData ] } ) + ); + } + valueOf() { + return this.toHTMLString(); + } + toString() { + return this.toHTMLString(); + } + toJSON() { + return this.toHTMLString(); + } + get length() { + return this.text.length; + } + get formats() { + return this[ RichTextInternalData ].formats; + } + get replacements() { + return this[ RichTextInternalData ].replacements; + } + get text() { + return this[ RichTextInternalData ].text; + } +} + /** * Create a RichText value from an `Element` tree (DOM), an HTML string or a * plain text string, with optionally a `Range` object to set the selection. If @@ -128,7 +210,6 @@ function toFormat( { tagName, attributes } ) { * @param {string} [$1.html] HTML to create value from. * @param {Range} [$1.range] Range to create value from. * @param {boolean} [$1.__unstableIsEditableTree] - * * @return {RichTextValue} A rich text value. */ export function create( { @@ -138,6 +219,14 @@ export function create( { range, __unstableIsEditableTree: isEditableTree, } = {} ) { + if ( html instanceof RichTextData ) { + return { + text: html.text, + formats: html.formats, + replacements: html.replacements, + }; + } + if ( typeof text === 'string' && text.length > 0 ) { return { formats: Array( text.length ), @@ -268,10 +357,42 @@ function filterRange( node, range, filter ) { * @see * https://developer.mozilla.org/en-US/docs/Web/CSS/white-space-collapse#collapsing_of_white_space * - * @param {string} string + * @param {HTMLElement} element + * @param {boolean} isRoot + * + * @return {HTMLElement} New element with collapsed whitespace. */ -export function collapseWhiteSpace( string ) { - return string.replace( /[\n\r\t]+/g, ' ' ); +function collapseWhiteSpace( element, isRoot = true ) { + const clone = element.cloneNode( true ); + clone.normalize(); + Array.from( clone.childNodes ).forEach( ( node, i, nodes ) => { + if ( node.nodeType === node.TEXT_NODE ) { + let newNodeValue = node.nodeValue; + + if ( /[\n\t\r\f]/.test( newNodeValue ) ) { + newNodeValue = newNodeValue.replace( /[\n\t\r\f]+/g, ' ' ); + } + + if ( newNodeValue.indexOf( ' ' ) !== -1 ) { + newNodeValue = newNodeValue.replace( / {2,}/g, ' ' ); + } + + if ( i === 0 && newNodeValue.startsWith( ' ' ) ) { + newNodeValue = newNodeValue.slice( 1 ); + } else if ( + isRoot && + i === nodes.length - 1 && + newNodeValue.endsWith( ' ' ) + ) { + newNodeValue = newNodeValue.slice( 0, -1 ); + } + + node.nodeValue = newNodeValue; + } else if ( node.nodeType === node.ELEMENT_NODE ) { + collapseWhiteSpace( node, false ); + } + } ); + return clone; } /** diff --git a/packages/rich-text/src/index.ts b/packages/rich-text/src/index.ts index 14d26cab8f7fb1..f82317d81573d0 100644 --- a/packages/rich-text/src/index.ts +++ b/packages/rich-text/src/index.ts @@ -1,7 +1,7 @@ export { store } from './store'; export { applyFormat } from './apply-format'; export { concat } from './concat'; -export { create } from './create'; +export { RichTextData, create } from './create'; export { getActiveFormat } from './get-active-format'; export { getActiveFormats } from './get-active-formats'; export { getActiveObject } from './get-active-object'; diff --git a/schemas/json/block.json b/schemas/json/block.json index d5b4a04452eaa6..7e0c8715a4abda 100644 --- a/schemas/json/block.json +++ b/schemas/json/block.json @@ -114,6 +114,7 @@ "object", "array", "string", + "rich-text", "integer", "number" ] @@ -159,6 +160,7 @@ "enum": [ "attribute", "text", + "rich-text", "html", "raw", "query", diff --git a/test/integration/fixtures/blocks/core__gallery__deprecated-1.json b/test/integration/fixtures/blocks/core__gallery__deprecated-1.json index 9e15ee7f1c7149..bd6108a97230a3 100644 --- a/test/integration/fixtures/blocks/core__gallery__deprecated-1.json +++ b/test/integration/fixtures/blocks/core__gallery__deprecated-1.json @@ -16,6 +16,7 @@ "attributes": { "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", "alt": "title", + "caption": "", "linkDestination": "none" }, "innerBlocks": [] @@ -26,6 +27,7 @@ "attributes": { "url": "data:image/jpeg;base64,/9j/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/yQALCAABAAEBAREA/8wABgAQEAX/2gAIAQEAAD8A0s8g/9k=", "alt": "title", + "caption": "", "linkDestination": "none" }, "innerBlocks": [] diff --git a/test/integration/fixtures/documents/ms-word-online-out.html b/test/integration/fixtures/documents/ms-word-online-out.html index 398281520f2542..8187b598f9a91a 100644 --- a/test/integration/fixtures/documents/ms-word-online-out.html +++ b/test/integration/fixtures/documents/ms-word-online-out.html @@ -8,33 +8,33 @@ <!-- wp:list --> <ul><!-- wp:list-item --> -<li>A&nbsp;</li> +<li>A </li> <!-- /wp:list-item --> <!-- wp:list-item --> -<li>Bulleted&nbsp;</li> +<li>Bulleted </li> <!-- /wp:list-item --> <!-- wp:list-item --> -<li>Indented&nbsp;</li> +<li>Indented </li> <!-- /wp:list-item --> <!-- wp:list-item --> -<li>List&nbsp;</li> +<li>List </li> <!-- /wp:list-item --></ul> <!-- /wp:list --> <!-- wp:list {"ordered":true,"start":1} --> <ol start="1"><!-- wp:list-item --> -<li>One&nbsp;</li> +<li>One </li> <!-- /wp:list-item --> <!-- wp:list-item --> -<li>Two&nbsp;</li> +<li>Two </li> <!-- /wp:list-item --> <!-- wp:list-item --> -<li>Three&nbsp;</li> +<li>Three </li> <!-- /wp:list-item --></ol> <!-- /wp:list --> diff --git a/test/integration/non-matched-tags-handling.test.js b/test/integration/non-matched-tags-handling.test.js index 67438192f13680..451a628c329775 100644 --- a/test/integration/non-matched-tags-handling.test.js +++ b/test/integration/non-matched-tags-handling.test.js @@ -19,9 +19,9 @@ describe( 'Handling of non matched tags in block transforms', () => { expect( simplePreformattedResult ).toHaveLength( 1 ); expect( simplePreformattedResult[ 0 ].name ).toBe( 'core/paragraph' ); - expect( simplePreformattedResult[ 0 ].attributes.content ).toBe( - 'Pre' - ); + expect( + simplePreformattedResult[ 0 ].attributes.content.valueOf() + ).toBe( 'Pre' ); const codeResult = pasteHandler( { HTML: '<pre><code>code</code></pre>', @@ -30,7 +30,7 @@ describe( 'Handling of non matched tags in block transforms', () => { expect( codeResult ).toHaveLength( 1 ); expect( codeResult[ 0 ].name ).toBe( 'core/code' ); - expect( codeResult[ 0 ].attributes.content ).toBe( 'code' ); + expect( codeResult[ 0 ].attributes.content.valueOf() ).toBe( 'code' ); expect( console ).toHaveLogged(); } ); } ); From f6adf1e98b7e49cea4c95281ad225128b94e4e13 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Thu, 7 Dec 2023 13:13:22 -0800 Subject: [PATCH 085/325] `FontSizePicker`: Add opt-in prop for 40px default size (#56804) * `FontSizePicker`: Add opt-in prop for 40px default size * Update button when __next40pxDefaultSize is true * Update changelog --- packages/components/CHANGELOG.md | 1 + .../font-size-picker/font-size-picker-select.tsx | 2 ++ .../font-size-picker-toggle-group.tsx | 10 +++++++++- packages/components/src/font-size-picker/index.tsx | 14 +++++++++++--- packages/components/src/font-size-picker/types.ts | 9 ++++++++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index c4153503fd798f..8a6d52e7582833 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -10,6 +10,7 @@ - `PaletteEdit`: Gradient pickers to use same width as color pickers ([#56801](https://github.com/WordPress/gutenberg/pull/56801)). - `FocalPointPicker`: Add opt-in prop for 40px default size ([#56021](https://github.com/WordPress/gutenberg/pull/56021)). - `DimensionControl`: Add opt-in prop for 40px default size ([#56805](https://github.com/WordPress/gutenberg/pull/56805)). +- `FontSizePicker`: Add opt-in prop for 40px default size ([#56804](https://github.com/WordPress/gutenberg/pull/56804)). ### Bug Fix diff --git a/packages/components/src/font-size-picker/font-size-picker-select.tsx b/packages/components/src/font-size-picker/font-size-picker-select.tsx index d3fc2ffe4a61fd..32438cfab81153 100644 --- a/packages/components/src/font-size-picker/font-size-picker-select.tsx +++ b/packages/components/src/font-size-picker/font-size-picker-select.tsx @@ -27,6 +27,7 @@ const CUSTOM_OPTION: FontSizePickerSelectOption = { const FontSizePickerSelect = ( props: FontSizePickerSelectProps ) => { const { + __next40pxDefaultSize, fontSizes, value, disableCustomFontSizes, @@ -67,6 +68,7 @@ const FontSizePickerSelect = ( props: FontSizePickerSelectProps ) => { return ( <CustomSelectControl + __next40pxDefaultSize={ __next40pxDefaultSize } __nextUnconstrainedWidth className="components-font-size-picker__select" label={ __( 'Font size' ) } diff --git a/packages/components/src/font-size-picker/font-size-picker-toggle-group.tsx b/packages/components/src/font-size-picker/font-size-picker-toggle-group.tsx index 697d9e11b67e47..69c86ecfc817bf 100644 --- a/packages/components/src/font-size-picker/font-size-picker-toggle-group.tsx +++ b/packages/components/src/font-size-picker/font-size-picker-toggle-group.tsx @@ -14,10 +14,18 @@ import { T_SHIRT_ABBREVIATIONS, T_SHIRT_NAMES } from './constants'; import type { FontSizePickerToggleGroupProps } from './types'; const FontSizePickerToggleGroup = ( props: FontSizePickerToggleGroupProps ) => { - const { fontSizes, value, __nextHasNoMarginBottom, size, onChange } = props; + const { + fontSizes, + value, + __nextHasNoMarginBottom, + __next40pxDefaultSize, + size, + onChange, + } = props; return ( <ToggleGroupControl __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __next40pxDefaultSize={ __next40pxDefaultSize } label={ __( 'Font size' ) } hideLabelFromVision value={ value } diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index e454b3093bf6a5..38488cf9fbb0e6 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -45,6 +45,7 @@ const UnforwardedFontSizePicker = ( const { /** Start opting into the new margin-free styles that will become the default in a future version. */ __nextHasNoMarginBottom = false, + __next40pxDefaultSize = false, fallbackFontSize, fontSizes = [], disableCustomFontSizes = false, @@ -165,6 +166,7 @@ const UnforwardedFontSizePicker = ( shouldUseSelectControl && ! showCustomValueControl && ( <FontSizePickerSelect + __next40pxDefaultSize={ __next40pxDefaultSize } fontSizes={ fontSizes } value={ value } disableCustomFontSizes={ disableCustomFontSizes } @@ -194,6 +196,7 @@ const UnforwardedFontSizePicker = ( fontSizes={ fontSizes } value={ value } __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __next40pxDefaultSize={ __next40pxDefaultSize } size={ size } onChange={ ( newValue ) => { if ( newValue === undefined ) { @@ -214,6 +217,7 @@ const UnforwardedFontSizePicker = ( <Flex className="components-font-size-picker__custom-size-control"> <FlexItem isBlock> <UnitControl + __next40pxDefaultSize={ __next40pxDefaultSize } label={ __( 'Custom' ) } labelPosition="top" hideLabelFromVision @@ -241,6 +245,9 @@ const UnforwardedFontSizePicker = ( __nextHasNoMarginBottom={ __nextHasNoMarginBottom } + __next40pxDefaultSize={ + __next40pxDefaultSize + } className="components-font-size-picker__custom-input" label={ __( 'Custom Size' ) } hideLabelFromVision @@ -276,9 +283,10 @@ const UnforwardedFontSizePicker = ( variant="secondary" __next40pxDefaultSize size={ - size !== '__unstable-large' - ? 'small' - : 'default' + size === '__unstable-large' || + props.__next40pxDefaultSize + ? 'default' + : 'small' } > { __( 'Reset' ) } diff --git a/packages/components/src/font-size-picker/types.ts b/packages/components/src/font-size-picker/types.ts index f4d00c2d3ce674..93634172224582 100644 --- a/packages/components/src/font-size-picker/types.ts +++ b/packages/components/src/font-size-picker/types.ts @@ -57,6 +57,12 @@ export type FontSizePickerProps = { * @default false */ __nextHasNoMarginBottom?: boolean; + /** + * Start opting into the larger default height that will become the default size in a future version. + * + * @default false + */ + __next40pxDefaultSize?: boolean; /** * Size of the control. * @@ -93,6 +99,7 @@ export type FontSizePickerSelectProps = Pick< >; onChange: NonNullable< FontSizePickerProps[ 'onChange' ] >; onSelectCustom: () => void; + __next40pxDefaultSize: boolean; }; export type FontSizePickerSelectOption = { @@ -104,7 +111,7 @@ export type FontSizePickerSelectOption = { export type FontSizePickerToggleGroupProps = Pick< FontSizePickerProps, - 'value' | 'size' | '__nextHasNoMarginBottom' + 'value' | 'size' | '__nextHasNoMarginBottom' | '__next40pxDefaultSize' > & { fontSizes: NonNullable< FontSizePickerProps[ 'fontSizes' ] >; onChange: NonNullable< FontSizePickerProps[ 'onChange' ] >; From f48ac055e68bafb869314fa7a9055303a3a1ae8c Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:01:56 +0200 Subject: [PATCH 086/325] Keycodes: avoid regex for capital case (#56822) --- package-lock.json | 6 ++--- .../test/__snapshots__/index.js.snap | 2 +- packages/keycodes/package.json | 3 +-- packages/keycodes/src/index.js | 27 +++++++++---------- 4 files changed, 17 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 86cf832a8e41dd..b47ce2e24a9e77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55868,8 +55868,7 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2" + "@wordpress/i18n": "file:../i18n" }, "engines": { "node": ">=12" @@ -70963,8 +70962,7 @@ "version": "file:packages/keycodes", "requires": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2" + "@wordpress/i18n": "file:../i18n" } }, "@wordpress/lazy-import": { diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap index b98bd562f0a6a3..79990664a2427b 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap @@ -898,7 +898,7 @@ exports[`KeyboardShortcutHelpModal should match snapshot when the modal is activ class="edit-post-keyboard-shortcut-help-modal__shortcut-term" > <kbd - aria-label="Shift + Alt + 1 6" + aria-label="Shift + Alt + 1-6" class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" > <kbd diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index c98ccc24cb5d84..9531b4980c1e20 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -28,8 +28,7 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@wordpress/i18n": "file:../i18n", - "change-case": "^4.1.2" + "@wordpress/i18n": "file:../i18n" }, "publishConfig": { "access": "public" diff --git a/packages/keycodes/src/index.js b/packages/keycodes/src/index.js index a9efb210496c3c..6b01109e2a40db 100644 --- a/packages/keycodes/src/index.js +++ b/packages/keycodes/src/index.js @@ -9,11 +9,6 @@ * shortcut combos directly to keyboardShortcut(). */ -/** - * External dependencies - */ -import { capitalCase } from 'change-case'; - /** * WordPress dependencies */ @@ -148,6 +143,17 @@ export const ZERO = 48; export { isAppleOS }; +/** + * Capitalise the first character of a string. + * @param {string} string String to capitalise. + * @return {string} Capitalised string. + */ +function capitaliseFirstCharacter( string ) { + return string.length < 2 + ? string.toUpperCase() + : string.charAt( 0 ).toUpperCase() + string.slice( 1 ); +} + /** * Map the values of an object with a specified callback and return the result object. * @@ -260,14 +266,7 @@ export const displayShortcutList = mapValues( /** @type {string[]} */ ( [] ) ); - // Symbols (~`,.) are removed by the default regular expression, - // so override the rule to allow symbols used for shortcuts. - // see: https://github.com/blakeembrey/change-case#options - const capitalizedCharacter = capitalCase( character, { - stripRegexp: /[^A-Z0-9~`,\.\\\-]/gi, - } ); - - return [ ...modifierKeys, capitalizedCharacter ]; + return [ ...modifierKeys, capitaliseFirstCharacter( character ) ]; }; } ); @@ -335,7 +334,7 @@ export const shortcutAriaLabel = mapValues( return [ ...modifier( _isApple ), character ] .map( ( key ) => - capitalCase( replacementKeyMap[ key ] ?? key ) + capitaliseFirstCharacter( replacementKeyMap[ key ] ?? key ) ) .join( isApple ? ' ' : ' + ' ); }; From b45f22c3e2d33367a43122e6e13ce5ac41cf1ebf Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Thu, 7 Dec 2023 16:16:08 -0600 Subject: [PATCH 087/325] Refactor <BlockToolbar />: Add variant prop and <NavigableToolbar /> wrapper (#56335) - Combine `<BlockContextualToolbar />` into `<BlockToolbar />`. This brings all the necessary functionality for `<BlockToolbar />` to be used, mainly by wrapping it in a `<NavigableToolbar />`. - Remove `<BlockContextualToolbar />`. This is no longer needed, and was originally intended for Contextual to mean Popover. - Replace usage of `<BlockContextualToobar />` in edit-site, edit-post, edit-widget, etc headers with `<BlockToolbar />` - Refactor `<SelectedBlockTools />` to become `<BlockToolbarPopover />` to better reflect its purpose. - Add `<PrivateBlockToolbar />` with new props of `__experimentalInitialIndex`, `__experimentalOnIndexChange`, and `focusOnMount`. - Export `<BlockToolbar />` from `<PrivateBlockToolbar />` with locked `__experimentalInitialIndex`, `__experimentalOnIndexChange`, and `focusOnMount` as `undefined`. Public API props are `hideDragHandles` and `variant` with a default of `unstyled`. - Remove concept of `isFixed` from the `<BlockToolbar />`. The styles of the current small-screen fixed toolbar will become the default. The popover and top toolbars will have its own CSS overrides applied. - Split `<BlockToolbarPopover />` and `<BlockBreadcrumbPopover />` into separate components for simplicity. - Top Toolbar now means. You have to implement the `<BlockToolbar />` on your own. --- packages/block-editor/README.md | 8 + .../block-parent-selector/style.scss | 11 - .../src/components/block-toolbar/index.js | 275 ++++++++++++------ .../src/components/block-toolbar/style.scss | 116 ++++---- .../src/components/block-tools/back-compat.js | 4 +- .../block-tools/block-contextual-toolbar.js | 100 ------- .../block-tools/block-toolbar-breadcrumb.js | 46 +++ .../block-tools/block-toolbar-popover.js | 90 ++++++ .../src/components/block-tools/index.js | 71 +++-- .../block-tools/selected-block-tools.js | 127 -------- .../src/components/block-tools/style.scss | 232 ++++----------- .../components/navigable-toolbar/README.md | 2 + .../src/components/navigable-toolbar/index.js | 4 +- packages/block-editor/src/private-apis.js | 2 - packages/block-editor/src/style.scss | 1 - .../src/components/header/index.js | 27 +- .../src/components/header/style.scss | 2 +- .../components/sidebar-block-editor/index.js | 14 +- .../sidebar-block-editor/style.scss | 20 -- .../edit-post/src/components/header/index.js | 7 +- .../src/components/header/style.scss | 55 +++- .../edit-post/src/components/layout/index.js | 18 +- packages/edit-post/src/editor.js | 9 +- .../src/components/block-editor/style.scss | 11 - .../block-editor/use-site-editor-settings.js | 80 ++--- .../edit-site/src/components/editor/index.js | 8 +- .../src/components/header-edit-mode/index.js | 6 +- .../components/header-edit-mode/style.scss | 41 ++- .../src/components/header/index.js | 7 +- .../src/components/header/style.scss | 27 +- .../index.js | 4 + .../index.js | 5 +- storybook/stories/playground/box/index.js | 2 + .../playground/with-undo-redo/index.js | 2 + .../playground/with-undo-redo/style.css | 6 +- .../specs/editor/various/is-typing.spec.js | 40 ++- .../various/shortcut-focus-toolbar.spec.js | 4 + 37 files changed, 725 insertions(+), 759 deletions(-) delete mode 100644 packages/block-editor/src/components/block-parent-selector/style.scss delete mode 100644 packages/block-editor/src/components/block-tools/block-contextual-toolbar.js create mode 100644 packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js create mode 100644 packages/block-editor/src/components/block-tools/block-toolbar-popover.js delete mode 100644 packages/block-editor/src/components/block-tools/selected-block-tools.js diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 2d6a5627a52a44..56ab5f1bd94d93 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -280,10 +280,18 @@ _Returns_ ### BlockToolbar +Renders the block toolbar. + _Related_ - <https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-toolbar/README.md> +_Parameters_ + +- _props_ `Object`: Components props. +- _props.hideDragHandle_ `boolean`: Show or hide the Drag Handle for drag and drop functionality. +- _props.variant_ `string`: Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. + ### BlockTools Renders block tools (the block toolbar, select/navigation mode toolbar, the insertion point and a slot for the inline rich text toolbar). Must be wrapped around the block content and editor styles wrapper or iframe. diff --git a/packages/block-editor/src/components/block-parent-selector/style.scss b/packages/block-editor/src/components/block-parent-selector/style.scss deleted file mode 100644 index c5a1869835188c..00000000000000 --- a/packages/block-editor/src/components/block-parent-selector/style.scss +++ /dev/null @@ -1,11 +0,0 @@ -.block-editor-block-parent-selector { - background: $white; - border-radius: $radius-block-ui; - - .block-editor-block-parent-selector__button { - width: $grid-unit-60; - height: $grid-unit-60; - border: $border-width solid $gray-900; - border-radius: $radius-block-ui; - } -} diff --git a/packages/block-editor/src/components/block-toolbar/index.js b/packages/block-editor/src/components/block-toolbar/index.js index 963cd8a475328a..7bb52a7e8f0906 100644 --- a/packages/block-editor/src/components/block-toolbar/index.js +++ b/packages/block-editor/src/components/block-toolbar/index.js @@ -6,6 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ +import { __ } from '@wordpress/i18n'; import { useSelect } from '@wordpress/data'; import { useRef } from '@wordpress/element'; import { useViewportMatch } from '@wordpress/compose'; @@ -32,38 +33,82 @@ import BlockEditVisuallyButton from '../block-edit-visually-button'; import { useShowHoveredOrFocusedGestures } from './utils'; import { store as blockEditorStore } from '../../store'; import __unstableBlockNameContext from './block-name-context'; +import NavigableToolbar from '../navigable-toolbar'; +import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; -const BlockToolbar = ( { hideDragHandle } ) => { - const { blockClientIds, blockType, isValid, isVisual, blockEditingMode } = - useSelect( ( select ) => { - const { - getBlockName, - getBlockMode, - getSelectedBlockClientIds, - isBlockValid, - getBlockRootClientId, - getBlockEditingMode, - } = select( blockEditorStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - const selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const blockRootClientId = getBlockRootClientId( - selectedBlockClientId - ); - return { - blockClientIds: selectedBlockClientIds, - blockType: - selectedBlockClientId && - getBlockType( getBlockName( selectedBlockClientId ) ), - rootClientId: blockRootClientId, - isValid: selectedBlockClientIds.every( ( id ) => - isBlockValid( id ) - ), - isVisual: selectedBlockClientIds.every( - ( id ) => getBlockMode( id ) === 'visual' - ), - blockEditingMode: getBlockEditingMode( selectedBlockClientId ), - }; - }, [] ); +/** + * Renders the block toolbar. + * + * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-toolbar/README.md + * + * @param {Object} props Components props. + * @param {boolean} props.hideDragHandle Show or hide the Drag Handle for drag and drop functionality. + * @param {boolean} props.focusOnMount Focus the toolbar when mounted. + * @param {number} props.__experimentalInitialIndex The initial index of the toolbar item to focus. + * @param {Function} props.__experimentalOnIndexChange Callback function to be called when the index of the focused toolbar item changes. + * @param {string} props.variant Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. + */ +export function PrivateBlockToolbar( { + hideDragHandle, + focusOnMount, + __experimentalInitialIndex, + __experimentalOnIndexChange, + variant = 'unstyled', +} ) { + const { + blockClientId, + blockClientIds, + isDefaultEditingMode, + blockType, + shouldShowVisualToolbar, + showParentSelector, + } = useSelect( ( select ) => { + const { + getBlockName, + getBlockMode, + getBlockParents, + getSelectedBlockClientIds, + isBlockValid, + getBlockRootClientId, + getBlockEditingMode, + } = select( blockEditorStore ); + const selectedBlockClientIds = getSelectedBlockClientIds(); + const selectedBlockClientId = selectedBlockClientIds[ 0 ]; + const blockRootClientId = getBlockRootClientId( selectedBlockClientId ); + const parents = getBlockParents( selectedBlockClientId ); + const firstParentClientId = parents[ parents.length - 1 ]; + const parentBlockName = getBlockName( firstParentClientId ); + const parentBlockType = getBlockType( parentBlockName ); + const _isDefaultEditingMode = + getBlockEditingMode( selectedBlockClientId ) === 'default'; + const isValid = selectedBlockClientIds.every( ( id ) => + isBlockValid( id ) + ); + const isVisual = selectedBlockClientIds.every( + ( id ) => getBlockMode( id ) === 'visual' + ); + return { + blockClientId: selectedBlockClientId, + blockClientIds: selectedBlockClientIds, + isDefaultEditingMode: _isDefaultEditingMode, + blockType: + selectedBlockClientId && + getBlockType( getBlockName( selectedBlockClientId ) ), + + shouldShowVisualToolbar: isValid && isVisual, + rootClientId: blockRootClientId, + showParentSelector: + parentBlockType && + getBlockEditingMode( firstParentClientId ) === 'default' && + hasBlockSupport( + parentBlockType, + '__experimentalParentSelector', + true + ) && + selectedBlockClientIds.length === 1 && + _isDefaultEditingMode, + }; + }, [] ); const toolbarWrapperRef = useRef( null ); @@ -76,86 +121,126 @@ const BlockToolbar = ( { hideDragHandle } ) => { const isLargeViewport = ! useViewportMatch( 'medium', '<' ); - if ( blockType ) { - if ( ! hasBlockSupport( blockType, '__experimentalToolbar', true ) ) { - return null; - } - } + const isToolbarEnabled = + blockType && + hasBlockSupport( blockType, '__experimentalToolbar', true ); + const hasAnyBlockControls = useHasAnyBlockControls(); - if ( blockClientIds.length === 0 ) { + if ( + ! isToolbarEnabled || + ( ! isDefaultEditingMode && ! hasAnyBlockControls ) + ) { return null; } - const shouldShowVisualToolbar = isValid && isVisual; const isMultiToolbar = blockClientIds.length > 1; const isSynced = isReusableBlock( blockType ) || isTemplatePart( blockType ); - const classes = classnames( 'block-editor-block-toolbar', { + // Shifts the toolbar to make room for the parent block selector. + const classes = classnames( 'block-editor-block-contextual-toolbar', { + 'has-parent': showParentSelector, + } ); + + const innerClasses = classnames( 'block-editor-block-toolbar', { 'is-synced': isSynced, } ); return ( - <div className={ classes } ref={ toolbarWrapperRef }> - { ! isMultiToolbar && - isLargeViewport && - blockEditingMode === 'default' && <BlockParentSelector /> } - { ( shouldShowVisualToolbar || isMultiToolbar ) && - blockEditingMode === 'default' && ( - <div ref={ nodeRef } { ...showHoveredOrFocusedGestures }> - <ToolbarGroup className="block-editor-block-toolbar__block-controls"> - <BlockSwitcher clientIds={ blockClientIds } /> - { ! isMultiToolbar && ( - <BlockLockToolbar - clientId={ blockClientIds[ 0 ] } - wrapperRef={ toolbarWrapperRef } + <NavigableToolbar + focusEditorOnEscape + className={ classes } + /* translators: accessibility text for the block toolbar */ + aria-label={ __( 'Block tools' ) } + // The variant is applied as "toolbar" when undefined, which is the black border style of the dropdown from the toolbar popover. + variant={ variant === 'toolbar' ? undefined : variant } + focusOnMount={ focusOnMount } + __experimentalInitialIndex={ __experimentalInitialIndex } + __experimentalOnIndexChange={ __experimentalOnIndexChange } + // Resets the index whenever the active block changes so + // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + key={ blockClientId } + > + <div ref={ toolbarWrapperRef } className={ innerClasses }> + { ! isMultiToolbar && + isLargeViewport && + isDefaultEditingMode && <BlockParentSelector /> } + { ( shouldShowVisualToolbar || isMultiToolbar ) && + isDefaultEditingMode && ( + <div + ref={ nodeRef } + { ...showHoveredOrFocusedGestures } + > + <ToolbarGroup className="block-editor-block-toolbar__block-controls"> + <BlockSwitcher clientIds={ blockClientIds } /> + { ! isMultiToolbar && ( + <BlockLockToolbar + clientId={ blockClientIds[ 0 ] } + wrapperRef={ toolbarWrapperRef } + /> + ) } + <BlockMover + clientIds={ blockClientIds } + hideDragHandle={ hideDragHandle } /> - ) } - <BlockMover - clientIds={ blockClientIds } - hideDragHandle={ hideDragHandle } - /> - </ToolbarGroup> - </div> + </ToolbarGroup> + </div> + ) } + { shouldShowVisualToolbar && isMultiToolbar && ( + <BlockGroupToolbar /> + ) } + { shouldShowVisualToolbar && ( + <> + <BlockControls.Slot + group="parent" + className="block-editor-block-toolbar__slot" + /> + <BlockControls.Slot + group="block" + className="block-editor-block-toolbar__slot" + /> + <BlockControls.Slot className="block-editor-block-toolbar__slot" /> + <BlockControls.Slot + group="inline" + className="block-editor-block-toolbar__slot" + /> + <BlockControls.Slot + group="other" + className="block-editor-block-toolbar__slot" + /> + <__unstableBlockNameContext.Provider + value={ blockType?.name } + > + <__unstableBlockToolbarLastItem.Slot /> + </__unstableBlockNameContext.Provider> + </> + ) } + <BlockEditVisuallyButton clientIds={ blockClientIds } /> + { isDefaultEditingMode && ( + <BlockSettingsMenu clientIds={ blockClientIds } /> ) } - { shouldShowVisualToolbar && isMultiToolbar && ( - <BlockGroupToolbar /> - ) } - { shouldShowVisualToolbar && ( - <> - <BlockControls.Slot - group="parent" - className="block-editor-block-toolbar__slot" - /> - <BlockControls.Slot - group="block" - className="block-editor-block-toolbar__slot" - /> - <BlockControls.Slot className="block-editor-block-toolbar__slot" /> - <BlockControls.Slot - group="inline" - className="block-editor-block-toolbar__slot" - /> - <BlockControls.Slot - group="other" - className="block-editor-block-toolbar__slot" - /> - <__unstableBlockNameContext.Provider - value={ blockType?.name } - > - <__unstableBlockToolbarLastItem.Slot /> - </__unstableBlockNameContext.Provider> - </> - ) } - <BlockEditVisuallyButton clientIds={ blockClientIds } /> - { blockEditingMode === 'default' && ( - <BlockSettingsMenu clientIds={ blockClientIds } /> - ) } - </div> + </div> + </NavigableToolbar> ); -}; +} /** + * Renders the block toolbar. + * * @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/block-toolbar/README.md + * + * @param {Object} props Components props. + * @param {boolean} props.hideDragHandle Show or hide the Drag Handle for drag and drop functionality. + * @param {string} props.variant Style variant of the toolbar, also passed to the Dropdowns rendered from Block Toolbar Buttons. */ -export default BlockToolbar; +export default function BlockToolbar( { hideDragHandle, variant } ) { + return ( + <PrivateBlockToolbar + hideDragHandle={ hideDragHandle } + variant={ variant } + focusOnMount={ undefined } + __experimentalInitialIndex={ undefined } + __experimentalOnIndexChange={ undefined } + /> + ); +} diff --git a/packages/block-editor/src/components/block-toolbar/style.scss b/packages/block-editor/src/components/block-toolbar/style.scss index 3f8a7057aef84f..85020cea2aa23f 100644 --- a/packages/block-editor/src/components/block-toolbar/style.scss +++ b/packages/block-editor/src/components/block-toolbar/style.scss @@ -56,57 +56,66 @@ } } -.block-editor-block-contextual-toolbar.is-fixed { +.block-editor-block-contextual-toolbar { position: sticky; top: 0; z-index: z-index(".block-editor-block-popover"); display: block; width: 100%; -} + // Block UI appearance. + background-color: $white; + flex-shrink: 3; + + // Raise the specificity. + &.components-accessible-toolbar { + border: none; + border-bottom: $border-width solid $gray-200; + border-radius: 0; + } -// on desktop browsers the fixed toolbar has tweaked borders -@include break-medium() { - .block-editor-block-contextual-toolbar.is-fixed { - .block-editor-block-toolbar { - .components-toolbar-group, - .components-toolbar { - border-right: none; - - &::after { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - margin-left: $grid-unit; - } - - & .components-toolbar-group.components-toolbar-group { - &::after { - display: none; - } - } - } + .block-editor-block-toolbar { + overflow: auto; + overflow-y: hidden; - > :last-child, - > :last-child .components-toolbar-group, - > :last-child .components-toolbar { - &::after { - display: none; - } + > :last-child, + > :last-child .components-toolbar-group, + > :last-child .components-toolbar { + &::after { + display: none; } } } -} -.block-editor-block-contextual-toolbar.has-parent:not(.is-fixed) { - margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); + .block-editor-block-toolbar .components-toolbar-group, + .block-editor-block-toolbar .components-toolbar { + border-right-color: $gray-200; + } - .show-icon-labels & { - margin-left: 0; + & > .block-editor-block-toolbar { + flex-grow: initial; + width: initial; + } + + .block-editor-block-parent-selector { + position: relative; + + // Parent selector dot divider + &::after { + content: "\00B7"; + position: absolute; + font-size: 16px; + right: 0; + bottom: $grid-unit-20; + } + } + + .block-editor-block-parent-selector__button { + position: relative; + top: -1px; } } + // Block controls. .block-editor-block-toolbar__block-controls { // Switcher. @@ -165,7 +174,6 @@ } // Padding overrides. - .components-accessible-toolbar .components-toolbar-group > div:first-child:last-child > .components-button.has-icon { padding-left: 6px; padding-right: 6px; @@ -181,10 +189,12 @@ } // Parent selector overrides - - .block-editor-block-parent-selector__button { + .block-editor-block-parent-selector .block-editor-block-parent-selector__button { border-top-right-radius: 0; border-bottom-right-radius: 0; + padding-left: $grid-unit-15; + padding-right: $grid-unit-15; + text-wrap: nowrap; .block-editor-block-icon { width: 0; @@ -210,25 +220,17 @@ // Mover overrides. .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left: 1px solid $gray-900; + border-left: 1px solid $gray-300; margin-left: 6px; margin-right: -6px; white-space: nowrap; } - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left-color: $gray-200; - } - .block-editor-block-mover .block-editor-block-mover__drag-handle.has-icon { padding-left: $grid-unit-15; padding-right: $grid-unit-15; } - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-mover__move-button-container { - border-width: 0; - } - @include break-small() { // Specificity override for https://github.com/WordPress/gutenberg/blob/try/block-toolbar-labels/packages/block-editor/src/components/block-mover/style.scss#L69 .is-up-button.is-up-button.is-up-button { @@ -237,17 +239,9 @@ order: 1; } - .block-editor-block-mover__move-button-container { - border-left: 1px solid $gray-900; - } - .is-down-button.is-down-button.is-down-button { order: 2; } - - .block-editor-block-contextual-toolbar.is-fixed .block-editor-block-mover__move-button-container::before { - background: $gray-300; - } } .block-editor-block-contextual-toolbar .block-editor-block-mover.is-horizontal .block-editor-block-mover-button.block-editor-block-mover-button { @@ -260,16 +254,6 @@ flex-shrink: 1; } - @include break-medium() { - .block-editor-block-contextual-toolbar.is-fixed { - .components-toolbar, - .components-toolbar-group { - flex-shrink: 0; - } - } - } - - .block-editor-rich-text__inline-format-toolbar-group { .components-button + .components-button { margin-left: 6px; diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js index 029419926e9ed5..14027760be1e6a 100644 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ b/packages/block-editor/src/components/block-tools/back-compat.js @@ -9,7 +9,7 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockPopover from './selected-block-tools'; +import BlockToolbarPopover from './block-toolbar-popover'; export default function BlockToolsBackCompat( { children } ) { const openRef = useContext( InsertionPointOpenRef ); @@ -28,7 +28,7 @@ export default function BlockToolsBackCompat( { children } ) { return ( <InsertionPoint __unstablePopoverSlot="block-toolbar"> - <BlockPopover __unstablePopoverSlot="block-toolbar" /> + <BlockToolbarPopover __unstablePopoverSlot="block-toolbar" /> { children } </InsertionPoint> ); diff --git a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js b/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js deleted file mode 100644 index b24a25ee60ed42..00000000000000 --- a/packages/block-editor/src/components/block-tools/block-contextual-toolbar.js +++ /dev/null @@ -1,100 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { hasBlockSupport, store as blocksStore } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import NavigableToolbar from '../navigable-toolbar'; -import BlockToolbar from '../block-toolbar'; -import { store as blockEditorStore } from '../../store'; -import { useHasAnyBlockControls } from '../block-controls/use-has-block-controls'; - -export default function BlockContextualToolbar( { - focusOnMount, - isFixed, - ...props -} ) { - const { - blockType, - blockEditingMode, - hasParents, - showParentSelector, - selectedBlockClientId, - } = useSelect( ( select ) => { - const { - getBlockName, - getBlockParents, - getSelectedBlockClientIds, - getBlockEditingMode, - } = select( blockEditorStore ); - const { getBlockType } = select( blocksStore ); - const selectedBlockClientIds = getSelectedBlockClientIds(); - const _selectedBlockClientId = selectedBlockClientIds[ 0 ]; - const parents = getBlockParents( _selectedBlockClientId ); - const firstParentClientId = parents[ parents.length - 1 ]; - const parentBlockName = getBlockName( firstParentClientId ); - const parentBlockType = getBlockType( parentBlockName ); - - return { - selectedBlockClientId: _selectedBlockClientId, - blockType: - _selectedBlockClientId && - getBlockType( getBlockName( _selectedBlockClientId ) ), - blockEditingMode: getBlockEditingMode( _selectedBlockClientId ), - hasParents: parents.length, - showParentSelector: - parentBlockType && - getBlockEditingMode( firstParentClientId ) === 'default' && - hasBlockSupport( - parentBlockType, - '__experimentalParentSelector', - true - ) && - selectedBlockClientIds.length <= 1 && - getBlockEditingMode( _selectedBlockClientId ) === 'default', - }; - }, [] ); - - const isToolbarEnabled = - blockType && - hasBlockSupport( blockType, '__experimentalToolbar', true ); - const hasAnyBlockControls = useHasAnyBlockControls(); - if ( - ! isToolbarEnabled || - ( blockEditingMode !== 'default' && ! hasAnyBlockControls ) - ) { - return null; - } - - // Shifts the toolbar to make room for the parent block selector. - const classes = classnames( 'block-editor-block-contextual-toolbar', { - 'has-parent': hasParents && showParentSelector, - 'is-fixed': isFixed, - } ); - - return ( - <NavigableToolbar - focusOnMount={ focusOnMount } - focusEditorOnEscape - className={ classes } - /* translators: accessibility text for the block toolbar */ - aria-label={ __( 'Block tools' ) } - variant={ isFixed ? 'unstyled' : undefined } - // Resets the index whenever the active block changes so - // this is not persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - key={ selectedBlockClientId } - { ...props } - > - <BlockToolbar hideDragHandle={ isFixed } /> - </NavigableToolbar> - ); -} diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js new file mode 100644 index 00000000000000..77afb824101d41 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-toolbar-breadcrumb.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * Internal dependencies + */ +import BlockSelectionButton from './block-selection-button'; +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; + +export default function BlockToolbarBreadcrumb( { + clientId, + __unstableContentRef, +} ) { + const { + capturingClientId, + isInsertionPointVisible, + lastClientId, + rootClientId, + } = useSelectedBlockToolProps( clientId ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + <BlockPopover + clientId={ capturingClientId || clientId } + bottomClientId={ lastClientId } + className={ classnames( 'block-editor-block-list__block-popover', { + 'is-insertion-point-visible': isInsertionPointVisible, + } ) } + resize={ false } + { ...popoverProps } + > + <BlockSelectionButton + clientId={ clientId } + rootClientId={ rootClientId } + /> + </BlockPopover> + ); +} diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js new file mode 100644 index 00000000000000..a50e5dc42b3712 --- /dev/null +++ b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js @@ -0,0 +1,90 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; +/** + * WordPress dependencies + */ +import { useDispatch } from '@wordpress/data'; +import { useEffect, useRef } from '@wordpress/element'; +import { useShortcut } from '@wordpress/keyboard-shortcuts'; +/** + * Internal dependencies + */ +import BlockPopover from '../block-popover'; +import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; +import useSelectedBlockToolProps from './use-selected-block-tool-props'; +import { store as blockEditorStore } from '../../store'; +import { PrivateBlockToolbar } from '../block-toolbar'; + +export default function BlockToolbarPopover( { + clientId, + isTyping, + __unstableContentRef, +} ) { + const { capturingClientId, isInsertionPointVisible, lastClientId } = + useSelectedBlockToolProps( clientId ); + + // Stores the active toolbar item index so the block toolbar can return focus + // to it when re-mounting. + const initialToolbarItemIndexRef = useRef(); + + useEffect( () => { + // Resets the index whenever the active block changes so this is not + // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 + initialToolbarItemIndexRef.current = undefined; + }, [ clientId ] ); + + const { stopTyping } = useDispatch( blockEditorStore ); + const isToolbarForced = useRef( false ); + + useShortcut( + 'core/block-editor/focus-toolbar', + () => { + isToolbarForced.current = true; + stopTyping( true ); + }, + { + isDisabled: false, + } + ); + + useEffect( () => { + isToolbarForced.current = false; + } ); + + const popoverProps = useBlockToolbarPopoverProps( { + contentElement: __unstableContentRef?.current, + clientId, + } ); + + return ( + ! isTyping && ( + <BlockPopover + clientId={ capturingClientId || clientId } + bottomClientId={ lastClientId } + className={ classnames( + 'block-editor-block-list__block-popover', + { + 'is-insertion-point-visible': isInsertionPointVisible, + } + ) } + resize={ false } + { ...popoverProps } + > + <PrivateBlockToolbar + // If the toolbar is being shown because of being forced + // it should focus the toolbar right after the mount. + focusOnMount={ isToolbarForced.current } + __experimentalInitialIndex={ + initialToolbarItemIndexRef.current + } + __experimentalOnIndexChange={ ( index ) => { + initialToolbarItemIndexRef.current = index; + } } + variant="toolbar" + /> + </BlockPopover> + ) + ); +} diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index bc2729fbb15990..969f36d878b875 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -2,7 +2,6 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useViewportMatch } from '@wordpress/compose'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; @@ -16,9 +15,9 @@ import { InsertionPointOpenRef, default as InsertionPoint, } from './insertion-point'; -import SelectedBlockTools from './selected-block-tools'; +import BlockToolbarPopover from './block-toolbar-popover'; +import BlockToolbarBreadcrumb from './block-toolbar-breadcrumb'; import { store as blockEditorStore } from '../../store'; -import BlockContextualToolbar from './block-contextual-toolbar'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; @@ -28,6 +27,7 @@ function selector( select ) { getFirstMultiSelectedBlockClientId, getBlock, getSettings, + hasMultiSelection, __unstableGetEditorMode, isTyping, } = select( blockEditorStore ); @@ -36,18 +36,35 @@ function selector( select ) { getSelectedBlockClientId() || getFirstMultiSelectedBlockClientId(); const { name = '', attributes = {} } = getBlock( clientId ) || {}; + const editorMode = __unstableGetEditorMode(); + const hasSelectedBlock = clientId && name; + const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( { + name, + attributes, + } ); + const _showEmptyBlockSideInserter = + clientId && + ! isTyping() && + editorMode === 'edit' && + isUnmodifiedDefaultBlock( { name, attributes } ); + const maybeShowBreadcrumb = + hasSelectedBlock && + ! hasMultiSelection() && + ( editorMode === 'navigation' || editorMode === 'zoom-out' ); return { clientId, hasFixedToolbar: getSettings().hasFixedToolbar, - hasSelectedBlock: clientId && name, isTyping: isTyping(), - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', - showEmptyBlockSideInserter: - clientId && - ! isTyping() && - __unstableGetEditorMode() === 'edit' && - isUnmodifiedDefaultBlock( { name, attributes } ), + isZoomOutMode: editorMode === 'zoom-out', + showEmptyBlockSideInserter: _showEmptyBlockSideInserter, + showBreadcrumb: ! _showEmptyBlockSideInserter && maybeShowBreadcrumb, + showBlockToolbar: + ! getSettings().hasFixedToolbar && + ! _showEmptyBlockSideInserter && + hasSelectedBlock && + ! isEmptyDefaultBlock && + ! maybeShowBreadcrumb, }; } @@ -65,14 +82,14 @@ export default function BlockTools( { __unstableContentRef, ...props } ) { - const isLargeViewport = useViewportMatch( 'medium' ); const { clientId, hasFixedToolbar, - hasSelectedBlock, isTyping, isZoomOutMode, showEmptyBlockSideInserter, + showBreadcrumb, + showBlockToolbar, } = useSelect( selector, [] ); const isMatch = useShortcutEventMatch(); const { getSelectedBlockClientIds, getBlockRootClientId } = @@ -162,12 +179,6 @@ export default function BlockTools( { const blockToolbarRef = usePopoverScroll( __unstableContentRef ); const blockToolbarAfterRef = usePopoverScroll( __unstableContentRef ); - // Conditions for fixed toolbar - // 1. Not zoom out mode - // 2. It's a large viewport. If it's a smaller viewport, let the floating toolbar handle it as it already has styles attached to make it render that way. - // 3. Fixed toolbar is enabled - const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; - return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions <div { ...props } onKeyDown={ onKeyDown }> @@ -177,11 +188,6 @@ export default function BlockTools( { __unstableContentRef={ __unstableContentRef } /> ) } - { /* If there is no slot available, such as in the standalone block editor, render within the editor */ } - - { ! isLargeViewport && ( // Small viewports always get a fixed toolbar - <BlockContextualToolbar isFixed /> - ) } { showEmptyBlockSideInserter && ( <EmptyBlockInserter @@ -189,17 +195,24 @@ export default function BlockTools( { clientId={ clientId } /> ) } - { /* Even if the toolbar is fixed, the block popover is still - needed for navigation and zoom-out mode. */ } - { ! showEmptyBlockSideInserter && hasSelectedBlock && ( - <SelectedBlockTools + + { showBlockToolbar && ( + <BlockToolbarPopover + __unstableContentRef={ __unstableContentRef } + clientId={ clientId } + isTyping={ isTyping } + /> + ) } + + { showBreadcrumb && ( + <BlockToolbarBreadcrumb __unstableContentRef={ __unstableContentRef } clientId={ clientId } /> ) } - { /* Used for the inline rich text toolbar. */ } - { ! isTopToolbar && ( + { /* Used for the inline rich text toolbar. Until this toolbar is combined into BlockToolbar, someone implementing their own BlockToolbar will also need to use this to see the image caption toolbar. */ } + { ! isZoomOutMode && ! hasFixedToolbar && ( <Popover.Slot name="block-toolbar" ref={ blockToolbarRef } diff --git a/packages/block-editor/src/components/block-tools/selected-block-tools.js b/packages/block-editor/src/components/block-tools/selected-block-tools.js deleted file mode 100644 index dfbd3e9ba3ca3c..00000000000000 --- a/packages/block-editor/src/components/block-tools/selected-block-tools.js +++ /dev/null @@ -1,127 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { useRef, useEffect } from '@wordpress/element'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { useShortcut } from '@wordpress/keyboard-shortcuts'; - -/** - * Internal dependencies - */ -import BlockSelectionButton from './block-selection-button'; -import BlockContextualToolbar from './block-contextual-toolbar'; -import { store as blockEditorStore } from '../../store'; -import BlockPopover from '../block-popover'; -import useBlockToolbarPopoverProps from './use-block-toolbar-popover-props'; -import useSelectedBlockToolProps from './use-selected-block-tool-props'; -import { useShouldContextualToolbarShow } from '../../utils/use-should-contextual-toolbar-show'; - -export default function SelectedBlockTools( { - clientId, - showEmptyBlockSideInserter, - __unstableContentRef, -} ) { - const { - capturingClientId, - isInsertionPointVisible, - lastClientId, - rootClientId, - } = useSelectedBlockToolProps( clientId ); - - const { shouldShowBreadcrumb } = useSelect( ( select ) => { - const { hasMultiSelection, __unstableGetEditorMode } = - select( blockEditorStore ); - - const editorMode = __unstableGetEditorMode(); - - return { - shouldShowBreadcrumb: - ! hasMultiSelection() && - ( editorMode === 'navigation' || editorMode === 'zoom-out' ), - }; - }, [] ); - - const isToolbarForced = useRef( false ); - const { shouldShowContextualToolbar, canFocusHiddenToolbar } = - useShouldContextualToolbarShow(); - - const { stopTyping } = useDispatch( blockEditorStore ); - - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: ! canFocusHiddenToolbar, - } - ); - - useEffect( () => { - isToolbarForced.current = false; - } ); - - // Stores the active toolbar item index so the block toolbar can return focus - // to it when re-mounting. - const initialToolbarItemIndexRef = useRef(); - - useEffect( () => { - // Resets the index whenever the active block changes so this is not - // persisted. See https://github.com/WordPress/gutenberg/pull/25760#issuecomment-717906169 - initialToolbarItemIndexRef.current = undefined; - }, [ clientId ] ); - - const popoverProps = useBlockToolbarPopoverProps( { - contentElement: __unstableContentRef?.current, - clientId, - } ); - - if ( showEmptyBlockSideInserter ) { - return null; - } - - if ( shouldShowBreadcrumb || shouldShowContextualToolbar ) { - return ( - <BlockPopover - clientId={ capturingClientId || clientId } - bottomClientId={ lastClientId } - className={ classnames( - 'block-editor-block-list__block-popover', - { - 'is-insertion-point-visible': isInsertionPointVisible, - } - ) } - resize={ false } - { ...popoverProps } - > - { shouldShowContextualToolbar && ( - <BlockContextualToolbar - // If the toolbar is being shown because of being forced - // it should focus the toolbar right after the mount. - focusOnMount={ isToolbarForced.current } - __experimentalInitialIndex={ - initialToolbarItemIndexRef.current - } - __experimentalOnIndexChange={ ( index ) => { - initialToolbarItemIndexRef.current = index; - } } - /> - ) } - { shouldShowBreadcrumb && ( - <BlockSelectionButton - clientId={ clientId } - rootClientId={ rootClientId } - /> - ) } - </BlockPopover> - ); - } - - return null; -} diff --git a/packages/block-editor/src/components/block-tools/style.scss b/packages/block-editor/src/components/block-tools/style.scss index 07f22bb4946ea2..3371d795e6c033 100644 --- a/packages/block-editor/src/components/block-tools/style.scss +++ b/packages/block-editor/src/components/block-tools/style.scss @@ -85,178 +85,6 @@ } } -/** - * Block Toolbar when contextual. - */ - -.block-editor-block-contextual-toolbar { - // Block UI appearance. - display: inline-flex; - border: $border-width solid $gray-900; - border-radius: $radius-block-ui; - background-color: $white; - - .block-editor-block-toolbar .components-toolbar-group, - .block-editor-block-toolbar .components-toolbar { - border-right-color: $gray-900; - } - - &.is-fixed { - overflow: hidden; - - .block-editor-block-toolbar { - overflow: auto; - overflow-y: hidden; - } - - border-bottom: $border-width solid $gray-200; - border-radius: 0; - - .block-editor-block-toolbar .components-toolbar-group, - .block-editor-block-toolbar .components-toolbar { - border-right-color: $gray-200; - } - } - - @include break-medium() { - &.is-fixed { - & > .block-editor-block-toolbar { - flex-grow: initial; - width: initial; - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - height: 3 * $grid-unit; - margin-top: $grid-unit + $grid-unit-05; - margin-right: 0; - background-color: $gray-300; - position: relative; - left: math.div(-$grid-unit-05, 2); - top: -1px; - } - } - - & > .block-editor-block-toolbar__group-collapse-fixed-toolbar { - border: none; - - .show-icon-labels & { - .components-button.has-icon { - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - font-size: $helptext-font-size; - } - } - } - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - height: 3 * $grid-unit; - margin-top: $grid-unit + $grid-unit-05; - margin-right: $grid-unit-10; - background-color: $gray-300; - position: relative; - left: 0; - top: -1px; - } - } - - & > .block-editor-block-toolbar__group-expand-fixed-toolbar { - border: none; - - .show-icon-labels & { - width: $grid-unit-80 * 4; - .components-button.has-icon { - // Hide the button icons when labels are set to display... - svg { - display: none; - } - // ... and display labels. - &::after { - content: attr(aria-label); - font-size: $helptext-font-size; - } - } - } - - // Add a border as separator in the block toolbar. - &::before { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - position: relative; - left: -8px; - height: 3 * $grid-unit; - top: -1px; - } - } - - .show-icon-labels & { - .block-editor-block-parent-selector .block-editor-block-parent-selector__button::after { - left: 0; - } - - .block-editor-block-toolbar__block-controls .block-editor-block-mover { - border-left: none; - &::before { - content: ""; - width: $border-width; - margin-top: $grid-unit + $grid-unit-05; - margin-bottom: $grid-unit + $grid-unit-05; - background-color: $gray-300; - position: relative; - } - } - } - } - - &.is-fixed .block-editor-block-parent-selector { - - .block-editor-block-parent-selector__button { - position: relative; - top: -1px; - border: 0; - padding-right: 6px; - padding-left: 6px; - - &::after { - content: "\00B7"; - font-size: 16px; - line-height: $grid-unit-40 + $grid-unit-10; - position: absolute; - left: $grid-unit-40 + $grid-unit-15 + 2px; - bottom: $grid-unit-05; - } - } - } - - &:not(.is-fixed) .block-editor-block-parent-selector { - position: absolute; - top: -$border-width; - left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); - - .show-icon-labels & { - position: relative; - left: auto; - top: auto; - margin-top: -$border-width; - margin-left: -$border-width; - margin-bottom: -$border-width; - } - } - } -} - /** * Block Label for Navigation/Selection Mode */ @@ -349,6 +177,7 @@ } .components-popover.block-editor-block-list__block-popover { + // Position the block toolbar. .block-editor-block-list__block-selection-button, .block-editor-block-contextual-toolbar { @@ -357,6 +186,30 @@ margin-bottom: $grid-unit-15; } + .block-editor-block-contextual-toolbar { + border: $border-width solid $gray-900; + border-radius: $radius-block-ui; + overflow: visible; // allow the parent selector to be visible + position: static; + width: auto; + + &.has-parent { + margin-left: calc(#{$grid-unit-60} + #{$grid-unit-10}); + .show-icon-labels & { + margin-left: 0; + } + } + } + + .block-editor-block-toolbar { + overflow: visible; + } + + .block-editor-block-toolbar .components-toolbar-group, + .block-editor-block-toolbar .components-toolbar { + border-right-color: $gray-900; + } + // Hide the block toolbar if the insertion point is shown. &.is-insertion-point-visible { visibility: hidden; @@ -368,6 +221,41 @@ // It's essential to hide the toolbar/popover so that `dragEnter` events can pass through them to the underlying elements. animation: hide-during-dragging 1ms linear forwards; } + + .block-editor-block-parent-selector { + position: absolute; + left: calc(-#{$grid-unit-60} - #{$grid-unit-10} - #{$border-width}); + + &::before { + content: ""; + } + + .block-editor-block-parent-selector__button { + border: 1px solid $gray-900; + padding-right: 6px; + padding-left: 6px; + background-color: $white; + } + } + + // Show Icon Label Styles + .show-icon-labels & { + + .block-editor-block-parent-selector { + position: static; + margin-top: -$border-width; + margin-left: -$border-width; + margin-bottom: -$border-width; + + .block-editor-block-parent-selector__button { + position: static; + } + } + .block-editor-block-mover__move-button-container, + .block-editor-block-toolbar__block-controls .block-editor-block-mover { + border-left: 1px solid $gray-900; + } + } } .is-dragging-components-draggable .components-tooltip { diff --git a/packages/block-editor/src/components/navigable-toolbar/README.md b/packages/block-editor/src/components/navigable-toolbar/README.md index 30a4d100195f85..317be48f38faa2 100644 --- a/packages/block-editor/src/components/navigable-toolbar/README.md +++ b/packages/block-editor/src/components/navigable-toolbar/README.md @@ -8,6 +8,8 @@ The component accepts the following props. Props not included in this set will b ## `focusOnMount` +_Note: this prop is deprecated._ + Whether to immediately focus when the component mounts. - Type: `Boolean` diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index fe216e1058f6f0..8954f7e17f132e 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -162,7 +162,7 @@ function useToolbarFocus( { const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; - }, [ initialIndex, initialFocusOnMount, toolbarRef ] ); + }, [ initialIndex, initialFocusOnMount, onIndexChange, toolbarRef ] ); const { lastFocus } = useSelect( ( select ) => { const { getLastFocus } = select( blockEditorStore ); @@ -210,9 +210,9 @@ export default function NavigableToolbar( { useToolbarFocus( { toolbarRef, focusOnMount, - isAccessibleToolbar, defaultIndex: initialIndex, onIndexChange, + isAccessibleToolbar, shouldUseKeyboardFocusShortcut, focusEditorOnEscape, } ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index b1d499ae099469..9837c206487bea 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -10,7 +10,6 @@ import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; -import BlockContextualToolbar from './components/block-tools/block-contextual-toolbar'; import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show'; import { cleanEmptyObject, useStyleOverride } from './hooks/utils'; import BlockQuickNavigation from './components/block-quick-navigation'; @@ -42,7 +41,6 @@ lock( privateApis, { PrivateListView, ResizableBoxPopover, BlockInfo, - BlockContextualToolbar, useShouldContextualToolbarShow, cleanEmptyObject, useStyleOverride, diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index a55756ae6f53d7..16de2dfdb71142 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -10,7 +10,6 @@ @import "./components/block-draggable/style.scss"; @import "./components/block-mover/style.scss"; @import "./components/block-navigation/style.scss"; -@import "./components/block-parent-selector/style.scss"; @import "./components/block-patterns-list/style.scss"; @import "./components/block-patterns-paging/style.scss"; @import "./components/block-popover/style.scss"; diff --git a/packages/customize-widgets/src/components/header/index.js b/packages/customize-widgets/src/components/header/index.js index 34e4573c719dd5..5bd0b2c2f4d471 100644 --- a/packages/customize-widgets/src/components/header/index.js +++ b/packages/customize-widgets/src/components/header/index.js @@ -6,13 +6,9 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { Popover, ToolbarButton } from '@wordpress/components'; -import { useViewportMatch } from '@wordpress/compose'; -import { - NavigableToolbar, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { createPortal, useEffect, useRef, useState } from '@wordpress/element'; +import { ToolbarButton } from '@wordpress/components'; +import { NavigableToolbar } from '@wordpress/block-editor'; +import { createPortal, useEffect, useState } from '@wordpress/element'; import { displayShortcut, isAppleOS } from '@wordpress/keycodes'; import { __, _x, isRTL } from '@wordpress/i18n'; import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; @@ -22,9 +18,6 @@ import { plus, undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; */ import Inserter from '../inserter'; import MoreMenu from '../more-menu'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { sidebar, @@ -33,8 +26,6 @@ function Header( { setIsInserterOpened, isFixedToolbarActive, } ) { - const isLargeViewport = useViewportMatch( 'medium' ); - const blockToolbarRef = useRef(); const [ [ hasUndo, hasRedo ], setUndoRedo ] = useState( [ sidebar.hasUndo(), sidebar.hasRedo(), @@ -107,18 +98,6 @@ function Header( { <Inserter setIsOpened={ setIsInserterOpened } />, inserter.contentContainer[ 0 ] ) } - - { isFixedToolbarActive && isLargeViewport && ( - <> - <div className="selected-block-tools-wrapper"> - <BlockContextualToolbar isFixed /> - </div> - <Popover.Slot - ref={ blockToolbarRef } - name="block-toolbar" - /> - </> - ) } </> ); } diff --git a/packages/customize-widgets/src/components/header/style.scss b/packages/customize-widgets/src/components/header/style.scss index d9d4a487e647c1..27460a82e0ad10 100644 --- a/packages/customize-widgets/src/components/header/style.scss +++ b/packages/customize-widgets/src/components/header/style.scss @@ -1,5 +1,5 @@ .customize-widgets-header { - @include break-medium() { + @include break-small() { // Make space for the floating toolbar. margin-bottom: $grid-unit-20 + $default-block-margin; } diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index ccb6fca871429e..c2e10bca16ec0b 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -1,11 +1,13 @@ /** * WordPress dependencies */ +import { useViewportMatch } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; import { useMemo, createPortal } from '@wordpress/element'; import { BlockList, + BlockToolbar, BlockTools, BlockInspector, privateApis as blockEditorPrivateApis, @@ -37,6 +39,7 @@ export default function SidebarBlockEditor( { inspector, } ) { const [ isInserterOpened, setIsInserterOpened ] = useInserter( inserter ); + const isMediumViewport = useViewportMatch( 'small' ); const { hasUploadPermissions, isFixedToolbarActive, @@ -77,7 +80,7 @@ export default function SidebarBlockEditor( { ...blockEditorSettings, __experimentalSetIsInserterOpened: setIsInserterOpened, mediaUpload: mediaUploadBlockEditor, - hasFixedToolbar: isFixedToolbarActive, + hasFixedToolbar: isFixedToolbarActive || ! isMediumViewport, keepCaretInsideBlock, __unstableHasCustomAppender: true, }; @@ -85,6 +88,7 @@ export default function SidebarBlockEditor( { hasUploadPermissions, blockEditorSettings, isFixedToolbarActive, + isMediumViewport, keepCaretInsideBlock, setIsInserterOpened, ] ); @@ -109,9 +113,13 @@ export default function SidebarBlockEditor( { inserter={ inserter } isInserterOpened={ isInserterOpened } setIsInserterOpened={ setIsInserterOpened } - isFixedToolbarActive={ isFixedToolbarActive } + isFixedToolbarActive={ + isFixedToolbarActive || ! isMediumViewport + } /> - + { ( isFixedToolbarActive || ! isMediumViewport ) && ( + <BlockToolbar hideDragHandle /> + ) } <BlockTools> <BlockCanvas shouldIframe={ false } diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/style.scss b/packages/customize-widgets/src/components/sidebar-block-editor/style.scss index a1b99447155eb2..1aa62ed32e847c 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/style.scss +++ b/packages/customize-widgets/src/components/sidebar-block-editor/style.scss @@ -1,23 +1,3 @@ -.block-editor-block-contextual-toolbar.is-fixed { - // The top position used for the 'sticky' positioning. - top: 0; - - // Offset the customizer's sidebar padding. - margin-left: -$grid-unit-15; - margin-right: -$grid-unit-15; - // added important to override the inline style coming from - // the block-editor/block-contextual-toolbar component. - width: calc(100% + #{ $grid-unit-30 }) !important; - - & > .block-editor-block-toolbar__group-collapse-fixed-toolbar { - display: none; - } - - // Scroll sideways. - overflow-y: auto; - z-index: z-index(".customize-widgets__block-toolbar"); -} - .customize-control-sidebar_block_editor .block-editor-block-list__block-popover { // FloatingUI library used in Popover component forces us to have an "absolute" inline style. // We need to override this in the customizer. diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 8bfeb2d253ce11..b92c6c44fe49f3 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -7,7 +7,7 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - privateApis as blockEditorPrivateApis, + BlockToolbar, store as blockEditorStore, } from '@wordpress/block-editor'; import { @@ -40,9 +40,6 @@ import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); const slideY = { hidden: { y: '-50px' }, @@ -130,7 +127,7 @@ function Header( { } ) } > - <BlockContextualToolbar isFixed /> + <BlockToolbar hideDragHandle /> </div> <Popover.Slot ref={ blockToolbarRef } diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index 6e634427e21986..55450ba8f1de28 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -52,12 +52,43 @@ } } - .block-editor-block-contextual-toolbar.is-fixed { - border: none; - } - .selected-block-tools-wrapper { overflow-x: hidden; + display: flex; + + .block-editor-block-contextual-toolbar { + border-bottom: 0; + } + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + // Modified group borders + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } &.is-collapsed { display: none; @@ -185,6 +216,22 @@ } } +.show-icon-labels { + + .edit-post-header__toolbar .block-editor-block-mover { + border-left: none; + + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + } +} + .edit-post-header__dropdown { .components-menu-item__button.components-menu-item__button, .components-button.editor-history__undo, diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 6aa4e3dd960070..eb67cc82783e4a 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -21,6 +21,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useBlockCommands, BlockBreadcrumb, + BlockToolbar, privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; @@ -138,7 +139,9 @@ function Layout() { const isMobileViewport = useViewportMatch( 'medium', '<' ); const isHugeViewport = useViewportMatch( 'huge', '>=' ); - const isLargeViewport = useViewportMatch( 'large' ); + const isWideViewport = useViewportMatch( 'large' ); + const isLargeViewport = useViewportMatch( 'medium' ); + const { openGeneralSidebar, closeGeneralSidebar, setIsInserterOpened } = useDispatch( editPostStore ); const { createErrorNotice } = useDispatch( noticesStore ); @@ -148,7 +151,6 @@ function Layout() { isRichEditingEnabled, sidebarIsOpened, hasActiveMetaboxes, - hasFixedToolbar, previousShortcut, nextShortcut, hasBlockSelected, @@ -167,8 +169,6 @@ function Layout() { return { showMetaBoxes: select( editorStore ).getRenderingMode() === 'post-only', - hasFixedToolbar: - select( editPostStore ).isFeatureActive( 'fixedToolbar' ), sidebarIsOpened: !! ( select( interfaceStore ).getActiveComplementaryArea( editPostStore.name @@ -219,12 +219,12 @@ function Layout() { if ( sidebarIsOpened && ! isHugeViewport ) { setIsInserterOpened( false ); } - }, [ sidebarIsOpened, isHugeViewport ] ); + }, [ isHugeViewport, setIsInserterOpened, sidebarIsOpened ] ); useEffect( () => { if ( isInserterOpened && ! isHugeViewport ) { closeGeneralSidebar(); } - }, [ isInserterOpened, isHugeViewport ] ); + }, [ closeGeneralSidebar, isInserterOpened, isHugeViewport ] ); // Local state for save panel. // Note 'truthy' callback implies an open panel. @@ -253,9 +253,8 @@ function Layout() { const className = classnames( 'edit-post-layout', 'is-mode-' + mode, { 'is-sidebar-opened': sidebarIsOpened, - 'has-fixed-toolbar': hasFixedToolbar, 'has-metaboxes': hasActiveMetaboxes, - 'is-distraction-free': isDistractionFree && isLargeViewport, + 'is-distraction-free': isDistractionFree && isWideViewport, 'is-entity-save-view-open': !! entitiesSavedStatesCallback, } ); @@ -302,7 +301,7 @@ function Layout() { <EditorKeyboardShortcuts /> <InterfaceSkeleton - isDistractionFree={ isDistractionFree && isLargeViewport } + isDistractionFree={ isDistractionFree && isWideViewport } className={ className } labels={ { ...interfaceLabels, @@ -346,6 +345,7 @@ function Layout() { { ( mode === 'text' || ! isRichEditingEnabled ) && ( <TextEditor /> ) } + { ! isLargeViewport && <BlockToolbar hideDragHandle /> } { isRichEditingEnabled && mode === 'visual' && ( <VisualEditor styles={ styles } /> ) } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index cff867c3f7a2cb..0abf3328635a86 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -14,6 +14,7 @@ import { SlotFillProvider } from '@wordpress/components'; import { store as coreStore } from '@wordpress/core-data'; import { store as preferencesStore } from '@wordpress/preferences'; import { CommandMenu } from '@wordpress/commands'; +import { useViewportMatch } from '@wordpress/compose'; /** * Internal dependencies @@ -26,6 +27,8 @@ import { unlock } from './lock-unlock'; const { ExperimentalEditorProvider } = unlock( editorPrivateApis ); function Editor( { postId, postType, settings, initialEdits, ...props } ) { + const isLargeViewport = useViewportMatch( 'medium' ); + const { hasFixedToolbar, focusMode, @@ -66,9 +69,9 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { getEditorSettings().supportsTemplateMode; const isViewable = getPostType( postType )?.viewable ?? false; const canEditTemplate = canUser( 'create', 'templates' ); - return { - hasFixedToolbar: isFeatureActive( 'fixedToolbar' ), + hasFixedToolbar: + isFeatureActive( 'fixedToolbar' ) || ! isLargeViewport, focusMode: isFeatureActive( 'focusMode' ), isDistractionFree: isFeatureActive( 'distractionFree' ), hasInlineToolbar: isFeatureActive( 'inlineToolbar' ), @@ -86,7 +89,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { post: postObject, }; }, - [ postType, postId ] + [ postType, postId, isLargeViewport ] ); const { updatePreferredStyleVariations, setIsInserterOpened } = diff --git a/packages/edit-site/src/components/block-editor/style.scss b/packages/edit-site/src/components/block-editor/style.scss index e02240eb880992..1da43730d95753 100644 --- a/packages/edit-site/src/components/block-editor/style.scss +++ b/packages/edit-site/src/components/block-editor/style.scss @@ -69,17 +69,6 @@ &.is-view-mode { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.8), 0 8px 10px -6px rgba(0, 0, 0, 0.8); - - /* - Temporary to hide the contextual toolbar in view mode. - See: https://github.com/WordPress/gutenberg/pull/46298 - This rule can possibly be removed once the - contextual toolbar has been redesigned. - See: https://github.com/WordPress/gutenberg/issues/40450 - */ - .block-editor-block-contextual-toolbar.is-fixed { - display: none; - } } } diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 962cfe09afb720..2deb2d4cb5fa6e 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -1,6 +1,7 @@ /** * WordPress dependencies */ +import { useViewportMatch } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; @@ -89,6 +90,7 @@ function useArchiveLabel( templateSlug ) { export function useSpecificEditorSettings() { const { setIsInserterOpened } = useDispatch( editSiteStore ); + const isLargeViewport = useViewportMatch( 'medium' ); const { templateSlug, focusMode, @@ -98,44 +100,46 @@ export function useSpecificEditorSettings() { canvasMode, settings, postWithTemplate, - } = useSelect( ( select ) => { - const { - getEditedPostType, - getEditedPostId, - getEditedPostContext, - getCanvasMode, - getSettings, - } = unlock( select( editSiteStore ) ); - const { get: getPreference } = select( preferencesStore ); - const { getEditedEntityRecord } = select( coreStore ); - const usedPostType = getEditedPostType(); - const usedPostId = getEditedPostId(); - const _record = getEditedEntityRecord( - 'postType', - usedPostType, - usedPostId - ); - const _context = getEditedPostContext(); - return { - templateSlug: _record.slug, - focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), - isDistractionFree: !! getPreference( - 'core/edit-site', - 'distractionFree' - ), - hasFixedToolbar: !! getPreference( - 'core/edit-site', - 'fixedToolbar' - ), - keepCaretInsideBlock: !! getPreference( - 'core/edit-site', - 'keepCaretInsideBlock' - ), - canvasMode: getCanvasMode(), - settings: getSettings(), - postWithTemplate: _context?.postId, - }; - }, [] ); + } = useSelect( + ( select ) => { + const { + getEditedPostType, + getEditedPostId, + getEditedPostContext, + getCanvasMode, + getSettings, + } = unlock( select( editSiteStore ) ); + const { get: getPreference } = select( preferencesStore ); + const { getEditedEntityRecord } = select( coreStore ); + const usedPostType = getEditedPostType(); + const usedPostId = getEditedPostId(); + const _record = getEditedEntityRecord( + 'postType', + usedPostType, + usedPostId + ); + const _context = getEditedPostContext(); + return { + templateSlug: _record.slug, + focusMode: !! getPreference( 'core/edit-site', 'focusMode' ), + isDistractionFree: !! getPreference( + 'core/edit-site', + 'distractionFree' + ), + hasFixedToolbar: + !! getPreference( 'core/edit-site', 'fixedToolbar' ) || + ! isLargeViewport, + keepCaretInsideBlock: !! getPreference( + 'core/edit-site', + 'keepCaretInsideBlock' + ), + canvasMode: getCanvasMode(), + settings: getSettings(), + postWithTemplate: _context?.postId, + }; + }, + [ isLargeViewport ] + ); const archiveLabels = useArchiveLabel( templateSlug ); const defaultRenderingMode = postWithTemplate ? 'template-locked' : 'all'; const defaultEditorSettings = useMemo( () => { diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 5a2f1e2ec4d1a9..295f4ec3cf5c60 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -8,10 +8,11 @@ import classnames from 'classnames'; */ import { useSelect } from '@wordpress/data'; import { Notice } from '@wordpress/components'; -import { useInstanceId } from '@wordpress/compose'; +import { useInstanceId, useViewportMatch } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; import { BlockBreadcrumb, + BlockToolbar, store as blockEditorStore, privateApis as blockEditorPrivateApis, BlockInspector, @@ -92,6 +93,8 @@ export default function Editor( { listViewToggleElement, isLoading } ) { const { type: editedPostType } = editedPost; + const isLargeViewport = useViewportMatch( 'medium' ); + const { context, contextPost, @@ -232,6 +235,9 @@ export default function Editor( { listViewToggleElement, isLoading } ) { <SidebarInspectorFill> <BlockInspector /> </SidebarInspectorFill> + { ! isLargeViewport && ( + <BlockToolbar hideDragHandle /> + ) } <SiteEditorCanvas /> <BlockRemovalWarningModal rules={ blockRemovalRules } diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index c6dbe4b6b91449..2d751be691a745 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -9,8 +9,8 @@ import classnames from 'classnames'; import { useViewportMatch, useReducedMotion } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { + BlockToolbar, __experimentalPreviewOptions as PreviewOptions, - privateApis as blockEditorPrivateApis, store as blockEditorStore, } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; @@ -43,8 +43,6 @@ import { import { unlock } from '../../lock-unlock'; import { FOCUSABLE_ENTITIES } from '../../utils/constants'; -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); - export default function HeaderEditMode( { setListViewToggleElement } ) { const { deviceType, @@ -157,7 +155,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { } ) } > - <BlockContextualToolbar isFixed /> + <BlockToolbar hideDragHandle /> </div> <Popover.Slot ref={ blockToolbarRef } diff --git a/packages/edit-site/src/components/header-edit-mode/style.scss b/packages/edit-site/src/components/header-edit-mode/style.scss index 0fe335c5292ba2..cbd0a7422b5364 100644 --- a/packages/edit-site/src/components/header-edit-mode/style.scss +++ b/packages/edit-site/src/components/header-edit-mode/style.scss @@ -49,9 +49,6 @@ $header-toolbar-min-width: 335px; min-width: 0; } - .block-editor-block-contextual-toolbar.is-fixed { - border: none; - } } .edit-site-header-edit-mode__toolbar { @@ -191,12 +188,50 @@ $header-toolbar-min-width: 335px; .edit-site-header-edit-mode__document-tools .edit-site-header-edit-mode__toolbar > * + * { margin-left: $grid-unit-10; } + + .block-editor-block-mover { + border-left: none; + + &::before { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + } } .has-fixed-toolbar { .selected-block-tools-wrapper { overflow-x: scroll; + .block-editor-block-contextual-toolbar { + border-bottom: 0; + } + + // Modified group borders + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } + &.is-collapsed { display: none; } diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 9251f528ca5ee4..9d4cb4cb60103a 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { BlockToolbar } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; @@ -16,9 +16,6 @@ import { store as preferencesStore } from '@wordpress/preferences'; import DocumentTools from './document-tools'; import SaveButton from '../save-button'; import MoreMenu from '../more-menu'; -import { unlock } from '../../lock-unlock'; - -const { BlockContextualToolbar } = unlock( blockEditorPrivateApis ); function Header( { setListViewToggleElement } ) { const isLargeViewport = useViewportMatch( 'medium' ); @@ -56,7 +53,7 @@ function Header( { setListViewToggleElement } ) { { hasFixedToolbar && isLargeViewport && ( <> <div className="selected-block-tools-wrapper"> - <BlockContextualToolbar isFixed /> + <BlockToolbar hideDragHandle /> </div> <Popover.Slot ref={ blockToolbarRef } diff --git a/packages/edit-widgets/src/components/header/style.scss b/packages/edit-widgets/src/components/header/style.scss index e279b0f79b4585..2dd4b88eebddfa 100644 --- a/packages/edit-widgets/src/components/header/style.scss +++ b/packages/edit-widgets/src/components/header/style.scss @@ -12,10 +12,31 @@ .selected-block-tools-wrapper { overflow-x: hidden; - } - .block-editor-block-contextual-toolbar.is-fixed { - border: none; + .block-editor-block-contextual-toolbar { + border-bottom: 0; + } + + // Modified group borders + .components-toolbar-group, + .components-toolbar { + border-right: none; + + &::after { + content: ""; + width: $border-width; + margin-top: $grid-unit + $grid-unit-05; + margin-bottom: $grid-unit + $grid-unit-05; + background-color: $gray-300; + margin-left: $grid-unit; + } + + & .components-toolbar-group.components-toolbar-group { + &::after { + display: none; + } + } + } } } diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js index 12a70e2e4da279..a9024da1f0074f 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-content/index.js @@ -3,11 +3,13 @@ */ import { BlockList, + BlockToolbar, BlockTools, BlockSelectionClearer, WritingFlow, __unstableEditorStyles as EditorStyles, } from '@wordpress/block-editor'; +import { useViewportMatch } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as preferencesStore } from '@wordpress/preferences'; @@ -29,6 +31,7 @@ export default function WidgetAreasBlockEditorContent( { ), [] ); + const isLargeViewport = useViewportMatch( 'medium' ); const styles = useMemo( () => { return hasThemeStyles ? blockEditorSettings.styles : []; @@ -37,6 +40,7 @@ export default function WidgetAreasBlockEditorContent( { return ( <div className="edit-widgets-block-editor"> <Notices /> + { ! isLargeViewport && <BlockToolbar hideDragHandle /> } <BlockTools> <KeyboardShortcuts /> <EditorStyles diff --git a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js index 1fbf51d05f7b7f..4abc420434cc44 100644 --- a/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js +++ b/packages/edit-widgets/src/components/widget-areas-block-editor-provider/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { SlotFillProvider } from '@wordpress/components'; +import { useViewportMatch } from '@wordpress/compose'; import { uploadMedia } from '@wordpress/media-utils'; import { useDispatch, useSelect } from '@wordpress/data'; import { @@ -32,6 +33,7 @@ export default function WidgetAreasBlockEditorProvider( { ...props } ) { const mediaPermissions = useResourcePermissions( 'media' ); + const isLargeViewport = useViewportMatch( 'medium' ); const { reusableBlocks, isFixedToolbarActive, @@ -78,7 +80,7 @@ export default function WidgetAreasBlockEditorProvider( { return { ...blockEditorSettings, __experimentalReusableBlocks: reusableBlocks, - hasFixedToolbar: isFixedToolbarActive, + hasFixedToolbar: isFixedToolbarActive || ! isLargeViewport, keepCaretInsideBlock, mediaUpload: mediaUploadBlockEditor, templateLock: 'all', @@ -89,6 +91,7 @@ export default function WidgetAreasBlockEditorProvider( { }, [ blockEditorSettings, isFixedToolbarActive, + isLargeViewport, keepCaretInsideBlock, mediaPermissions.canCreate, reusableBlocks, diff --git a/storybook/stories/playground/box/index.js b/storybook/stories/playground/box/index.js index 444b7810e5e89e..4cb7047b73ec20 100644 --- a/storybook/stories/playground/box/index.js +++ b/storybook/stories/playground/box/index.js @@ -6,6 +6,7 @@ import { registerCoreBlocks } from '@wordpress/block-library'; import { BlockEditorProvider, BlockCanvas, + BlockToolbar, BlockTools, } from '@wordpress/block-editor'; @@ -36,6 +37,7 @@ export default function EditorBox() { hasFixedToolbar: true, } } > + <BlockToolbar hideDragHandle /> <BlockTools /> <BlockCanvas height="100%" styles={ editorStyles } /> </BlockEditorProvider> diff --git a/storybook/stories/playground/with-undo-redo/index.js b/storybook/stories/playground/with-undo-redo/index.js index a51d8624282a6d..537ea16aade99b 100644 --- a/storybook/stories/playground/with-undo-redo/index.js +++ b/storybook/stories/playground/with-undo-redo/index.js @@ -7,6 +7,7 @@ import { registerCoreBlocks } from '@wordpress/block-library'; import { BlockEditorProvider, BlockCanvas, + BlockToolbar, BlockTools, } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; @@ -58,6 +59,7 @@ export default function EditorWithUndoRedo() { icon={ redoIcon } label="Redo" /> + <BlockToolbar hideDragHandle /> <BlockTools /> </div> <BlockCanvas height="100%" styles={ editorStyles } /> diff --git a/storybook/stories/playground/with-undo-redo/style.css b/storybook/stories/playground/with-undo-redo/style.css index a3f0bd5d23debf..6ed082a1de7196 100644 --- a/storybook/stories/playground/with-undo-redo/style.css +++ b/storybook/stories/playground/with-undo-redo/style.css @@ -6,5 +6,9 @@ display: flex; align-items: center; border-bottom: 1px solid #ddd; - height: 48px; + height: 46px; } + +.editor-with-undo-redo__toolbar .components-accessible-toolbar.block-editor-block-contextual-toolbar { + border-bottom: none; +} \ No newline at end of file diff --git a/test/e2e/specs/editor/various/is-typing.spec.js b/test/e2e/specs/editor/various/is-typing.spec.js index 0cd5e0d6f64953..8063f688409c4e 100644 --- a/test/e2e/specs/editor/various/is-typing.spec.js +++ b/test/e2e/specs/editor/various/is-typing.spec.js @@ -14,24 +14,27 @@ test.describe( 'isTyping', () => { // Insert paragraph await page.keyboard.type( 'Type' ); - const blockToolbar = page.locator( - 'role=toolbar[name="Block tools"i]' + const blockToolbarPopover = page.locator( + '[data-wp-component="Popover"]', + { + has: page.locator( 'role=toolbar[name="Block tools"i]' ), + } ); - // Toolbar should not be showing - await expect( blockToolbar ).toBeHidden(); + // Toolbar Popover should not be showing + await expect( blockToolbarPopover ).toBeHidden(); // Moving the mouse shows the toolbar. await editor.showBlockToolbar(); - // Toolbar is visible. - await expect( blockToolbar ).toBeVisible(); + // Toolbar Popover is visible. + await expect( blockToolbarPopover ).toBeVisible(); // Typing again hides the toolbar await page.keyboard.type( ' and continue' ); - // Toolbar is hidden again - await expect( blockToolbar ).toBeHidden(); + // Toolbar Popover is hidden again + await expect( blockToolbarPopover ).toBeHidden(); } ); test( 'should not close the dropdown when typing in it', async ( { @@ -41,17 +44,22 @@ test.describe( 'isTyping', () => { // Add a block with a dropdown in the toolbar that contains an input. await editor.insertBlock( { name: 'core/query' } ); - // Tab to Start Blank Button - await page.keyboard.press( 'Tab' ); - // Select the Start Blank Button - await page.keyboard.press( 'Enter' ); - // Select the First variation - await page.keyboard.press( 'Enter' ); + await editor.canvas + .getByRole( 'document', { name: 'Block: Query Loop' } ) + .getByRole( 'button', { name: 'Start blank' } ) + .click(); + + await editor.canvas + .getByRole( 'button', { name: 'Title & Date' } ) + .click(); + // Moving the mouse shows the toolbar. await editor.showBlockToolbar(); // Open the dropdown. - await page.getByRole( 'button', { name: 'Display settings' } ).click(); - + const displaySettings = page.getByRole( 'button', { + name: 'Display settings', + } ); + await displaySettings.click(); const itemsPerPageInput = page.getByLabel( 'Items per Page' ); // Make sure we're where we think we are await expect( itemsPerPageInput ).toBeFocused(); diff --git a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js index 0223821613f55e..a8e49f7a6b84dd 100644 --- a/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js +++ b/test/e2e/specs/editor/various/shortcut-focus-toolbar.spec.js @@ -98,6 +98,10 @@ test.describe( 'Focus toolbar shortcut (alt + F10)', () => { // Test: Focus the block toolbar from empty block await editor.insertBlock( { name: 'core/paragraph' } ); + // This fails if we don't wait for the block toolbar to show. + await expect( + toolbarUtils.blockToolbarParagraphButton + ).toBeVisible(); await toolbarUtils.moveToToolbarShortcut(); await expect( toolbarUtils.blockToolbarParagraphButton From d541afc710a01a6ac741fe0a03c65c4f4ddd8ca6 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 00:39:17 +0200 Subject: [PATCH 088/325] Mobile: remove rich text multiline (#56117) --- .../src/components/rich-text/index.js | 51 ++---------------- .../src/components/rich-text/index.native.js | 33 +++--------- .../rich-text/native/index.native.js | 53 +++---------------- .../components/rich-text/with-deprecations.js | 51 ++++++++++++++++++ .../src/components/post-title/index.native.js | 1 - 5 files changed, 70 insertions(+), 119 deletions(-) create mode 100644 packages/block-editor/src/components/rich-text/with-deprecations.js diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index 1a6793ca9efe73..a3b7b44e214a5b 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -13,14 +13,11 @@ import { createContext, } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; -import { children as childrenSource } from '@wordpress/blocks'; -import { useInstanceId, useMergeRefs } from '@wordpress/compose'; +import { useMergeRefs } from '@wordpress/compose'; import { __unstableUseRichText as useRichText, - __unstableCreateElement, removeFormat, } from '@wordpress/rich-text'; -import deprecated from '@wordpress/deprecated'; import { Popover } from '@wordpress/components'; /** @@ -46,7 +43,7 @@ import { useFirefoxCompat } from './use-firefox-compat'; import FormatEdit from './format-edit'; import { getAllowedFormats } from './utils'; import { Content } from './content'; -import RichTextMultiline from './multiline'; +import { withDeprecations } from './with-deprecations'; export const keyboardShortcutContext = createContext(); export const inputEventContext = createContext(); @@ -387,47 +384,9 @@ export function RichTextWrapper( ); } -const ForwardedRichTextWrapper = forwardRef( RichTextWrapper ); - -function RichTextSwitcher( props, ref ) { - let value = props.value; - let onChange = props.onChange; - - // Handle deprecated format. - if ( Array.isArray( value ) ) { - deprecated( 'wp.blockEditor.RichText value prop as children type', { - since: '6.1', - version: '6.3', - alternative: 'value prop as string', - link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', - } ); - - value = childrenSource.toHTML( props.value ); - onChange = ( newValue ) => - props.onChange( - childrenSource.fromDOM( - __unstableCreateElement( document, newValue ).childNodes - ) - ); - } - - const Component = props.multiline - ? RichTextMultiline - : ForwardedRichTextWrapper; - const instanceId = useInstanceId( RichTextSwitcher ); - - return ( - <Component - { ...props } - identifier={ props.identifier || instanceId } - value={ value } - onChange={ onChange } - ref={ ref } - /> - ); -} - -const ForwardedRichTextContainer = forwardRef( RichTextSwitcher ); +const ForwardedRichTextContainer = withDeprecations( + forwardRef( RichTextWrapper ) +); ForwardedRichTextContainer.Content = Content; ForwardedRichTextContainer.isEmpty = ( value ) => { diff --git a/packages/block-editor/src/components/rich-text/index.native.js b/packages/block-editor/src/components/rich-text/index.native.js index 9427962eced198..acadfb24a72217 100644 --- a/packages/block-editor/src/components/rich-text/index.native.js +++ b/packages/block-editor/src/components/rich-text/index.native.js @@ -39,17 +39,17 @@ import FormatToolbarContainer from './format-toolbar-container'; import { store as blockEditorStore } from '../../store'; import { addActiveFormats, - getMultilineTag, getAllowedFormats, createLinkInParagraph, } from './utils'; import EmbedHandlerPicker from './embed-handler-picker'; import { Content } from './content'; import RichText from './native'; +import { withDeprecations } from './with-deprecations'; const classes = 'block-editor-rich-text__editable'; -function RichTextWrapper( +export function RichTextWrapper( { children, tagName, @@ -58,7 +58,6 @@ function RichTextWrapper( value: originalValue, onChange: originalOnChange, isSelected: originalIsSelected, - multiline, inlineToolbar, wrapperClassName, autocompleters, @@ -80,7 +79,6 @@ function RichTextWrapper( disableLineBreaks, unstableOnFocus, __unstableAllowPrefixTransformations, - __unstableMultilineRootTag, // Native props. __unstableMobileNoFocusOnMount, deleteEnter, @@ -179,7 +177,6 @@ function RichTextWrapper( selectionChange, __unstableMarkAutomaticChange, } = useDispatch( blockEditorStore ); - const multilineTag = getMultilineTag( multiline ); const adjustedAllowedFormats = getAllowedFormats( { allowedFormats, disableFormats, @@ -261,10 +258,7 @@ function RichTextWrapper( if ( ! hasPastedBlocks || ! isEmpty( before ) ) { blocks.push( onSplit( - toHTMLString( { - value: before, - multilineTag, - } ), + toHTMLString( { value: before } ), ! isAfterOriginal ) ); @@ -288,13 +282,7 @@ function RichTextWrapper( : ! onSplitMiddle || ! isEmpty( after ) ) { blocks.push( - onSplit( - toHTMLString( { - value: after, - multilineTag, - } ), - isAfterOriginal - ) + onSplit( toHTMLString( { value: after } ), isAfterOriginal ) ); } @@ -308,7 +296,7 @@ function RichTextWrapper( onReplace( blocks, indexToSelect, initialPosition ); }, - [ onReplace, onSplit, multilineTag, onSplitMiddle ] + [ onReplace, onSplit, onSplitMiddle ] ); const onEnter = useCallback( @@ -370,7 +358,6 @@ function RichTextWrapper( onReplace, onSplit, __unstableMarkAutomaticChange, - multiline, splitValue, onSplitAtEnd, ] @@ -392,9 +379,6 @@ function RichTextWrapper( if ( isInternal ) { const pastedValue = create( { html, - multilineTag, - multilineWrapperTags: - multilineTag === 'li' ? [ 'ul', 'ol' ] : undefined, preserveWhiteSpace, } ); addActiveFormats( pastedValue, activeFormats ); @@ -496,7 +480,6 @@ function RichTextWrapper( onSplit, splitValue, __unstableEmbedURLOnPaste, - multilineTag, preserveWhiteSpace, pastePlainText, ] @@ -568,7 +551,6 @@ function RichTextWrapper( onPaste={ onPaste } __unstableIsSelected={ isSelected } __unstableInputRule={ inputRule } - __unstableMultilineTag={ multilineTag } __unstableOnEnterFormattedText={ enterFormattedText } __unstableOnExitFormattedText={ exitFormattedText } __unstableOnCreateUndoLevel={ __unstableMarkLastChangeAsPersistent } @@ -582,7 +564,6 @@ function RichTextWrapper( __unstableAllowPrefixTransformations={ __unstableAllowPrefixTransformations } - __unstableMultilineRootTag={ __unstableMultilineRootTag } // Native props. blockIsSelected={ originalIsSelected !== undefined @@ -675,7 +656,9 @@ function RichTextWrapper( ); } -const ForwardedRichTextContainer = forwardRef( RichTextWrapper ); +const ForwardedRichTextContainer = withDeprecations( + forwardRef( RichTextWrapper ) +); ForwardedRichTextContainer.Content = Content; diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 951d52ece6d694..165316fdbde769 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -105,27 +105,11 @@ const DEFAULT_FONT_SIZE = 16; const MIN_LINE_HEIGHT = 1; export class RichText extends Component { - constructor( { - value, - selectionStart, - selectionEnd, - __unstableMultilineTag: multiline, - } ) { + constructor( { value, selectionStart, selectionEnd } ) { super( ...arguments ); - this.isMultiline = false; - if ( multiline === true || multiline === 'p' || multiline === 'li' ) { - this.multilineTag = multiline === true ? 'p' : multiline; - this.isMultiline = true; - } - - if ( this.multilineTag === 'li' ) { - this.multilineWrapperTags = [ 'ul', 'ol' ]; - } - this.isIOS = Platform.OS === 'ios'; this.createRecord = this.createRecord.bind( this ); - this.restoreParagraphTags = this.restoreParagraphTags.bind( this ); this.onChangeFromAztec = this.onChangeFromAztec.bind( this ); this.onKeyDown = this.onKeyDown.bind( this ); this.handleEnter = this.handleEnter.bind( this ); @@ -223,8 +207,6 @@ export class RichText extends Component { ...create( { html: this.value, range: null, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ), }; @@ -235,12 +217,7 @@ export class RichText extends Component { valueToFormat( value ) { // Remove the outer root tags. - return this.removeRootTagsProducedByAztec( - toHTMLString( { - value, - multilineTag: this.multilineTag, - } ) - ); + return this.removeRootTagsProducedByAztec( toHTMLString( { value } ) ); } getActiveFormatNames( record ) { @@ -357,29 +334,15 @@ export class RichText extends Component { const contentWithoutRootTag = this.removeRootTagsProducedByAztec( unescapeSpaces( event.nativeEvent.text ) ); - let formattedContent = contentWithoutRootTag; - if ( ! this.isIOS ) { - formattedContent = this.restoreParagraphTags( - contentWithoutRootTag, - this.multilineTag - ); - } this.debounceCreateUndoLevel(); - const refresh = this.value !== formattedContent; - this.value = formattedContent; + const refresh = this.value !== contentWithoutRootTag; + this.value = contentWithoutRootTag; // We don't want to refresh if our goal is just to create a record. if ( refresh ) { - this.props.onChange( formattedContent ); - } - } - - restoreParagraphTags( value, tag ) { - if ( tag === 'p' && ( ! value || ! value.startsWith( '<p>' ) ) ) { - return '<p>' + value + '</p>'; + this.props.onChange( contentWithoutRootTag ); } - return value; } /* @@ -739,8 +702,6 @@ export class RichText extends Component { if ( Array.isArray( value ) ) { return create( { html: childrenBlock.toHTML( value ), - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ); } @@ -748,8 +709,6 @@ export class RichText extends Component { if ( this.props.format === 'string' ) { return create( { html: value, - multilineTag: this.multilineTag, - multilineWrapperTags: this.multilineWrapperTags, preserveWhiteSpace, } ); } @@ -1323,7 +1282,7 @@ export class RichText extends Component { fontWeight={ this.props.fontWeight } fontStyle={ this.props.fontStyle } disableEditingMenu={ disableEditingMenu } - isMultiline={ this.isMultiline } + isMultiline={ false } textAlign={ this.props.textAlign } { ...( this.isIOS ? { maxWidth } : {} ) } minWidth={ minWidth } diff --git a/packages/block-editor/src/components/rich-text/with-deprecations.js b/packages/block-editor/src/components/rich-text/with-deprecations.js new file mode 100644 index 00000000000000..8feab2206900af --- /dev/null +++ b/packages/block-editor/src/components/rich-text/with-deprecations.js @@ -0,0 +1,51 @@ +/** + * WordPress dependencies + */ +import { forwardRef } from '@wordpress/element'; +import { children as childrenSource } from '@wordpress/blocks'; +import { useInstanceId } from '@wordpress/compose'; +import { __unstableCreateElement } from '@wordpress/rich-text'; +import deprecated from '@wordpress/deprecated'; + +/** + * Internal dependencies + */ +import RichTextMultiline from './multiline'; + +export function withDeprecations( Component ) { + return forwardRef( ( props, ref ) => { + let value = props.value; + let onChange = props.onChange; + + // Handle deprecated format. + if ( Array.isArray( value ) ) { + deprecated( 'wp.blockEditor.RichText value prop as children type', { + since: '6.1', + version: '6.3', + alternative: 'value prop as string', + link: 'https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields/', + } ); + + value = childrenSource.toHTML( props.value ); + onChange = ( newValue ) => + props.onChange( + childrenSource.fromDOM( + __unstableCreateElement( document, newValue ).childNodes + ) + ); + } + + const NewComponent = props.multiline ? RichTextMultiline : Component; + const instanceId = useInstanceId( NewComponent ); + + return ( + <NewComponent + { ...props } + identifier={ props.identifier || instanceId } + value={ value } + onChange={ onChange } + ref={ ref } + /> + ); + } ); +} diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index 1ec0dd3ade3bfc..6d905e743581e9 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -155,7 +155,6 @@ class PostTitle extends Component { tagsToEliminate={ [ 'strong' ] } unstableOnFocus={ this.props.onSelect } onBlur={ this.props.onBlur } // Always assign onBlur as a props. - multiline={ false } style={ titleStyles } styles={ styles } fontSize={ 24 } From 67bea2afc3f4bc3e3f23525f53bcdf70ee634f85 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Thu, 7 Dec 2023 19:01:51 -0600 Subject: [PATCH 089/325] Remove BlockTools BackCompat (#56874) It was deprecated in 5.8 and slated to be removed in 6.3. --- .../src/components/block-tools/back-compat.js | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 packages/block-editor/src/components/block-tools/back-compat.js diff --git a/packages/block-editor/src/components/block-tools/back-compat.js b/packages/block-editor/src/components/block-tools/back-compat.js deleted file mode 100644 index 14027760be1e6a..00000000000000 --- a/packages/block-editor/src/components/block-tools/back-compat.js +++ /dev/null @@ -1,35 +0,0 @@ -/** - * WordPress dependencies - */ -import { useContext } from '@wordpress/element'; -import { Disabled } from '@wordpress/components'; -import deprecated from '@wordpress/deprecated'; - -/** - * Internal dependencies - */ -import InsertionPoint, { InsertionPointOpenRef } from './insertion-point'; -import BlockToolbarPopover from './block-toolbar-popover'; - -export default function BlockToolsBackCompat( { children } ) { - const openRef = useContext( InsertionPointOpenRef ); - const isDisabled = useContext( Disabled.Context ); - - // If context is set, `BlockTools` is a parent component. - if ( openRef || isDisabled ) { - return children; - } - - deprecated( 'wp.components.Popover.Slot name="block-toolbar"', { - alternative: 'wp.blockEditor.BlockTools', - since: '5.8', - version: '6.3', - } ); - - return ( - <InsertionPoint __unstablePopoverSlot="block-toolbar"> - <BlockToolbarPopover __unstablePopoverSlot="block-toolbar" /> - { children } - </InsertionPoint> - ); -} From 3ad117e075dc6bab30a40d7f39e5455b5b66748b Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Thu, 7 Dec 2023 17:29:31 -0800 Subject: [PATCH 090/325] `BorderControl`: Fix button styles (#56730) * `BorderControl`: Fix button styles * Add __next40pxDefaultSize to Button when BorderControl has large size * Update changelog * Remove explicit return leftover from unused changes --- packages/components/CHANGELOG.md | 1 + .../border-control-dropdown/component.tsx | 4 +++- .../border-control/border-control-dropdown/hook.ts | 5 +++-- packages/components/src/border-control/styles.ts | 11 ++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 8a6d52e7582833..ed33677b55a82a 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -16,6 +16,7 @@ - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). +- `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). ## 25.13.0 (2023-11-29) diff --git a/packages/components/src/border-control/border-control-dropdown/component.tsx b/packages/components/src/border-control/border-control-dropdown/component.tsx index 4f43a6ed0ce55d..3ee01bcda8f3b3 100644 --- a/packages/components/src/border-control/border-control-dropdown/component.tsx +++ b/packages/components/src/border-control/border-control-dropdown/component.tsx @@ -149,6 +149,7 @@ const BorderControlDropdown = ( popoverControlsClassName, resetButtonClassName, showDropdownHeader, + size, __unstablePopoverProps, ...otherProps } = useBorderControlDropdown( props ); @@ -178,6 +179,7 @@ const BorderControlDropdown = ( tooltipPosition={ dropdownPosition } label={ __( 'Border color and style picker' ) } showTooltip={ true } + __next40pxDefaultSize={ size === '__unstable-large' ? true : false } > <span className={ indicatorWrapperClassName }> <ColorIndicator @@ -198,7 +200,7 @@ const BorderControlDropdown = ( <HStack> <StyledLabel>{ __( 'Border color' ) }</StyledLabel> <Button - isSmall + size="small" label={ __( 'Close border color' ) } icon={ closeSmall } onClick={ onClose } diff --git a/packages/components/src/border-control/border-control-dropdown/hook.ts b/packages/components/src/border-control/border-control-dropdown/hook.ts index b60aa52a34e2eb..5366babc266c61 100644 --- a/packages/components/src/border-control/border-control-dropdown/hook.ts +++ b/packages/components/src/border-control/border-control-dropdown/hook.ts @@ -57,8 +57,8 @@ export function useBorderControlDropdown( // Generate class names. const cx = useCx(); const classes = useMemo( () => { - return cx( styles.borderControlDropdown( size ), className ); - }, [ className, cx, size ] ); + return cx( styles.borderControlDropdown, className ); + }, [ className, cx ] ); const indicatorClassName = useMemo( () => { return cx( styles.borderColorIndicator ); @@ -95,6 +95,7 @@ export function useBorderControlDropdown( popoverContentClassName, popoverControlsClassName, resetButtonClassName, + size, __experimentalIsRenderedInSidebar, }; } diff --git a/packages/components/src/border-control/styles.ts b/packages/components/src/border-control/styles.ts index 322e9563d58a4d..1a2263b9fb6ff2 100644 --- a/packages/components/src/border-control/styles.ts +++ b/packages/components/src/border-control/styles.ts @@ -59,18 +59,11 @@ export const wrapperHeight = ( size?: 'default' | '__unstable-large' ) => { `; }; -export const borderControlDropdown = ( - size?: 'default' | '__unstable-large' -) => css` +export const borderControlDropdown = css` background: #fff; && > button { - /* - * Override button component styles to fit within BorderControl - * regardless of size. - */ - height: ${ size === '__unstable-large' ? '40px' : '30px' }; - width: ${ size === '__unstable-large' ? '40px' : '30px' }; + aspect-ratio: 1; padding: 0; display: flex; align-items: center; From 27b7d6202a69d8c055cda7761282f6726369d1ee Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Fri, 8 Dec 2023 09:29:37 +0200 Subject: [PATCH 091/325] DataViews: Remove TanStack (#56873) --- package-lock.json | 46 --- packages/dataviews/package.json | 1 - packages/dataviews/src/view-table.js | 502 ++++++++------------------- 3 files changed, 153 insertions(+), 396 deletions(-) diff --git a/package-lock.json b/package-lock.json index b47ce2e24a9e77..ded852521693f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15265,37 +15265,6 @@ "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" }, - "node_modules/@tanstack/react-table": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.10.3.tgz", - "integrity": "sha512-Qya1cJ+91arAlW7IRDWksRDnYw28O446jJ/ljkRSc663EaftJoBCAU10M+VV1K6MpCBLrXq1BD5IQc1zj/ZEjA==", - "dependencies": { - "@tanstack/table-core": "8.10.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16", - "react-dom": ">=16" - } - }, - "node_modules/@tanstack/table-core": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.10.3.tgz", - "integrity": "sha512-hJ55YfJlWbfzRROfcyA/kC1aZr/shsLA8XNAwN8jXylhYWGLnPmiJJISrUfj4dMMWRiFi0xBlnlC7MLH+zSrcw==", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@testing-library/dom": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", @@ -55060,7 +55029,6 @@ "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", @@ -67561,19 +67529,6 @@ "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" }, - "@tanstack/react-table": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.10.3.tgz", - "integrity": "sha512-Qya1cJ+91arAlW7IRDWksRDnYw28O446jJ/ljkRSc663EaftJoBCAU10M+VV1K6MpCBLrXq1BD5IQc1zj/ZEjA==", - "requires": { - "@tanstack/table-core": "8.10.3" - } - }, - "@tanstack/table-core": { - "version": "8.10.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.10.3.tgz", - "integrity": "sha512-hJ55YfJlWbfzRROfcyA/kC1aZr/shsLA8XNAwN8jXylhYWGLnPmiJJISrUfj4dMMWRiFi0xBlnlC7MLH+zSrcw==" - }, "@testing-library/dom": { "version": "9.3.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.1.tgz", @@ -70419,7 +70374,6 @@ "version": "file:packages/dataviews", "requires": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 3d15ea435ab4f8..1872480d759c37 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -28,7 +28,6 @@ "sideEffects": false, "dependencies": { "@babel/runtime": "^7.16.0", - "@tanstack/react-table": "^8.10.3", "@wordpress/a11y": "file:../a11y", "@wordpress/components": "file:../components", "@wordpress/compose": "file:../compose", diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 209b9e443dc2a2..e34d99008657bc 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -1,15 +1,3 @@ -/** - * External dependencies - */ -import { - getCoreRowModel, - getFilteredRowModel, - getSortedRowModel, - getPaginationRowModel, - useReactTable, - flexRender, -} from '@tanstack/react-table'; - /** * WordPress dependencies */ @@ -30,7 +18,7 @@ import { Icon, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { useMemo, Children, Fragment } from '@wordpress/element'; +import { Children, Fragment } from '@wordpress/element'; /** * Internal dependencies @@ -48,32 +36,23 @@ const { DropdownSubMenuTriggerV2: DropdownSubMenuTrigger, } = unlock( componentsPrivateApis ); -const EMPTY_OBJECT = {}; const sortingItemsInfo = { asc: { icon: arrowUp, label: __( 'Sort ascending' ) }, desc: { icon: arrowDown, label: __( 'Sort descending' ) }, }; const sortIcons = { asc: chevronUp, desc: chevronDown }; -function HeaderMenu( { dataView, header } ) { - if ( header.isPlaceholder ) { - return null; - } - const text = flexRender( - header.column.columnDef.header, - header.getContext() - ); - const isSortable = !! header.column.getCanSort(); - const isHidable = !! header.column.getCanHide(); +function HeaderMenu( { field, view, onChangeView } ) { + const isSortable = field.enableSorting !== false; + const isHidable = field.enableHiding !== false; if ( ! isSortable && ! isHidable ) { - return text; + return field.header; } - const sortedDirection = header.column.getIsSorted(); - + const isSorted = view.sort?.field === field.id; let filter, filterInView; const otherFilters = []; - if ( header.column.columnDef.type === ENUMERATION_TYPE ) { - let columnOperators = header.column.columnDef.filterBy?.operators; + if ( field.type === ENUMERATION_TYPE ) { + let columnOperators = field.filterBy?.operators; if ( ! columnOperators || ! Array.isArray( columnOperators ) ) { columnOperators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; } @@ -82,9 +61,9 @@ function HeaderMenu( { dataView, header } ) { ); if ( operators.length >= 0 ) { filter = { - field: header.column.columnDef.id, + field: field.id, operators, - elements: header.column.columnDef.elements || [], + elements: field.elements || [], }; filterInView = { field: filter.field, @@ -96,31 +75,25 @@ function HeaderMenu( { dataView, header } ) { const isFilterable = !! filter; if ( isFilterable ) { - const columnFilters = dataView.getState().columnFilters; + const columnFilters = view.filters; columnFilters.forEach( ( columnFilter ) => { - const [ field, operator ] = - Object.keys( columnFilter )[ 0 ].split( ':' ); - const value = Object.values( columnFilter )[ 0 ]; - if ( field === filter.field ) { + if ( columnFilter.field === filter.field ) { filterInView = { - field, - operator, - value, + ...columnFilter, }; } else { otherFilters.push( columnFilter ); } } ); } - return ( <DropdownMenu align="start" trigger={ <Button - icon={ sortIcons[ header.column.getIsSorted() ] } + icon={ isSorted && sortIcons[ view.sort.direction ] } iconPosition="right" - text={ text } + text={ field.header } style={ { padding: 0 } } size="compact" /> @@ -130,47 +103,61 @@ function HeaderMenu( { dataView, header } ) { { isSortable && ( <DropdownMenuGroup> { Object.entries( sortingItemsInfo ).map( - ( [ direction, info ] ) => ( - <DropdownMenuItem - key={ direction } - role="menuitemradio" - aria-checked={ - sortedDirection === direction - } - prefix={ <Icon icon={ info.icon } /> } - suffix={ - sortedDirection === direction && ( - <Icon icon={ check } /> - ) - } - onSelect={ ( event ) => { - event.preventDefault(); - if ( sortedDirection === direction ) { - dataView.resetSorting(); - } else { - dataView.setSorting( [ - { - id: header.column.id, - desc: direction === 'desc', - }, - ] ); + ( [ direction, info ] ) => { + const isActive = + isSorted && + view.sort.direction === direction; + return ( + <DropdownMenuItem + key={ direction } + role="menuitemradio" + aria-checked={ isActive } + prefix={ <Icon icon={ info.icon } /> } + suffix={ + isActive && <Icon icon={ check } /> } - } } - > - { info.label } - </DropdownMenuItem> - ) + onSelect={ ( event ) => { + event.preventDefault(); + if ( + isSorted && + view.sort.direction === + direction + ) { + onChangeView( { + ...view, + sort: undefined, + } ); + } else { + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); + } + } } + > + { info.label } + </DropdownMenuItem> + ); + } ) } </DropdownMenuGroup> ) } { isHidable && ( <DropdownMenuItem role="menuitemradio" - aria-checked={ ! header.column.getIsVisible() } + aria-checked={ false } prefix={ <Icon icon={ unseen } /> } onSelect={ ( event ) => { event.preventDefault(); - header.column.getToggleVisibilityHandler()( event ); + onChangeView( { + ...view, + hiddenFields: view.hiddenFields.concat( + field.id + ), + } ); } } > { __( 'Hide' ) } @@ -216,17 +203,20 @@ function HeaderMenu( { dataView, header } ) { ) } onSelect={ () => { - dataView.setColumnFilters( [ - ...otherFilters, - { - [ filter.field + - ':' + - filterInView?.operator ]: - isActive + onChangeView( { + ...view, + filters: [ + ...otherFilters, + { + field: filter.field, + operator: + filterInView?.operator, + value: isActive ? undefined : element.value, - }, - ] ); + }, + ], + } ); } } > { element.label } @@ -270,15 +260,18 @@ function HeaderMenu( { dataView, header } ) { ) } onSelect={ () => - dataView.setColumnFilters( [ - ...otherFilters, - { - [ filter.field + - ':' + - OPERATOR_IN ]: - filterInView?.value, - }, - ] ) + onChangeView( { + ...view, + filters: [ + ...otherFilters, + { + field: filter.field, + operator: + OPERATOR_IN, + value: filterInView?.value, + }, + ], + } ) } > { __( 'Is' ) } @@ -297,15 +290,18 @@ function HeaderMenu( { dataView, header } ) { ) } onSelect={ () => - dataView.setColumnFilters( [ - ...otherFilters, - { - [ filter.field + - ':' + - OPERATOR_NOT_IN ]: - filterInView?.value, - }, - ] ) + onChangeView( { + ...view, + filters: [ + ...otherFilters, + { + field: filter.field, + operator: + OPERATOR_NOT_IN, + value: filterInView?.value, + }, + ], + } ) } > { __( 'Is not' ) } @@ -340,208 +336,18 @@ function ViewTable( { data, getItemId, isLoading = false, - paginationInfo, deferredRendering, } ) { - const columns = useMemo( () => { - const _columns = fields.map( ( field ) => { - const { render, getValue, ...column } = field; - column.cell = ( props ) => render( { item: props.row.original } ); - if ( getValue ) { - column.accessorFn = ( item ) => getValue( { item } ); - } - return column; - } ); - if ( actions?.length ) { - _columns.push( { - header: __( 'Actions' ), - id: 'actions', - cell: ( props ) => { - return ( - <ItemActions - item={ props.row.original } - actions={ actions } - /> - ); - }, - enableHiding: false, - } ); - } - - return _columns; - }, [ fields, actions ] ); - - const columnVisibility = useMemo( () => { - if ( ! view.hiddenFields?.length ) { - return; - } - return view.hiddenFields.reduce( - ( accumulator, fieldId ) => ( { - ...accumulator, - [ fieldId ]: false, - } ), - {} - ); - }, [ view.hiddenFields ] ); - - /** - * Transform the filters from the view format into the tanstack columns filter format. - * - * Input: - * - * view.filters = [ - * { field: 'date', operator: 'before', value: '2020-01-01' }, - * { field: 'date', operator: 'after', value: '2020-01-01' }, - * ] - * - * Output: - * - * columnFilters = [ - * { "date:before": '2020-01-01' }, - * { "date:after": '2020-01-01' } - * ] - * - * @param {Array} filters The view filters to transform. - * @return {Array} The transformed TanStack column filters. - */ - const toTanStackColumnFilters = ( filters ) => - filters?.map( ( filter ) => ( { - [ filter.field + ':' + filter.operator ]: filter.value, - } ) ); - - /** - * Transform the filters from the view format into the tanstack columns filter format. - * - * Input: - * - * columnFilters = [ - * { "date:before": '2020-01-01'}, - * { "date:after": '2020-01-01' } - * ] - * - * Output: - * - * view.filters = [ - * { field: 'date', operator: 'before', value: '2020-01-01' }, - * { field: 'date', operator: 'after', value: '2020-01-01' }, - * ] - * - * @param {Array} filters The TanStack column filters to transform. - * @return {Array} The transformed view filters. - */ - const fromTanStackColumnFilters = ( filters ) => - filters.map( ( filter ) => { - const [ key, value ] = Object.entries( filter )[ 0 ]; - const [ field, operator ] = key.split( ':' ); - return { field, operator, value }; - } ); - + const visibleFields = fields.filter( + ( field ) => + ! view.hiddenFields.includes( field.id ) && + ! [ view.layout.mediaField, view.layout.primaryField ].includes( + field.id + ) + ); const shownData = useAsyncList( data ); const usedData = deferredRendering ? shownData : data; - const dataView = useReactTable( { - data: usedData, - columns, - manualSorting: true, - manualFiltering: true, - manualPagination: true, - enableRowSelection: true, - state: { - sorting: view.sort - ? [ - { - id: view.sort.field, - desc: view.sort.direction === 'desc', - }, - ] - : [], - globalFilter: view.search, - columnFilters: toTanStackColumnFilters( view.filters ), - pagination: { - pageIndex: view.page, - pageSize: view.perPage, - }, - columnVisibility: columnVisibility ?? EMPTY_OBJECT, - }, - getRowId: getItemId, - onSortingChange: ( sortingUpdater ) => { - onChangeView( ( currentView ) => { - const sort = - typeof sortingUpdater === 'function' - ? sortingUpdater( - currentView.sort - ? [ - { - id: currentView.sort.field, - desc: - currentView.sort - .direction === 'desc', - }, - ] - : [] - ) - : sortingUpdater; - if ( ! sort.length ) { - return { - ...currentView, - sort: {}, - }; - } - const [ { id, desc } ] = sort; - return { - ...currentView, - sort: { field: id, direction: desc ? 'desc' : 'asc' }, - }; - } ); - }, - onColumnVisibilityChange: ( columnVisibilityUpdater ) => { - onChangeView( ( currentView ) => { - const hiddenFields = Object.entries( - columnVisibilityUpdater() - ).reduce( - ( accumulator, [ fieldId, value ] ) => { - if ( value ) { - return accumulator.filter( - ( id ) => id !== fieldId - ); - } - return [ ...accumulator, fieldId ]; - }, - [ ...( currentView.hiddenFields || [] ) ] - ); - return { - ...currentView, - hiddenFields, - }; - } ); - }, - onGlobalFilterChange: ( value ) => { - onChangeView( { ...view, search: value, page: 1 } ); - }, - onColumnFiltersChange: ( columnFiltersUpdater ) => { - onChangeView( { - ...view, - filters: fromTanStackColumnFilters( columnFiltersUpdater() ), - page: 1, - } ); - }, - onPaginationChange: ( paginationUpdater ) => { - onChangeView( ( currentView ) => { - const { pageIndex, pageSize } = paginationUpdater( { - pageIndex: currentView.page, - pageSize: currentView.perPage, - } ); - return { ...view, page: pageIndex, perPage: pageSize }; - } ); - }, - getCoreRowModel: getCoreRowModel(), - getFilteredRowModel: getFilteredRowModel(), - getSortedRowModel: getSortedRowModel(), - getPaginationRowModel: getPaginationRowModel(), - pageCount: paginationInfo.totalPages, - } ); - - const { rows } = dataView.getRowModel(); - const hasRows = !! rows?.length; + const hasData = !! usedData?.length; if ( isLoading ) { // TODO:Add spinner or progress bar.. return ( @@ -550,77 +356,75 @@ function ViewTable( { </div> ); } - const sortValues = { asc: 'ascending', desc: 'descending' }; - return ( <div className="dataviews-table-view-wrapper"> - { hasRows && ( + { hasData && ( <table className="dataviews-table-view"> <thead> - { dataView.getHeaderGroups().map( ( headerGroup ) => ( - <tr key={ headerGroup.id }> - { headerGroup.headers.map( ( header ) => ( - <th - key={ header.id } - colSpan={ header.colSpan } - style={ { - width: - header.column.columnDef.width || - undefined, - minWidth: - header.column.columnDef - .minWidth || undefined, - maxWidth: - header.column.columnDef - .maxWidth || undefined, - } } - data-field-id={ header.id } - aria-sort={ - sortValues[ - header.column.getIsSorted() - ] - } - > - <HeaderMenu - dataView={ dataView } - header={ header } - /> - </th> - ) ) } - </tr> - ) ) } + <tr> + { visibleFields.map( ( field ) => ( + <th + key={ field.id } + style={ { + width: field.width || undefined, + minWidth: field.minWidth || undefined, + maxWidth: field.maxWidth || undefined, + } } + data-field-id={ field.id } + aria-sort={ + view.sort?.field === field.id && + sortValues[ view.sort.direction ] + } + scope="col" + > + <HeaderMenu + field={ field } + view={ view } + onChangeView={ onChangeView } + /> + </th> + ) ) } + { !! actions?.length && ( + <th data-field-id="actions"> + { __( 'Actions' ) } + </th> + ) } + </tr> </thead> <tbody> - { rows.map( ( row ) => ( - <tr key={ row.id }> - { row.getVisibleCells().map( ( cell ) => ( + { usedData.map( ( item, index ) => ( + <tr key={ getItemId?.( item ) || index }> + { visibleFields.map( ( field ) => ( <td - key={ cell.column.id } + key={ field.id } style={ { - width: - cell.column.columnDef.width || - undefined, + width: field.width || undefined, minWidth: - cell.column.columnDef - .minWidth || undefined, + field.minWidth || undefined, maxWidth: - cell.column.columnDef - .maxWidth || undefined, + field.maxWidth || undefined, } } > - { flexRender( - cell.column.columnDef.cell, - cell.getContext() - ) } + { field.render( { + item, + } ) } </td> ) ) } + { !! actions?.length && ( + <td> + <ItemActions + item={ item } + actions={ actions } + /> + </td> + ) } </tr> ) ) } </tbody> </table> ) } - { ! hasRows && ( + { ! hasData && ( <div className="dataviews-no-results"> <p>{ __( 'No results' ) }</p> </div> From d87092cae0dda2284ffcea05ef768bc097497579 Mon Sep 17 00:00:00 2001 From: Dave Smith <getdavemail@gmail.com> Date: Fri, 8 Dec 2023 08:05:30 +0000 Subject: [PATCH 092/325] Simplify page list edit warning (#56829) * Alter wording * Make button name match intention * Update wording * Use tweaked wording * Copy tweak + remove emphasis * Tweak --------- Co-authored-by: Rich Tabor <hi@richtabor.com> --- .../block-library/src/page-list/convert-to-links-modal.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/page-list/convert-to-links-modal.js b/packages/block-library/src/page-list/convert-to-links-modal.js index cd4049fecff588..f47b5e3de259dd 100644 --- a/packages/block-library/src/page-list/convert-to-links-modal.js +++ b/packages/block-library/src/page-list/convert-to-links-modal.js @@ -5,7 +5,7 @@ import { Button, Modal } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; export const convertDescription = __( - 'This page list is synced with the published pages on your site. Detach the page list to add, delete, or reorder pages yourself.' + "This navigation menu displays your website's pages. Editing it will enable you to add, delete, or reorder pages. However, new pages will no longer be added automatically." ); export function ConvertToLinksModal( { onClick, onClose, disabled } ) { @@ -30,7 +30,7 @@ export function ConvertToLinksModal( { onClick, onClose, disabled } ) { disabled={ disabled } onClick={ onClick } > - { __( 'Detach' ) } + { __( 'Edit' ) } </Button> </div> </Modal> From 11862cf7fe251ebb64993c76aa828f2557e745ed Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 8 Dec 2023 09:32:02 -0500 Subject: [PATCH 093/325] Components: replace `TabPanel` with `Tabs` in the editor's `ColorPanel` (#56878) * replace `TabPanel` with `Tabs` * implement initial tab to track current value * focusable false * defer unlock call --- .../components/global-styles/color-panel.js | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 3b55ec36fc91db..99a5519e9dd008 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -12,12 +12,12 @@ import { __experimentalHStack as HStack, __experimentalZStack as ZStack, __experimentalDropdownContentWrapper as DropdownContentWrapper, - TabPanel, ColorIndicator, Flex, FlexItem, Dropdown, Button, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { useCallback } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -29,6 +29,7 @@ import ColorGradientControl from '../colors-gradients/control'; import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; import { getValueFromVariable } from './utils'; import { setImmutably } from '../../utils/object'; +import { unlock } from '../../lock-unlock'; export function useHasColorPanel( settings ) { const hasTextPanel = useHasTextPanel( settings ); @@ -203,12 +204,11 @@ function ColorPanelDropdown( { colorGradientControlSettings, panelId, } ) { - const tabConfigs = tabs.map( ( { key, label: tabLabel } ) => { - return { - name: key, - title: tabLabel, - }; - } ); + const currentTab = tabs.find( ( tab ) => tab.userValue !== undefined ); + // Unlocking `Tabs` too early causes the `unlock` method to receive an empty + // object, due to circular dependencies. + // See https://github.com/WordPress/gutenberg/issues/52692 + const { Tabs } = unlock( componentsPrivateApis ); return ( <ToolsPanelItem @@ -258,26 +258,35 @@ function ColorPanelDropdown( { /> ) } { tabs.length > 1 && ( - <TabPanel tabs={ tabConfigs }> - { ( tab ) => { - const selectedTab = tabs.find( - ( t ) => t.key === tab.name - ); - - if ( ! selectedTab ) { - return null; - } - + <Tabs initialTabId={ currentTab?.key }> + <Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.Tab + key={ tab.key } + id={ tab.key } + > + { tab.label } + </Tabs.Tab> + ) ) } + </Tabs.TabList> + + { tabs.map( ( tab ) => { return ( - <ColorPanelTab - { ...selectedTab } - colorGradientControlSettings={ - colorGradientControlSettings - } - /> + <Tabs.TabPanel + key={ tab.key } + id={ tab.key } + focusable={ false } + > + <ColorPanelTab + { ...tab } + colorGradientControlSettings={ + colorGradientControlSettings + } + /> + </Tabs.TabPanel> ); - } } - </TabPanel> + } ) } + </Tabs> ) } </div> </DropdownContentWrapper> From 3613e9ee566da85f9a1cafe95e9a9dc235bc63bb Mon Sep 17 00:00:00 2001 From: mimi <mimitips@gmail.com> Date: Fri, 8 Dec 2023 23:49:34 +0900 Subject: [PATCH 094/325] copy/fix capitalization of WordPress (#56834) --- .../fundamentals/javascript-in-the-block-editor.md | 2 +- packages/block-editor/src/components/link-control/test/index.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 73c6a6c56e6328..615f7f74ce151a 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -44,7 +44,7 @@ Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/ho - [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) - [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) -- [Wordpress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) +- [WordPress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) - [Javascript Reference](https://developer.mozilla.org/en-US/docs/Web/JavaScript) | MDN Web Docs - [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository - [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository diff --git a/packages/block-editor/src/components/link-control/test/index.js b/packages/block-editor/src/components/link-control/test/index.js index e0366a3f27ef5f..32db57a55d76ed 100644 --- a/packages/block-editor/src/components/link-control/test/index.js +++ b/packages/block-editor/src/components/link-control/test/index.js @@ -2140,7 +2140,7 @@ describe( 'Post types', () => { describe( 'Rich link previews', () => { const selectedLink = { id: '1', - title: 'Wordpress.org', // Customize this for differentiation in assertions. + title: 'WordPress.org', // Customize this for differentiation in assertions. url: 'https://www.wordpress.org', type: 'URL', }; From cb1bbc79e3abf86d95f9f52b1a57311dbdd3ed0a Mon Sep 17 00:00:00 2001 From: Taylor Gorman <taylor@thegorman.group> Date: Fri, 8 Dec 2023 08:55:23 -0600 Subject: [PATCH 095/325] Link to Dashicons (#56872) --- docs/reference-guides/block-api/block-metadata.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index edc61d138128e6..d023742092df1e 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -197,7 +197,7 @@ The `ancestor` property makes a block available inside the specified block types { "icon": "smile" } ``` -An icon property should be specified to make it easier to identify a block. These can be any of WordPress' Dashicons (slug serving also as a fallback in non-js contexts). +An icon property should be specified to make it easier to identify a block. These can be any of [WordPress' Dashicons](https://developer.wordpress.org/resource/dashicons/) (slug serving also as a fallback in non-js contexts). **Note:** It's also possible to override this property on the client-side with the source of the SVG element. In addition, this property can be defined with JavaScript as an object containing background and foreground colors. This colors will appear with the icon when they are applicable e.g.: in the inserter. Custom SVG icons are automatically wrapped in the [wp.primitives.SVG](/packages/primitives/README.md) component to add accessibility attributes (aria-hidden, role, and focusable). From 46ea0ff0bb96db3b50d06f466cf3af7bc0b21ed5 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 17:00:09 +0200 Subject: [PATCH 096/325] Fix PHP linter failing (#56905) --- .../plugins/interactive-blocks/router-navigate/render.php | 1 + .../plugins/interactive-blocks/router-regions/render.php | 1 + .../e2e-tests/plugins/interactive-blocks/store-tag/render.php | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php index 6dceffa32da8f8..3fbddf623db605 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/render.php @@ -3,6 +3,7 @@ * HTML for testing the router navigate function. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php index 0bcc14ccb266f7..33c319e130fe21 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/render.php @@ -3,6 +3,7 @@ * HTML for testing the hydration of router regions. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ diff --git a/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php b/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php index 57200f295c33b4..06deea9e1169da 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/store-tag/render.php @@ -3,6 +3,7 @@ * HTML for testing the hydration of the serialized store. * * @package gutenberg-test-interactive-blocks + * * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable */ From 78721950a400c13442aa1e42f1207fc129cb6a99 Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Fri, 8 Dec 2023 16:37:30 +0100 Subject: [PATCH 097/325] DropdownMenuV2Ariakit: prevent prefix collapsing if all radios or checkboxes are unselected (#56720) * DropdownMenuV2Ariakit: prevent prefix collapsing if all radios or checkboxes are unselected * CHANGELOG * DRY it up --- packages/components/CHANGELOG.md | 4 ++++ .../src/dropdown-menu-v2-ariakit/styles.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ed33677b55a82a..d7a838344f5cd5 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -18,6 +18,10 @@ - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). - `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). +### Internal + +- `DropdownMenuV2Ariakit`: prevent prefix collapsing if all radios or checkboxes are unselected ([#56720](https://github.com/WordPress/gutenberg/pull/56720)). + ## 25.13.0 (2023-11-29) ### Enhancements diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index 465bdb1aebb30e..eaa249ae86b78c 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -212,6 +212,18 @@ export const ItemPrefixWrapper = styled.span` /* Always occupy the first column, even when auto-collapsing */ grid-column: 1; + /* + * Even when the item is not checked, occupy the same screen space to avoid + * the space collapside when no items are checked. + */ + ${ DropdownMenuCheckboxItem } > &, + ${ DropdownMenuRadioItem } > & { + /* Same width as the check icons */ + min-width: ${ space( 6 ) }; + } + + ${ DropdownMenuCheckboxItem } > &, + ${ DropdownMenuRadioItem } > &, &:not( :empty ) { margin-inline-end: ${ space( 2 ) }; } From 6211c7a61af40c6f0d24c8e140ea81c07eba96fb Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 8 Dec 2023 11:29:51 -0500 Subject: [PATCH 098/325] Tabs: replace `id` with new `tabId` prop (#56883) * replace `id` with `tabId` * update stories and tests * update `ColorGradientControl` implementation * changelog * use `tabId` in tests for consistency * update `ColorPanel` implementation --- .../components/colors-gradients/control.js | 8 +- .../components/global-styles/color-panel.js | 4 +- packages/components/CHANGELOG.md | 4 + packages/components/src/tabs/README.md | 8 +- .../src/tabs/stories/index.story.tsx | 96 +++++++++---------- packages/components/src/tabs/tab.tsx | 6 +- packages/components/src/tabs/tabpanel.tsx | 10 +- packages/components/src/tabs/test/index.tsx | 48 +++++----- packages/components/src/tabs/types.ts | 11 ++- 9 files changed, 104 insertions(+), 91 deletions(-) diff --git a/packages/block-editor/src/components/colors-gradients/control.js b/packages/block-editor/src/components/colors-gradients/control.js index 0cb2fcdda44875..cf82510f78c896 100644 --- a/packages/block-editor/src/components/colors-gradients/control.js +++ b/packages/block-editor/src/components/colors-gradients/control.js @@ -141,15 +141,15 @@ function ColorGradientControlInner( { } > <Tabs.TabList> - <Tabs.Tab id={ TAB_IDS.color }> + <Tabs.Tab tabId={ TAB_IDS.color }> { __( 'Solid' ) } </Tabs.Tab> - <Tabs.Tab id={ TAB_IDS.gradient }> + <Tabs.Tab tabId={ TAB_IDS.gradient }> { __( 'Gradient' ) } </Tabs.Tab> </Tabs.TabList> <Tabs.TabPanel - id={ TAB_IDS.color } + tabId={ TAB_IDS.color } className={ 'block-editor-color-gradient-control__panel' } @@ -158,7 +158,7 @@ function ColorGradientControlInner( { { tabPanels.color } </Tabs.TabPanel> <Tabs.TabPanel - id={ TAB_IDS.gradient } + tabId={ TAB_IDS.gradient } className={ 'block-editor-color-gradient-control__panel' } diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 99a5519e9dd008..469a4080f1e600 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -263,7 +263,7 @@ function ColorPanelDropdown( { { tabs.map( ( tab ) => ( <Tabs.Tab key={ tab.key } - id={ tab.key } + tabId={ tab.key } > { tab.label } </Tabs.Tab> @@ -274,7 +274,7 @@ function ColorPanelDropdown( { return ( <Tabs.TabPanel key={ tab.key } - id={ tab.key } + tabId={ tab.key } focusable={ false } > <ColorPanelTab diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d7a838344f5cd5..ed4b88d000f883 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -22,6 +22,10 @@ - `DropdownMenuV2Ariakit`: prevent prefix collapsing if all radios or checkboxes are unselected ([#56720](https://github.com/WordPress/gutenberg/pull/56720)). +### Experimental + +- `Tabs`: implement new `tabId` prop ([#56883](https://github.com/WordPress/gutenberg/pull/56883)). + ## 25.13.0 (2023-11-29) ### Enhancements diff --git a/packages/components/src/tabs/README.md b/packages/components/src/tabs/README.md index 423216e940584d..732dec9dba7df0 100644 --- a/packages/components/src/tabs/README.md +++ b/packages/components/src/tabs/README.md @@ -163,9 +163,9 @@ The children elements, which should be a series of `Tabs.TabPanel` components. ##### Props -###### `id`: `string` +###### `tabId`: `string` -The id of the tab, which is prepended with the `Tabs` instance ID. +A unique identifier for the tab, which is used to generate a unique id for the underlying element. The value of this prop should match with the value of the `tabId` prop on the corresponding `Tabs.TabPanel` component. - Required: Yes @@ -198,9 +198,9 @@ The children elements, generally the content to display on the tabpanel. - Required: No -###### `id`: `string` +###### `tabId`: `string` -The id of the tabpanel, which is combined with the `Tabs` instance ID and the suffix `-view` +A unique identifier for the tabpanel, which is used to generate an instanced id for the underlying element. The value of this prop should match with the value of the `tabId` prop on the corresponding `Tabs.Tab` component. - Required: Yes diff --git a/packages/components/src/tabs/stories/index.story.tsx b/packages/components/src/tabs/stories/index.story.tsx index ce8c8324edaee4..0e7ab725e371dc 100644 --- a/packages/components/src/tabs/stories/index.story.tsx +++ b/packages/components/src/tabs/stories/index.story.tsx @@ -40,17 +40,17 @@ const Template: StoryFn< typeof Tabs > = ( props ) => { return ( <Tabs { ...props }> <Tabs.TabList> - <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' } focusable={ false }> + <Tabs.TabPanel tabId="tab3" focusable={ false }> <p>Selected tab: Tab 3</p> <p> This tabpanel has its <code>focusable</code> prop set to @@ -71,19 +71,19 @@ const DisabledTabTemplate: StoryFn< typeof Tabs > = ( props ) => { return ( <Tabs { ...props }> <Tabs.TabList> - <Tabs.Tab id={ 'tab1' } disabled={ true }> + <Tabs.Tab tabId="tab1" disabled={ true }> Tab 1 </Tabs.Tab> - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> @@ -96,31 +96,31 @@ const WithTabIconsAndTooltipsTemplate: StoryFn< typeof Tabs > = ( props ) => { <Tabs { ...props }> <Tabs.TabList> <Tabs.Tab - id={ 'tab1' } + tabId="tab1" render={ <Button icon={ wordpress } label="Tab 1" showTooltip /> } /> <Tabs.Tab - id={ 'tab2' } + tabId="tab2" render={ <Button icon={ link } label="Tab 2" showTooltip /> } /> <Tabs.Tab - id={ 'tab3' } + tabId="tab3" render={ <Button icon={ more } label="Tab 3" showTooltip /> } /> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> @@ -140,18 +140,18 @@ const UsingSlotFillTemplate: StoryFn< typeof Tabs > = ( props ) => { <SlotFillProvider> <Tabs { ...props }> <Tabs.TabList> - <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> <Fill name="tabs-are-fun"> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Fill> @@ -196,9 +196,9 @@ const CloseButtonTemplate: StoryFn< typeof Tabs > = ( props ) => { } } > <Tabs.TabList> - <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> <Button variant={ 'tertiary' } @@ -211,13 +211,13 @@ const CloseButtonTemplate: StoryFn< typeof Tabs > = ( props ) => { Close Tabs </Button> </div> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> @@ -251,19 +251,19 @@ const ControlledModeTemplate: StoryFn< typeof Tabs > = ( props ) => { } } > <Tabs.TabList> - <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> + <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> @@ -314,19 +314,19 @@ const TabBecomesDisabledTemplate: StoryFn< typeof Tabs > = ( props ) => { </Button> <Tabs { ...props }> <Tabs.TabList> - <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> - <Tabs.Tab id={ 'tab2' } disabled={ disableTab2 }> + <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> + <Tabs.Tab tabId="tab2" disabled={ disableTab2 }> Tab 2 </Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> @@ -348,17 +348,17 @@ const TabGetsRemovedTemplate: StoryFn< typeof Tabs > = ( props ) => { </Button> <Tabs { ...props }> <Tabs.TabList> - { ! removeTab1 && <Tabs.Tab id={ 'tab1' }>Tab 1</Tabs.Tab> } - <Tabs.Tab id={ 'tab2' }>Tab 2</Tabs.Tab> - <Tabs.Tab id={ 'tab3' }>Tab 3</Tabs.Tab> + { ! removeTab1 && <Tabs.Tab tabId="tab1">Tab 1</Tabs.Tab> } + <Tabs.Tab tabId="tab2">Tab 2</Tabs.Tab> + <Tabs.Tab tabId="tab3">Tab 3</Tabs.Tab> </Tabs.TabList> - <Tabs.TabPanel id={ 'tab1' }> + <Tabs.TabPanel tabId="tab1"> <p>Selected tab: Tab 1</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab2' }> + <Tabs.TabPanel tabId="tab2"> <p>Selected tab: Tab 2</p> </Tabs.TabPanel> - <Tabs.TabPanel id={ 'tab3' }> + <Tabs.TabPanel tabId="tab3"> <p>Selected tab: Tab 3</p> </Tabs.TabPanel> </Tabs> diff --git a/packages/components/src/tabs/tab.tsx b/packages/components/src/tabs/tab.tsx index 4bfc99e8ef43b1..e1aa85c636cdd1 100644 --- a/packages/components/src/tabs/tab.tsx +++ b/packages/components/src/tabs/tab.tsx @@ -15,15 +15,15 @@ import type { WordPressComponentProps } from '../context'; export const Tab = forwardRef< HTMLButtonElement, - WordPressComponentProps< TabProps, 'button', false > ->( function Tab( { children, id, disabled, render, ...otherProps }, ref ) { + Omit< WordPressComponentProps< TabProps, 'button', false >, 'id' > +>( function Tab( { children, tabId, disabled, render, ...otherProps }, ref ) { const context = useTabsContext(); if ( ! context ) { warning( '`Tabs.Tab` must be wrapped in a `Tabs` component.' ); return null; } const { store, instanceId } = context; - const instancedTabId = `${ instanceId }-${ id }`; + const instancedTabId = `${ instanceId }-${ tabId }`; return ( <StyledTab ref={ ref } diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index 8e8d72280a4935..14c449bf41d135 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -20,20 +20,24 @@ import type { WordPressComponentProps } from '../context'; export const TabPanel = forwardRef< HTMLDivElement, - WordPressComponentProps< TabPanelProps, 'div', false > ->( function TabPanel( { children, id, focusable = true, ...otherProps }, ref ) { + Omit< WordPressComponentProps< TabPanelProps, 'div', false >, 'id' > +>( function TabPanel( + { children, tabId, focusable = true, ...otherProps }, + ref +) { const context = useTabsContext(); if ( ! context ) { warning( '`Tabs.TabPanel` must be wrapped in a `Tabs` component.' ); return null; } const { store, instanceId } = context; + const instancedTabId = `${ instanceId }-${ tabId }`; return ( <StyledTabPanel ref={ ref } store={ store } - id={ `${ instanceId }-${ id }-view` } + id={ instancedTabId } focusable={ focusable } { ...otherProps } > diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index f923dc455fd7bd..7e2d4671224858 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -16,7 +16,7 @@ import Tabs from '..'; import type { TabsProps } from '../types'; type Tab = { - id: string; + tabId: string; title: string; content: React.ReactNode; tab: { @@ -30,19 +30,19 @@ type Tab = { const TABS: Tab[] = [ { - id: 'alpha', + tabId: 'alpha', title: 'Alpha', content: 'Selected tab: Alpha', tab: { className: 'alpha-class' }, }, { - id: 'beta', + tabId: 'beta', title: 'Beta', content: 'Selected tab: Beta', tab: { className: 'beta-class' }, }, { - id: 'gamma', + tabId: 'gamma', title: 'Gamma', content: 'Selected tab: Gamma', tab: { className: 'gamma-class' }, @@ -52,7 +52,7 @@ const TABS: Tab[] = [ const TABS_WITH_DELTA: Tab[] = [ ...TABS, { - id: 'delta', + tabId: 'delta', title: 'Delta', content: 'Selected tab: Delta', tab: { className: 'delta-class' }, @@ -70,8 +70,8 @@ const UncontrolledTabs = ( { <Tabs.TabList> { tabs.map( ( tabObj ) => ( <Tabs.Tab - key={ tabObj.id } - id={ tabObj.id } + key={ tabObj.tabId } + tabId={ tabObj.tabId } className={ tabObj.tab.className } disabled={ tabObj.tab.disabled } > @@ -81,8 +81,8 @@ const UncontrolledTabs = ( { </Tabs.TabList> { tabs.map( ( tabObj ) => ( <Tabs.TabPanel - key={ tabObj.id } - id={ tabObj.id } + key={ tabObj.tabId } + tabId={ tabObj.tabId } focusable={ tabObj.tabpanel?.focusable } > { tabObj.content } @@ -114,8 +114,8 @@ const ControlledTabs = ( { <Tabs.TabList> { tabs.map( ( tabObj ) => ( <Tabs.Tab - key={ tabObj.id } - id={ tabObj.id } + key={ tabObj.tabId } + tabId={ tabObj.tabId } className={ tabObj.tab.className } disabled={ tabObj.tab.disabled } > @@ -124,7 +124,7 @@ const ControlledTabs = ( { ) ) } </Tabs.TabList> { tabs.map( ( tabObj ) => ( - <Tabs.TabPanel key={ tabObj.id } id={ tabObj.id }> + <Tabs.TabPanel key={ tabObj.tabId } tabId={ tabObj.tabId }> { tabObj.content } </Tabs.TabPanel> ) ) } @@ -201,7 +201,7 @@ describe( 'Tabs', () => { } ); it( 'should not focus on the related TabPanel when pressing the Tab key if `focusable: false` is set', async () => { const TABS_WITH_ALPHA_FOCUSABLE_FALSE = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, content: ( @@ -442,7 +442,7 @@ describe( 'Tabs', () => { const mockOnSelect = jest.fn(); const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'delta' + tabObj.tabId === 'delta' ? { ...tabObj, tab: { @@ -604,7 +604,7 @@ describe( 'Tabs', () => { } ); it( 'should not load any tab if the active tab is removed and there are no enabled tabs', async () => { const TABS_WITH_BETA_GAMMA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id !== 'alpha' + tabObj.tabId !== 'alpha' ? { ...tabObj, tab: { @@ -726,7 +726,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -801,7 +801,7 @@ describe( 'Tabs', () => { const TABS_WITH_DELTA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'delta' + tabObj.tabId === 'delta' ? { ...tabObj, tab: { @@ -849,7 +849,7 @@ describe( 'Tabs', () => { it( 'should select first enabled tab when the initial tab is disabled', async () => { const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -878,7 +878,7 @@ describe( 'Tabs', () => { it( 'should select first enabled tab when the tab associated to `initialTabId` is disabled', async () => { const TABS_ONLY_GAMMA_ENABLED = TABS.map( ( tabObj ) => - tabObj.id !== 'gamma' + tabObj.tabId !== 'gamma' ? { ...tabObj, tab: { @@ -920,7 +920,7 @@ describe( 'Tabs', () => { expect( mockOnSelect ).toHaveBeenLastCalledWith( 'alpha' ); const TABS_WITH_ALPHA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'alpha' + tabObj.tabId === 'alpha' ? { ...tabObj, tab: { @@ -967,7 +967,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); const TABS_WITH_GAMMA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'gamma' + tabObj.tabId === 'gamma' ? { ...tabObj, tab: { @@ -1051,7 +1051,7 @@ describe( 'Tabs', () => { // Remove beta rerender( <ControlledTabs - tabs={ TABS.filter( ( tab ) => tab.id !== 'beta' ) } + tabs={ TABS.filter( ( tab ) => tab.tabId !== 'beta' ) } selectedTabId="beta" /> ); @@ -1085,7 +1085,7 @@ describe( 'Tabs', () => { it( 'should not render any tab if `selectedTabId` refers to a disabled tab', async () => { const TABS_WITH_DELTA_WITH_BETA_DISABLED = TABS_WITH_DELTA.map( ( tabObj ) => - tabObj.id === 'beta' + tabObj.tabId === 'beta' ? { ...tabObj, tab: { @@ -1122,7 +1122,7 @@ describe( 'Tabs', () => { expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); const TABS_WITH_BETA_DISABLED = TABS.map( ( tabObj ) => - tabObj.id === 'beta' + tabObj.tabId === 'beta' ? { ...tabObj, tab: { diff --git a/packages/components/src/tabs/types.ts b/packages/components/src/tabs/types.ts index 8b071937410919..389665b13357fe 100644 --- a/packages/components/src/tabs/types.ts +++ b/packages/components/src/tabs/types.ts @@ -78,8 +78,10 @@ export type TabListProps = { export type TabProps = { /** * The id of the tab, which is prepended with the `Tabs` instanceId. + * The value of this prop should match with the value of the `tabId` prop on + * the corresponding `Tabs.TabPanel` component. */ - id: string; + tabId: string; /** * The children elements, generally the text to display on the tab. */ @@ -103,9 +105,12 @@ export type TabPanelProps = { */ children?: React.ReactNode; /** - * A unique identifier for the tabpanel, which is used to generate a unique `id` for the underlying element. + * A unique identifier for the tabpanel, which is used to generate an + * instanced id for the underlying element. + * The value of this prop should match with the value of the `tabId` prop on + * the corresponding `Tabs.Tab` component. */ - id: string; + tabId: string; /** * Determines whether or not the tabpanel element should be focusable. * If `false`, pressing the tab key will skip over the tabpanel, and instead From cfa3fd6178706eb52bf4043f1be7af030bdcc660 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Fri, 8 Dec 2023 12:16:49 -0500 Subject: [PATCH 099/325] fix: Guard against a block styles crash due to null block values (#56903) * fix: Guard against a block styles crash due to null block values In certain scenarios, e.g., when the editor hangs due to poor performance, the `getBlock` value unexpectedly returns `null`. To guard against a crash in this scenario, we now conditionally access attributes on the block. A ideal resolution would be improving editor performance to avoid the scenario where a race condition causes a crash, but the fact remains that `getBlock` _can_ return `null` values and that we should not presume that it will not. * docs: Add change log entry --- .../src/components/block-styles/index.native.js | 6 ++++-- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/block-styles/index.native.js b/packages/block-editor/src/components/block-styles/index.native.js index 7967e79c2c1cd8..40e29476287ffb 100644 --- a/packages/block-editor/src/components/block-styles/index.native.js +++ b/packages/block-editor/src/components/block-styles/index.native.js @@ -19,14 +19,16 @@ import StylePreview from './preview'; import containerStyles from './style.scss'; import { store as blockEditorStore } from '../../store'; +const EMPTY_ARRAY = []; + function BlockStyles( { clientId, url } ) { const selector = ( select ) => { const { getBlock } = select( blockEditorStore ); const { getBlockStyles } = select( blocksStore ); const block = getBlock( clientId ); return { - styles: getBlockStyles( block.name ), - className: block.attributes.className || '', + styles: getBlockStyles( block?.name ) || EMPTY_ARRAY, + className: block?.attributes?.className || '', }; }; diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 992d61ea9ce37b..791b39fee2c7f8 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] [internal] Move InserterButton from components package to block-editor package [#56494] - [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] +- [*] Guard against an Image block styles crash due to null block values [#56903] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] From 91cb8fee759eef27e6e2f2c93c2a36ba29eb4082 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 8 Dec 2023 20:08:24 +0200 Subject: [PATCH 100/325] Block editor: hooks: manage BlockListBlock filters in one place (#56875) --- packages/block-editor/src/hooks/align.js | 41 +---- packages/block-editor/src/hooks/border.js | 117 +++++------- packages/block-editor/src/hooks/color.js | 120 +++++------- packages/block-editor/src/hooks/duotone.js | 149 ++++++--------- packages/block-editor/src/hooks/font-size.js | 89 +++------ packages/block-editor/src/hooks/index.js | 18 +- .../block-editor/src/hooks/layout-child.js | 53 ++++++ packages/block-editor/src/hooks/layout.js | 62 ------- packages/block-editor/src/hooks/position.js | 86 ++++----- packages/block-editor/src/hooks/style.js | 174 +++++++----------- packages/block-editor/src/hooks/test/align.js | 109 +---------- packages/block-editor/src/hooks/test/color.js | 112 ----------- packages/block-editor/src/hooks/utils.js | 107 ++++++++++- 13 files changed, 466 insertions(+), 771 deletions(-) create mode 100644 packages/block-editor/src/hooks/layout-child.js delete mode 100644 packages/block-editor/src/hooks/test/color.js diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 2019228cf2d3e8..189f82ccf429f8 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -6,7 +6,6 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { createHigherOrderComponent } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport, @@ -155,54 +154,27 @@ function BlockEditAlignmentToolbarControlsPure( { export default { shareWithChildBlocks: true, edit: BlockEditAlignmentToolbarControlsPure, + useBlockProps, attributeKeys: [ 'align' ], hasSupport( name ) { return hasBlockSupport( name, 'align', false ); }, }; -function BlockListBlockWithDataAlign( { block: BlockListBlock, props } ) { - const { name, attributes } = props; - const { align } = attributes; +function useBlockProps( { name, align } ) { const blockAllowedAlignments = getValidAlignments( getBlockSupport( name, 'align' ), hasBlockSupport( name, 'alignWide', true ) ); const validAlignments = useAvailableAlignments( blockAllowedAlignments ); - let wrapperProps = props.wrapperProps; if ( validAlignments.some( ( alignment ) => alignment.name === align ) ) { - wrapperProps = { ...wrapperProps, 'data-align': align }; + return { 'data-align': align }; } - return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />; + return {}; } -/** - * Override the default block element to add alignment wrapper props. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withDataAlign = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - // If an alignment is not assigned, there's no need to go through the - // effort to validate or assign its value. - if ( props.attributes.align === undefined ) { - return <BlockListBlock { ...props } />; - } - - return ( - <BlockListBlockWithDataAlign - block={ BlockListBlock } - props={ props } - /> - ); - }, - 'withDataAlign' -); - /** * Override props assigned to save component to inject alignment class name if * block supports it. @@ -237,11 +209,6 @@ addFilter( 'core/editor/align/addAttribute', addAttribute ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/align/with-data-align', - withDataAlign -); addFilter( 'blocks.getSaveContent.extraProps', 'core/editor/align/addAssignedAlign', diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index d8905b29a29617..6ac4dd2360fb08 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -8,7 +8,7 @@ import classnames from 'classnames'; */ import { getBlockSupport } from '@wordpress/blocks'; import { __experimentalHasSplitBorders as hasSplitBorders } from '@wordpress/components'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { pure } from '@wordpress/compose'; import { Platform, useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { useSelect } from '@wordpress/data'; @@ -330,72 +330,55 @@ function addEditProps( settings ) { return settings; } -/** - * This adds inline styles for color palette colors. - * Ideally, this is not needed and themes should load their palettes on the editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withBorderColorPaletteStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const { borderColor, style } = attributes; - const { colors } = useMultipleOriginColorsAndGradients(); - - if ( - ! hasBorderSupport( name, 'color' ) || - shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) - ) { - return <BlockListBlock { ...props } />; - } +function useBlockProps( { name, borderColor, style } ) { + const { colors } = useMultipleOriginColorsAndGradients(); - const { color: borderColorValue } = getMultiOriginColor( { - colors, - namedColor: borderColor, - } ); - const { color: borderTopColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.top?.color ), - } ); - const { color: borderRightColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.right?.color ), - } ); - - const { color: borderBottomColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( - style?.border?.bottom?.color - ), - } ); - const { color: borderLeftColor } = getMultiOriginColor( { - colors, - namedColor: getColorSlugFromVariable( style?.border?.left?.color ), - } ); - - const extraStyles = { - borderTopColor: borderTopColor || borderColorValue, - borderRightColor: borderRightColor || borderColorValue, - borderBottomColor: borderBottomColor || borderColorValue, - borderLeftColor: borderLeftColor || borderColorValue, - }; - const cleanedExtraStyles = cleanEmptyObject( extraStyles ) || {}; - - let wrapperProps = props.wrapperProps; - wrapperProps = { - ...props.wrapperProps, - style: { - ...props.wrapperProps?.style, - ...cleanedExtraStyles, - }, - }; + if ( + ! hasBorderSupport( name, 'color' ) || + shouldSkipSerialization( name, BORDER_SUPPORT_KEY, 'color' ) + ) { + return {}; + } - return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />; + const { color: borderColorValue } = getMultiOriginColor( { + colors, + namedColor: borderColor, + } ); + const { color: borderTopColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.top?.color ), + } ); + const { color: borderRightColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.right?.color ), + } ); + + const { color: borderBottomColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.bottom?.color ), + } ); + const { color: borderLeftColor } = getMultiOriginColor( { + colors, + namedColor: getColorSlugFromVariable( style?.border?.left?.color ), + } ); + + const extraStyles = { + borderTopColor: borderTopColor || borderColorValue, + borderRightColor: borderRightColor || borderColorValue, + borderBottomColor: borderBottomColor || borderColorValue, + borderLeftColor: borderLeftColor || borderColorValue, + }; + + return { style: cleanEmptyObject( extraStyles ) || {} }; +} + +export default { + useBlockProps, + attributeKeys: [ 'borderColor', 'style' ], + hasSupport( name ) { + return hasBorderSupport( name, 'color' ); }, - 'withBorderColorPaletteStyles' -); +}; addFilter( 'blocks.registerBlockType', @@ -414,9 +397,3 @@ addFilter( 'core/border/addEditProps', addEditProps ); - -addFilter( - 'editor.BlockListBlock', - 'core/border/with-border-color-palette-styles', - withBorderColorPaletteStyles -); diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index d5cb21e5dcf9a2..f259ff9c9c0865 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -9,7 +9,7 @@ import classnames from 'classnames'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, Platform, useCallback } from '@wordpress/element'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; +import { pure } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; /** @@ -364,72 +364,56 @@ function ColorEditPure( { clientId, name, setAttributes, settings } ) { // and not the whole attributes object. export const ColorEdit = pure( ColorEditPure ); -/** - * This adds inline styles for color palette colors. - * Ideally, this is not needed and themes should load their palettes on the editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withColorPaletteStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const { backgroundColor, textColor } = attributes; - const [ userPalette, themePalette, defaultPalette ] = useSettings( - 'color.palette.custom', - 'color.palette.theme', - 'color.palette.default' - ); - - const colors = useMemo( - () => [ - ...( userPalette || [] ), - ...( themePalette || [] ), - ...( defaultPalette || [] ), - ], - [ userPalette, themePalette, defaultPalette ] - ); - if ( - ! hasColorSupport( name ) || - shouldSkipSerialization( name, COLOR_SUPPORT_KEY ) - ) { - return <BlockListBlock { ...props } />; - } - const extraStyles = {}; - - if ( - textColor && - ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'text' ) - ) { - extraStyles.color = getColorObjectByAttributeValues( - colors, - textColor - )?.color; - } - if ( - backgroundColor && - ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'background' ) - ) { - extraStyles.backgroundColor = getColorObjectByAttributeValues( - colors, - backgroundColor - )?.color; - } +function useBlockProps( { name, backgroundColor, textColor } ) { + const [ userPalette, themePalette, defaultPalette ] = useSettings( + 'color.palette.custom', + 'color.palette.theme', + 'color.palette.default' + ); - let wrapperProps = props.wrapperProps; - wrapperProps = { - ...props.wrapperProps, - style: { - ...extraStyles, - ...props.wrapperProps?.style, - }, - }; + const colors = useMemo( + () => [ + ...( userPalette || [] ), + ...( themePalette || [] ), + ...( defaultPalette || [] ), + ], + [ userPalette, themePalette, defaultPalette ] + ); + if ( + ! hasColorSupport( name ) || + shouldSkipSerialization( name, COLOR_SUPPORT_KEY ) + ) { + return {}; + } + const extraStyles = {}; - return <BlockListBlock { ...props } wrapperProps={ wrapperProps } />; - }, - 'withColorPaletteStyles' -); + if ( + textColor && + ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'text' ) + ) { + extraStyles.color = getColorObjectByAttributeValues( + colors, + textColor + )?.color; + } + if ( + backgroundColor && + ! shouldSkipSerialization( name, COLOR_SUPPORT_KEY, 'background' ) + ) { + extraStyles.backgroundColor = getColorObjectByAttributeValues( + colors, + backgroundColor + )?.color; + } + + return { style: extraStyles }; +} + +export default { + useBlockProps, + attributeKeys: [ 'backgroundColor', 'textColor' ], + hasSupport: hasColorSupport, +}; const MIGRATION_PATHS = { linkColor: [ [ 'style', 'elements', 'link', 'color', 'text' ] ], @@ -477,12 +461,6 @@ addFilter( addEditProps ); -addFilter( - 'editor.BlockListBlock', - 'core/color/with-color-palette-styles', - withColorPaletteStyles -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/color/addTransforms', diff --git a/packages/block-editor/src/hooks/duotone.js b/packages/block-editor/src/hooks/duotone.js index c0b76d12cb3707..0df0d50d644575 100644 --- a/packages/block-editor/src/hooks/duotone.js +++ b/packages/block-editor/src/hooks/duotone.js @@ -1,7 +1,6 @@ /** * External dependencies */ -import classnames from 'classnames'; import { extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; @@ -13,7 +12,7 @@ import { getBlockType, hasBlockSupport, } from '@wordpress/blocks'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; import { useMemo, useEffect } from '@wordpress/element'; @@ -178,6 +177,7 @@ function DuotonePanelPure( { style, setAttributes, name } ) { export default { shareWithChildBlocks: true, edit: DuotonePanelPure, + useBlockProps, attributeKeys: [ 'style' ], hasSupport( name ) { return hasBlockSupport( name, 'filter.duotone' ); @@ -212,7 +212,7 @@ function addDuotoneAttributes( settings ) { return settings; } -function DuotoneStyles( { +function useDuotoneStyles( { clientId, id: filterId, selector: duotoneSelector, @@ -310,98 +310,69 @@ function DuotoneStyles( { blockElement.style.display = display; } }, [ isValidFilter, blockElement ] ); - - return null; } -/** - * Override the default block element to include duotone styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -const withDuotoneStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const id = useInstanceId( BlockListBlock ); - - const selector = useMemo( () => { - const blockType = getBlockType( props.name ); - - if ( blockType ) { - // Backwards compatibility for `supports.color.__experimentalDuotone` - // is provided via the `block_type_metadata_settings` filter. If - // `supports.filter.duotone` has not been set and the - // experimental property has been, the experimental property - // value is copied into `supports.filter.duotone`. - const duotoneSupport = getBlockSupport( - blockType, - 'filter.duotone', - false - ); - if ( ! duotoneSupport ) { - return null; - } - - // If the experimental duotone support was set, that value is - // to be treated as a selector and requires scoping. - const experimentalDuotone = getBlockSupport( - blockType, - 'color.__experimentalDuotone', - false - ); - if ( experimentalDuotone ) { - const rootSelector = getBlockCSSSelector( blockType ); - return typeof experimentalDuotone === 'string' - ? scopeSelector( rootSelector, experimentalDuotone ) - : rootSelector; - } - - // Regular filter.duotone support uses filter.duotone selectors with fallbacks. - return getBlockCSSSelector( blockType, 'filter.duotone', { - fallback: true, - } ); +function useBlockProps( { name, style } ) { + const id = useInstanceId( useBlockProps ); + const selector = useMemo( () => { + const blockType = getBlockType( name ); + + if ( blockType ) { + // Backwards compatibility for `supports.color.__experimentalDuotone` + // is provided via the `block_type_metadata_settings` filter. If + // `supports.filter.duotone` has not been set and the + // experimental property has been, the experimental property + // value is copied into `supports.filter.duotone`. + const duotoneSupport = getBlockSupport( + blockType, + 'filter.duotone', + false + ); + if ( ! duotoneSupport ) { + return null; } - }, [ props.name ] ); - - const attribute = props?.attributes?.style?.color?.duotone; - - const filterClass = `wp-duotone-${ id }`; - - const shouldRender = selector && attribute; - - const className = shouldRender - ? classnames( props?.className, filterClass ) - : props?.className; - - // CAUTION: code added before this line will be executed - // for all blocks, not just those that support duotone. Code added - // above this line should be carefully evaluated for its impact on - // performance. - return ( - <> - { shouldRender && ( - <DuotoneStyles - clientId={ props.clientId } - id={ filterClass } - selector={ selector } - attribute={ attribute } - /> - ) } - <BlockListBlock { ...props } className={ className } /> - </> - ); - }, - 'withDuotoneStyles' -); + + // If the experimental duotone support was set, that value is + // to be treated as a selector and requires scoping. + const experimentalDuotone = getBlockSupport( + blockType, + 'color.__experimentalDuotone', + false + ); + if ( experimentalDuotone ) { + const rootSelector = getBlockCSSSelector( blockType ); + return typeof experimentalDuotone === 'string' + ? scopeSelector( rootSelector, experimentalDuotone ) + : rootSelector; + } + + // Regular filter.duotone support uses filter.duotone selectors with fallbacks. + return getBlockCSSSelector( blockType, 'filter.duotone', { + fallback: true, + } ); + } + }, [ name ] ); + + const attribute = style?.color?.duotone; + + const filterClass = `wp-duotone-${ id }`; + + const shouldRender = selector && attribute; + + useDuotoneStyles( { + clientId: id, + id: filterClass, + selector, + attribute, + } ); + + return { + className: shouldRender ? filterClass : '', + }; +} addFilter( 'blocks.registerBlockType', 'core/editor/duotone/add-attributes', addDuotoneAttributes ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/duotone/with-styles', - withDuotoneStyles -); diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index 146abe1d1f72f6..a7ef79f0d4f2bf 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -4,7 +4,6 @@ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; import TokenList from '@wordpress/token-list'; -import { createHigherOrderComponent } from '@wordpress/compose'; import { select } from '@wordpress/data'; /** @@ -175,62 +174,38 @@ export function useIsFontSizeDisabled( { name: blockName } = {} ) { ); } -/** - * Add inline styles for font sizes. - * Ideally, this is not needed and themes load the font-size classes on the - * editor. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -const withFontSizeInlineStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const [ fontSizes ] = useSettings( 'typography.fontSizes' ); - const { - name: blockName, - attributes: { fontSize, style }, - wrapperProps, - } = props; - - // Only add inline styles if the block supports font sizes, - // doesn't skip serialization of font sizes, - // doesn't already have an inline font size, - // and does have a class to extract the font size from. - if ( - ! hasBlockSupport( blockName, FONT_SIZE_SUPPORT_KEY ) || - shouldSkipSerialization( - blockName, - TYPOGRAPHY_SUPPORT_KEY, - 'fontSize' - ) || - ! fontSize || - style?.typography?.fontSize - ) { - return <BlockListBlock { ...props } />; - } +function useBlockProps( { name, fontSize, style } ) { + const [ fontSizes ] = useSettings( 'typography.fontSizes' ); - const fontSizeValue = getFontSize( - fontSizes, - fontSize, - style?.typography?.fontSize - ).size; - - const newProps = { - ...props, - wrapperProps: { - ...wrapperProps, - style: { - fontSize: fontSizeValue, - ...wrapperProps?.style, - }, - }, - }; + // Only add inline styles if the block supports font sizes, + // doesn't skip serialization of font sizes, + // doesn't already have an inline font size, + // and does have a class to extract the font size from. + if ( + ! hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ) || + shouldSkipSerialization( name, TYPOGRAPHY_SUPPORT_KEY, 'fontSize' ) || + ! fontSize || + style?.typography?.fontSize + ) { + return; + } + + const fontSizeValue = getFontSize( + fontSizes, + fontSize, + style?.typography?.fontSize + ).size; + + return { style: { fontSize: fontSizeValue } }; +} - return <BlockListBlock { ...newProps } />; +export default { + useBlockProps, + attributeKeys: [ 'fontSize', 'style' ], + hasSupport( name ) { + return hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ); }, - 'withFontSizeInlineStyles' -); +}; const MIGRATION_PATHS = { fontSize: [ [ 'fontSize' ], [ 'style', 'typography', 'fontSize' ] ], @@ -332,12 +307,6 @@ addFilter( addFilter( 'blocks.registerBlockType', 'core/font/addEditProps', addEditProps ); -addFilter( - 'editor.BlockListBlock', - 'core/font-size/with-font-size-inline-styles', - withFontSizeInlineStyles -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/font-size/addTransforms', diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 6ae589dd672bf1..506f2a50a83a73 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,7 +1,7 @@ /** * Internal dependencies */ -import { createBlockEditFilter } from './utils'; +import { createBlockEditFilter, createBlockListBlockFilter } from './utils'; import './compat'; import align from './align'; import './lock'; @@ -11,13 +11,14 @@ import customClassName from './custom-class-name'; import './generated-class-name'; import style from './style'; import './settings'; -import './color'; +import color from './color'; import duotone from './duotone'; import './font-family'; -import './font-size'; -import './border'; +import fontSize from './font-size'; +import border from './border'; import position from './position'; import layout from './layout'; +import childLayout from './layout-child'; import './content-lock-ui'; import './metadata'; import customFields from './custom-fields'; @@ -38,6 +39,15 @@ createBlockEditFilter( blockRenaming, ].filter( Boolean ) ); +createBlockListBlockFilter( [ + align, + color, + duotone, + fontSize, + border, + position, + childLayout, +] ); export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/layout-child.js b/packages/block-editor/src/hooks/layout-child.js new file mode 100644 index 00000000000000..58a75a568c40d0 --- /dev/null +++ b/packages/block-editor/src/hooks/layout-child.js @@ -0,0 +1,53 @@ +/** + * WordPress dependencies + */ +import { useInstanceId } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { useStyleOverride } from './utils'; + +function useBlockPropsChildLayoutStyles( { style } ) { + const shouldRenderChildLayoutStyles = useSelect( ( select ) => { + return ! select( blockEditorStore ).getSettings().disableLayoutStyles; + } ); + const layout = style?.layout ?? {}; + const { selfStretch, flexSize } = layout; + const id = useInstanceId( useBlockPropsChildLayoutStyles ); + const selector = `.wp-container-content-${ id }`; + + let css = ''; + if ( shouldRenderChildLayoutStyles ) { + if ( selfStretch === 'fixed' && flexSize ) { + css = `${ selector } { + flex-basis: ${ flexSize }; + box-sizing: border-box; + }`; + } else if ( selfStretch === 'fill' ) { + css = `${ selector } { + flex-grow: 1; + }`; + } + } + + useStyleOverride( { css } ); + + // Only attach a container class if there is generated CSS to be attached. + if ( ! css ) { + return; + } + + // Attach a `wp-container-content` id-based classname. + return { className: `wp-container-content-${ id }` }; +} + +export default { + useBlockProps: useBlockPropsChildLayoutStyles, + attributeKeys: [ 'style' ], + hasSupport() { + return true; + }, +}; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 46239e1de07037..18bb46a87a1b87 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -417,63 +417,6 @@ export const withLayoutStyles = createHigherOrderComponent( 'withLayoutStyles' ); -function BlockWithChildLayoutStyles( { block: BlockListBlock, props } ) { - const layout = props.attributes.style?.layout ?? {}; - const { selfStretch, flexSize } = layout; - - const id = useInstanceId( BlockListBlock ); - const selector = `.wp-container-content-${ id }`; - - let css = ''; - if ( selfStretch === 'fixed' && flexSize ) { - css = `${ selector } { - flex-basis: ${ flexSize }; - box-sizing: border-box; - }`; - } else if ( selfStretch === 'fill' ) { - css = `${ selector } { - flex-grow: 1; - }`; - } - - // Attach a `wp-container-content` id-based classname. - const className = classnames( props.className, { - [ `wp-container-content-${ id }` ]: !! css, // Only attach a container class if there is generated CSS to be attached. - } ); - - useStyleOverride( { css } ); - - return <BlockListBlock { ...props } className={ className } />; -} - -/** - * Override the default block element to add the child layout styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withChildLayoutStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const shouldRenderChildLayoutStyles = useSelect( ( select ) => { - return ! select( blockEditorStore ).getSettings() - .disableLayoutStyles; - } ); - - if ( ! shouldRenderChildLayoutStyles ) { - return <BlockListBlock { ...props } />; - } - - return ( - <BlockWithChildLayoutStyles - block={ BlockListBlock } - props={ props } - /> - ); - }, - 'withChildLayoutStyles' -); - addFilter( 'blocks.registerBlockType', 'core/layout/addAttribute', @@ -484,8 +427,3 @@ addFilter( 'core/editor/layout/with-layout-styles', withLayoutStyles ); -addFilter( - 'editor.BlockListBlock', - 'core/editor/layout/with-child-layout-styles', - withChildLayoutStyles -); diff --git a/packages/block-editor/src/hooks/position.js b/packages/block-editor/src/hooks/position.js index cdeb90822f0ac4..5017cb34fc18bd 100644 --- a/packages/block-editor/src/hooks/position.js +++ b/packages/block-editor/src/hooks/position.js @@ -12,10 +12,9 @@ import { BaseControl, privateApis as componentsPrivateApis, } from '@wordpress/components'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; import { useMemo, Platform } from '@wordpress/element'; -import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies @@ -322,63 +321,44 @@ export default { } return <PositionPanelPure { ...props } />; }, + useBlockProps, attributeKeys: [ 'style' ], hasSupport( name ) { return hasBlockSupport( name, POSITION_SUPPORT_KEY ); }, }; -/** - * Override the default block element to add the position styles. - * - * @param {Function} BlockListBlock Original component. - * - * @return {Function} Wrapped component. - */ -export const withPositionStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const { name, attributes } = props; - const hasPositionBlockSupport = hasBlockSupport( - name, - POSITION_SUPPORT_KEY - ); - const isPositionDisabled = useIsPositionDisabled( props ); - const allowPositionStyles = - hasPositionBlockSupport && ! isPositionDisabled; - - const id = useInstanceId( BlockListBlock ); - - // Higher specificity to override defaults in editor UI. - const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; - - // Get CSS string for the current position values. - let css; - if ( allowPositionStyles ) { - css = - getPositionCSS( { - selector: positionSelector, - style: attributes?.style, - } ) || ''; - } +function useBlockProps( { name, style } ) { + const hasPositionBlockSupport = hasBlockSupport( + name, + POSITION_SUPPORT_KEY + ); + const isPositionDisabled = useIsPositionDisabled( { name } ); + const allowPositionStyles = hasPositionBlockSupport && ! isPositionDisabled; + + const id = useInstanceId( useBlockProps ); + + // Higher specificity to override defaults in editor UI. + const positionSelector = `.wp-container-${ id }.wp-container-${ id }`; + + // Get CSS string for the current position values. + let css; + if ( allowPositionStyles ) { + css = + getPositionCSS( { + selector: positionSelector, + style, + } ) || ''; + } - // Attach a `wp-container-` id-based class name. - const className = classnames( props?.className, { - [ `wp-container-${ id }` ]: allowPositionStyles && !! css, // Only attach a container class if there is generated CSS to be attached. - [ `is-position-${ attributes?.style?.position?.type }` ]: - allowPositionStyles && - !! css && - !! attributes?.style?.position?.type, - } ); + // Attach a `wp-container-` id-based class name. + const className = classnames( { + [ `wp-container-${ id }` ]: allowPositionStyles && !! css, // Only attach a container class if there is generated CSS to be attached. + [ `is-position-${ style?.position?.type }` ]: + allowPositionStyles && !! css && !! style?.position?.type, + } ); - useStyleOverride( { css } ); + useStyleOverride( { css } ); - return <BlockListBlock { ...props } className={ className } />; - }, - 'withPositionStyles' -); - -addFilter( - 'editor.BlockListBlock', - 'core/editor/position/with-position-styles', - withPositionStyles -); + return { className }; +} diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 40363423168874..935e8260fa89f6 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ @@ -13,7 +8,7 @@ import { hasBlockSupport, __EXPERIMENTAL_ELEMENTS as ELEMENTS, } from '@wordpress/blocks'; -import { createHigherOrderComponent, useInstanceId } from '@wordpress/compose'; +import { useInstanceId } from '@wordpress/compose'; import { getCSSRules, compileCSS } from '@wordpress/style-engine'; /** @@ -379,6 +374,8 @@ function BlockStyleControls( { export default { edit: BlockStyleControls, hasSupport: hasStyleSupport, + attributeKeys: [ 'style' ], + useBlockProps, }; // Defines which element types are supported, including their hover styles or @@ -393,115 +390,90 @@ const elementTypes = [ }, ]; -/** - * Override the default block element to include elements styles. - * - * @param {Function} BlockListBlock Original component - * @return {Function} Wrapped component - */ -const withElementsStyles = createHigherOrderComponent( - ( BlockListBlock ) => ( props ) => { - const blockElementsContainerIdentifier = `wp-elements-${ useInstanceId( - BlockListBlock - ) }`; - - // The .editor-styles-wrapper selector is required on elements styles. As it is - // added to all other editor styles, not providing it causes reset and global - // styles to override element styles because of higher specificity. - const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; - const blockElementStyles = props.attributes.style?.elements; - - const styles = useMemo( () => { - if ( ! blockElementStyles ) { +function useBlockProps( { name, style } ) { + const blockElementsContainerIdentifier = `wp-elements-${ useInstanceId( + useBlockProps + ) }`; + + // The .editor-styles-wrapper selector is required on elements styles. As it is + // added to all other editor styles, not providing it causes reset and global + // styles to override element styles because of higher specificity. + const baseElementSelector = `.editor-styles-wrapper .${ blockElementsContainerIdentifier }`; + const blockElementStyles = style?.elements; + + const styles = useMemo( () => { + if ( ! blockElementStyles ) { + return; + } + + const elementCSSRules = []; + + elementTypes.forEach( ( { elementType, pseudo, elements } ) => { + const skipSerialization = shouldSkipSerialization( + name, + COLOR_SUPPORT_KEY, + elementType + ); + + if ( skipSerialization ) { return; } - const elementCSSRules = []; + const elementStyles = blockElementStyles?.[ elementType ]; - elementTypes.forEach( ( { elementType, pseudo, elements } ) => { - const skipSerialization = shouldSkipSerialization( - props.name, - COLOR_SUPPORT_KEY, - elementType + // Process primary element type styles. + if ( elementStyles ) { + const selector = scopeSelector( + baseElementSelector, + ELEMENTS[ elementType ] ); - if ( skipSerialization ) { - return; - } - - const elementStyles = blockElementStyles?.[ elementType ]; - - // Process primary element type styles. - if ( elementStyles ) { - const selector = scopeSelector( - baseElementSelector, - ELEMENTS[ elementType ] - ); - - elementCSSRules.push( - compileCSS( elementStyles, { selector } ) - ); - - // Process any interactive states for the element type. - if ( pseudo ) { - pseudo.forEach( ( pseudoSelector ) => { - if ( elementStyles[ pseudoSelector ] ) { - elementCSSRules.push( - compileCSS( - elementStyles[ pseudoSelector ], - { - selector: scopeSelector( - baseElementSelector, - `${ ELEMENTS[ elementType ] }${ pseudoSelector }` - ), - } - ) - ); - } - } ); - } - } + elementCSSRules.push( + compileCSS( elementStyles, { selector } ) + ); - // Process related elements e.g. h1-h6 for headings - if ( elements ) { - elements.forEach( ( element ) => { - if ( blockElementStyles[ element ] ) { + // Process any interactive states for the element type. + if ( pseudo ) { + pseudo.forEach( ( pseudoSelector ) => { + if ( elementStyles[ pseudoSelector ] ) { elementCSSRules.push( - compileCSS( blockElementStyles[ element ], { + compileCSS( elementStyles[ pseudoSelector ], { selector: scopeSelector( baseElementSelector, - ELEMENTS[ element ] + `${ ELEMENTS[ elementType ] }${ pseudoSelector }` ), } ) ); } } ); } - } ); + } - return elementCSSRules.length > 0 - ? elementCSSRules.join( '' ) - : undefined; - }, [ baseElementSelector, blockElementStyles, props.name ] ); - - useStyleOverride( { css: styles } ); - - return ( - <BlockListBlock - { ...props } - className={ - props.attributes.style?.elements - ? classnames( - props.className, - blockElementsContainerIdentifier - ) - : props.className - } - /> - ); - }, - 'withElementsStyles' -); + // Process related elements e.g. h1-h6 for headings + if ( elements ) { + elements.forEach( ( element ) => { + if ( blockElementStyles[ element ] ) { + elementCSSRules.push( + compileCSS( blockElementStyles[ element ], { + selector: scopeSelector( + baseElementSelector, + ELEMENTS[ element ] + ), + } ) + ); + } + } ); + } + } ); + + return elementCSSRules.length > 0 + ? elementCSSRules.join( '' ) + : undefined; + }, [ baseElementSelector, blockElementStyles, name ] ); + + useStyleOverride( { css: styles } ); + return { className: blockElementsContainerIdentifier }; +} addFilter( 'blocks.registerBlockType', @@ -520,9 +492,3 @@ addFilter( 'core/style/addEditProps', addEditProps ); - -addFilter( - 'editor.BlockListBlock', - 'core/editor/with-elements-styles', - withElementsStyles -); diff --git a/packages/block-editor/src/hooks/test/align.js b/packages/block-editor/src/hooks/test/align.js index c695399e993b0e..73c55133a4b29b 100644 --- a/packages/block-editor/src/hooks/test/align.js +++ b/packages/block-editor/src/hooks/test/align.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import { render, screen } from '@testing-library/react'; - /** * WordPress dependencies */ @@ -16,8 +11,7 @@ import { /** * Internal dependencies */ -import BlockEditorProvider from '../../components/provider'; -import { getValidAlignments, withDataAlign, addAssignedAlign } from '../align'; +import { getValidAlignments, addAssignedAlign } from '../align'; const noop = () => {}; @@ -149,107 +143,6 @@ describe( 'align', () => { } ); } ); - describe( 'withDataAlign', () => { - it( 'should render with wrapper props', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: true, - }, - } ); - - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( - <button { ...wrapperProps } /> - ) ); - - render( - <BlockEditorProvider - settings={ { alignWide: true, supportsLayout: false } } - value={ [] } - > - <EnhancedComponent - attributes={ { - align: 'wide', - } } - name="core/foo" - /> - </BlockEditorProvider> - ); - - expect( screen.getByRole( 'button' ) ).toHaveAttribute( - 'data-align', - 'wide' - ); - } ); - - it( 'should not render wide/full wrapper props if wide controls are not enabled', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: true, - }, - } ); - - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( - <button { ...wrapperProps } /> - ) ); - - render( - <BlockEditorProvider - settings={ { alignWide: false } } - value={ [] } - > - <EnhancedComponent - name="core/foo" - attributes={ { - align: 'wide', - } } - /> - </BlockEditorProvider> - ); - - expect( screen.getByRole( 'button' ) ).not.toHaveAttribute( - 'data-align', - 'wide' - ); - } ); - - it( 'should not render invalid align', () => { - registerBlockType( 'core/foo', { - ...blockSettings, - supports: { - align: true, - alignWide: false, - }, - } ); - - const EnhancedComponent = withDataAlign( ( { wrapperProps } ) => ( - <button { ...wrapperProps } /> - ) ); - - render( - <BlockEditorProvider - settings={ { alignWide: true } } - value={ [] } - > - <EnhancedComponent - name="core/foo" - attributes={ { - align: 'wide', - } } - /> - </BlockEditorProvider> - ); - - expect( screen.getByRole( 'button' ) ).not.toHaveAttribute( - 'data-align', - 'wide' - ); - } ); - } ); - describe( 'addAssignedAlign', () => { it( 'should do nothing if block does not support align', () => { registerBlockType( 'core/foo', blockSettings ); diff --git a/packages/block-editor/src/hooks/test/color.js b/packages/block-editor/src/hooks/test/color.js deleted file mode 100644 index 179ad21f913df4..00000000000000 --- a/packages/block-editor/src/hooks/test/color.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * External dependencies - */ -import { render } from '@testing-library/react'; - -/** - * WordPress dependencies - */ -import { registerBlockType, unregisterBlockType } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import BlockEditorProvider from '../../components/provider'; -import { withColorPaletteStyles } from '../color'; - -describe( 'withColorPaletteStyles', () => { - const settings = { - __experimentalFeatures: { - color: { - palette: { - default: [ - { - name: 'Pale pink', - slug: 'pale-pink', - color: '#f78da7', - }, - { - name: 'Vivid green cyan', - slug: 'vivid-green-cyan', - color: '#00d084', - }, - ], - }, - }, - }, - }; - - const EnhancedComponent = withColorPaletteStyles( - ( { getStyleObj, wrapperProps } ) => ( - <div>{ getStyleObj( wrapperProps.style ) }</div> - ) - ); - - beforeAll( () => { - registerBlockType( 'core/test-block', { - save: () => undefined, - edit: () => undefined, - category: 'text', - title: 'test block', - supports: { - color: { - text: true, - background: true, - }, - }, - } ); - } ); - - afterAll( () => { - unregisterBlockType( 'core/test-block' ); - } ); - - it( 'should add color styles from attributes', () => { - const getStyleObj = jest.fn(); - - render( - <BlockEditorProvider settings={ settings } value={ [] }> - <EnhancedComponent - attributes={ { - backgroundColor: 'vivid-green-cyan', - textColor: 'pale-pink', - } } - name="core/test-block" - getStyleObj={ getStyleObj } - /> - </BlockEditorProvider> - ); - - expect( getStyleObj ).toHaveBeenLastCalledWith( { - color: '#f78da7', - backgroundColor: '#00d084', - } ); - } ); - - it( 'should not add undefined style values', () => { - // This test ensures that undefined `color` and `backgroundColor` styles - // are not added to the styles object. An undefined `backgroundColor` - // style causes a React warning when gradients are used, as the gradient - // style currently uses the `background` shorthand syntax. - // See: https://github.com/WordPress/gutenberg/issues/36899. - const getStyleObj = jest.fn(); - - render( - <BlockEditorProvider settings={ settings } value={ [] }> - <EnhancedComponent - attributes={ { - backgroundColor: undefined, - textColor: undefined, - } } - name="core/test-block" - getStyleObj={ getStyleObj } - /> - </BlockEditorProvider> - ); - // Check explictly for the object used in the call, because - // `toHaveBeenCalledWith` does not check for empty keys. - expect( - getStyleObj.mock.calls[ getStyleObj.mock.calls.length - 1 ][ 0 ] - ).toStrictEqual( {} ); - } ); -} ); diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 98638ae5dabf54..76260ed0d4a63c 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { getBlockSupport } from '@wordpress/blocks'; -import { useMemo, useEffect, useId } from '@wordpress/element'; +import { useMemo, useEffect, useId, useState } from '@wordpress/element'; import { useDispatch } from '@wordpress/data'; import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { addFilter } from '@wordpress/hooks'; @@ -16,6 +16,10 @@ import { useSettingsForBlockElement } from '../components/global-styles/hooks'; import { getValueFromObjectPath, setImmutably } from '../utils/object'; import { store as blockEditorStore } from '../store'; import { unlock } from '../lock-unlock'; +/** + * External dependencies + */ +import classnames from 'classnames'; /** * Removed falsy values from nested object. @@ -430,3 +434,104 @@ export function createBlockEditFilter( features ) { ); addFilter( 'editor.BlockEdit', 'core/editor/hooks', withBlockEditHooks ); } + +function BlockProps( { index, useBlockProps, setAllWrapperProps, ...props } ) { + const wrapperProps = useBlockProps( props ); + const setWrapperProps = ( next ) => + setAllWrapperProps( ( prev ) => { + const nextAll = [ ...prev ]; + nextAll[ index ] = next; + return nextAll; + } ); + // Setting state after every render is fine because this component is + // pure and will only re-render when needed props change. + useEffect( () => { + // We could shallow compare the props, but since this component only + // changes when needed attributes change, the benefit is probably small. + setWrapperProps( wrapperProps ); + return () => { + setWrapperProps( undefined ); + }; + } ); + return null; +} + +const BlockPropsPure = pure( BlockProps ); + +export function createBlockListBlockFilter( features ) { + const withBlockListBlockHooks = createHigherOrderComponent( + ( BlockListBlock ) => ( props ) => { + const [ allWrapperProps, setAllWrapperProps ] = useState( + Array( features.length ).fill( undefined ) + ); + return [ + ...features.map( ( feature, i ) => { + const { + hasSupport, + attributeKeys = [], + useBlockProps, + } = feature; + + const neededProps = {}; + for ( const key of attributeKeys ) { + if ( props.attributes[ key ] ) { + neededProps[ key ] = props.attributes[ key ]; + } + } + + if ( + ! hasSupport( props.name ) || + // Skip rendering if none of the needed attributes are + // set. + ! Object.keys( neededProps ).length + ) { + return null; + } + + return ( + <BlockPropsPure + // We can use the index because the array length + // is fixed per page load right now. + key={ i } + index={ i } + useBlockProps={ useBlockProps } + // This component is pure, so we must pass a stable + // function reference. + setAllWrapperProps={ setAllWrapperProps } + name={ props.name } + // This component is pure, so only pass needed + // props!!! + { ...neededProps } + /> + ); + } ), + <BlockListBlock + key="edit" + { ...props } + wrapperProps={ allWrapperProps + .filter( Boolean ) + .reduce( ( acc, wrapperProps ) => { + return { + ...acc, + ...wrapperProps, + className: classnames( + acc.className, + wrapperProps.className + ), + style: { + ...acc.style, + ...wrapperProps.style, + }, + }; + }, props.wrapperProps || {} ) } + />, + ]; + }, + 'withBlockListBlockHooks' + ); + addFilter( + 'editor.BlockListBlock', + 'core/editor/hooks', + withBlockListBlockHooks + ); +} From 2e95c2067d94dc2ff2e577fe3a4b45b88c6a97f1 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Fri, 8 Dec 2023 13:18:38 -0500 Subject: [PATCH 101/325] Tabs: sync browser focus to selected tab in controlled mode (#56658) * link browser focus to selected tab changes in controlled mode * remove unnecessary memoization * remove duplicate condition * dont apply when selectOnMove is false * use `items` to find active element, instead of a ref * CHANGELOG * add `selectOnMove` unit tests * convert new tests to ariakit/test * remove redundant tests --- packages/components/CHANGELOG.md | 4 + packages/components/src/tabs/index.tsx | 23 +++- packages/components/src/tabs/test/index.tsx | 111 +++++++++++++++++++- 3 files changed, 136 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ed4b88d000f883..33e4e58d29e7c0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -26,6 +26,10 @@ - `Tabs`: implement new `tabId` prop ([#56883](https://github.com/WordPress/gutenberg/pull/56883)). +### Experimental + +- `Tabs`: improve focus handling in controlled mode ([#56658](https://github.com/WordPress/gutenberg/pull/56658)). + ## 25.13.0 (2023-11-29) ### Enhancements diff --git a/packages/components/src/tabs/index.tsx b/packages/components/src/tabs/index.tsx index 12dd0b4fcc83f4..7f738cb9f08a96 100644 --- a/packages/components/src/tabs/index.tsx +++ b/packages/components/src/tabs/index.tsx @@ -45,7 +45,7 @@ function Tabs( { const isControlled = selectedTabId !== undefined; const { items, selectedId } = store.useState(); - const { setSelectedId } = store; + const { setSelectedId, move } = store; // Keep track of whether tabs have been populated. This is used to prevent // certain effects from firing too early while tab data and relevant @@ -154,6 +154,27 @@ function Tabs( { setSelectedId, ] ); + // In controlled mode, make sure browser focus follows the selected tab if + // the selection is changed while a tab is already being focused. + useLayoutEffect( () => { + if ( ! isControlled || ! selectOnMove ) { + return; + } + const currentItem = items.find( ( item ) => item.id === selectedId ); + const activeElement = currentItem?.element?.ownerDocument.activeElement; + const tabsHasFocus = items.some( ( item ) => { + return activeElement && activeElement === item.element; + } ); + + if ( + activeElement && + tabsHasFocus && + selectedId !== activeElement.id + ) { + move( selectedId ); + } + }, [ isControlled, items, move, selectOnMove, selectedId ] ); + const contextValue = useMemo( () => ( { store, diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index 7e2d4671224858..70ad3c1c18ae54 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -7,7 +7,7 @@ import { press, click } from '@ariakit/test'; /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -102,6 +102,10 @@ const ControlledTabs = ( { string | undefined | null >( props.selectedTabId ); + useEffect( () => { + setSelectedTabId( props.selectedTabId ); + }, [ props.selectedTabId ] ); + return ( <Tabs { ...props } @@ -1168,5 +1172,110 @@ describe( 'Tabs', () => { ).not.toBeInTheDocument(); } ); } ); + + describe( 'When `selectOnMove` is `true`', () => { + it( 'should automatically select a newly focused tab', async () => { + render( <ControlledTabs tabs={ TABS } selectedTabId="beta" /> ); + + await press.Tab(); + + // Tab key should focus the currently selected tab, which is Beta. + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( await getSelectedTab() ).toHaveFocus(); + + // Arrow keys should select and move focus to the next tab. + await press.ArrowRight(); + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( await getSelectedTab() ).toHaveFocus(); + } ); + it( 'should automatically update focus when the selected tab is changed by the controlling component', async () => { + const { rerender } = render( + <ControlledTabs tabs={ TABS } selectedTabId="beta" /> + ); + + // Tab key should focus the currently selected tab, which is Beta. + await press.Tab(); + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( await getSelectedTab() ).toHaveFocus(); + + rerender( + <ControlledTabs tabs={ TABS } selectedTabId="gamma" /> + ); + + // When the selected tab is changed, it should automatically receive focus. + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( await getSelectedTab() ).toHaveFocus(); + } ); + } ); + describe( 'When `selectOnMove` is `false`', () => { + it( 'should apply focus without automatically changing the selected tab', async () => { + render( + <ControlledTabs + tabs={ TABS } + selectedTabId="beta" + selectOnMove={ false } + /> + ); + + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + + // Tab key should focus the currently selected tab, which is Beta. + await press.Tab(); + expect( + await screen.findByRole( 'tab', { name: 'Beta' } ) + ).toHaveFocus(); + + // Arrow key should move focus but not automatically change the selected tab. + await press.ArrowRight(); + expect( + screen.getByRole( 'tab', { name: 'Gamma' } ) + ).toHaveFocus(); + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + + // Pressing the spacebar should select the focused tab. + await press.Space(); + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + + // Arrow key should move focus but not automatically change the selected tab. + await press.ArrowRight(); + expect( + screen.getByRole( 'tab', { name: 'Alpha' } ) + ).toHaveFocus(); + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + + // Pressing the enter/return should select the focused tab. + await press.Enter(); + expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + } ); + it( 'should not automatically update focus when the selected tab is changed by the controlling component', async () => { + const { rerender } = render( + <ControlledTabs + tabs={ TABS } + selectedTabId="beta" + selectOnMove={ false } + /> + ); + + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + + // Tab key should focus the currently selected tab, which is Beta. + await press.Tab(); + expect( await getSelectedTab() ).toHaveFocus(); + + rerender( + <ControlledTabs + tabs={ TABS } + selectedTabId="gamma" + selectOnMove={ false } + /> + ); + + // When the selected tab is changed, it should not automatically receive focus. + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( + screen.getByRole( 'tab', { name: 'Beta' } ) + ).toHaveFocus(); + } ); + } ); } ); } ); From 8a702c320edfe34be161d7e59c03e0c7adeae92a Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 8 Dec 2023 19:48:41 +0100 Subject: [PATCH 102/325] Editor: Move the edit template blocks notification to editor package (#56901) --- .../block-editor/site-editor-canvas.js | 81 +++++++++---------- .../page-content-focus-notifications/index.js | 8 -- .../edit-template-blocks-notification.js} | 8 +- .../src/components/editor-canvas/index.js | 2 + 4 files changed, 46 insertions(+), 53 deletions(-) delete mode 100644 packages/edit-site/src/components/page-content-focus-notifications/index.js rename packages/{edit-site/src/components/page-content-focus-notifications/edit-template-notification.js => editor/src/components/editor-canvas/edit-template-blocks-notification.js} (94%) diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index bfbb2d3eac43ff..944dbab3f96f08 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -24,7 +24,6 @@ import { NAVIGATION_POST_TYPE, } from '../../utils/constants'; import { unlock } from '../../lock-unlock'; -import PageContentFocusNotifications from '../page-content-focus-notifications'; export default function SiteEditorCanvas() { const { clearSelectedBlock } = useDispatch( blockEditorStore ); @@ -60,50 +59,46 @@ export default function SiteEditorCanvas() { const forceFullHeight = isNavigationFocusMode; return ( - <> - <EditorCanvasContainer.Slot> - { ( [ editorCanvasView ] ) => - editorCanvasView ? ( - <div className="edit-site-visual-editor is-focus-mode"> - { editorCanvasView } - </div> - ) : ( - <BlockTools - className={ classnames( 'edit-site-visual-editor', { - 'is-focus-mode': - isFocusMode || !! editorCanvasView, - 'is-view-mode': isViewMode, - } ) } - __unstableContentRef={ contentRef } - onClick={ ( event ) => { - // Clear selected block when clicking on the gray background. - if ( event.target === event.currentTarget ) { - clearSelectedBlock(); - } - } } + <EditorCanvasContainer.Slot> + { ( [ editorCanvasView ] ) => + editorCanvasView ? ( + <div className="edit-site-visual-editor is-focus-mode"> + { editorCanvasView } + </div> + ) : ( + <BlockTools + className={ classnames( 'edit-site-visual-editor', { + 'is-focus-mode': isFocusMode || !! editorCanvasView, + 'is-view-mode': isViewMode, + } ) } + __unstableContentRef={ contentRef } + onClick={ ( event ) => { + // Clear selected block when clicking on the gray background. + if ( event.target === event.currentTarget ) { + clearSelectedBlock(); + } + } } + > + <BackButton /> + <ResizableEditor + enableResizing={ enableResizing } + height={ + sizes.height && ! forceFullHeight + ? sizes.height + : '100%' + } > - <BackButton /> - <ResizableEditor + <EditorCanvas enableResizing={ enableResizing } - height={ - sizes.height && ! forceFullHeight - ? sizes.height - : '100%' - } + settings={ settings } + contentRef={ contentRef } > - <EditorCanvas - enableResizing={ enableResizing } - settings={ settings } - contentRef={ contentRef } - > - { resizeObserver } - </EditorCanvas> - </ResizableEditor> - </BlockTools> - ) - } - </EditorCanvasContainer.Slot> - <PageContentFocusNotifications contentRef={ contentRef } /> - </> + { resizeObserver } + </EditorCanvas> + </ResizableEditor> + </BlockTools> + ) + } + </EditorCanvasContainer.Slot> ); } diff --git a/packages/edit-site/src/components/page-content-focus-notifications/index.js b/packages/edit-site/src/components/page-content-focus-notifications/index.js deleted file mode 100644 index b4d8f62436ba27..00000000000000 --- a/packages/edit-site/src/components/page-content-focus-notifications/index.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Internal dependencies - */ -import EditTemplateNotification from './edit-template-notification'; - -export default function PageContentFocusNotifications( { contentRef } ) { - return <EditTemplateNotification contentRef={ contentRef } />; -} diff --git a/packages/edit-site/src/components/page-content-focus-notifications/edit-template-notification.js b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js similarity index 94% rename from packages/edit-site/src/components/page-content-focus-notifications/edit-template-notification.js rename to packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js index 8799eb4d661281..047ca6688ff021 100644 --- a/packages/edit-site/src/components/page-content-focus-notifications/edit-template-notification.js +++ b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js @@ -6,7 +6,11 @@ import { useEffect, useState, useRef } from '@wordpress/element'; import { store as noticesStore } from '@wordpress/notices'; import { __ } from '@wordpress/i18n'; import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; -import { store as editorStore } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; /** * Component that: @@ -22,7 +26,7 @@ import { store as editorStore } from '@wordpress/editor'; * @param {import('react').RefObject<HTMLElement>} props.contentRef Ref to the block * editor iframe canvas. */ -export default function EditTemplateNotification( { contentRef } ) { +export default function EditTemplateBlocksNotification( { contentRef } ) { const renderingMode = useSelect( ( select ) => select( editorStore ).getRenderingMode(), [] diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index 86df470087862b..a0e82694b10449 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -27,6 +27,7 @@ import { useMergeRefs } from '@wordpress/compose'; import PostTitle from '../post-title'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; +import EditTemplateBlocksNotification from './edit-template-blocks-notification'; const { LayoutStyle, @@ -361,6 +362,7 @@ function EditorCanvas( } renderAppender={ renderAppender } /> + <EditTemplateBlocksNotification contentRef={ localRef } /> </RecursionProvider> { children } </BlockCanvas> From 25cd2468f825dc5bf1f92185aef27c5173d0d22e Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack <birgit.pauli@gmail.com> Date: Fri, 8 Dec 2023 21:41:35 +0100 Subject: [PATCH 103/325] Doc: Spinner - add Storybook link (#56818) * Add link to the Spinner's Storybook page * remove 'also' --- packages/components/src/search-control/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/search-control/README.md b/packages/components/src/search-control/README.md index bd12580f3c8780..8656cc597c7add 100644 --- a/packages/components/src/search-control/README.md +++ b/packages/components/src/search-control/README.md @@ -8,6 +8,8 @@ SearchControl components let users display a search control. 1. [Development guidelines](#development-guidelines) 2. [Related components](#related-components) + Check out the [Storybook page](https://wordpress.github.io/gutenberg/?path=/docs/components-spinner--docs) for a visual exploration of this component. + ## Development guidelines ### Usage From 83434bf46dc0529d9a97e8c4a5155d2a57c407f2 Mon Sep 17 00:00:00 2001 From: Ben Dwyer <ben@scruffian.com> Date: Fri, 8 Dec 2023 20:56:37 +0000 Subject: [PATCH 104/325] Improve text and design of the block removal warnings (#56869) * Improve text and design of the block removal warnings * Updating heading from "Are you sure?" to "Be careful!" --------- Co-authored-by: annezazu <annezazu@gmail.com> --- .../block-removal-warning-modal/index.js | 21 +++++++------------ .../specs/site-editor/block-removal.spec.js | 6 ++++-- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/block-removal-warning-modal/index.js b/packages/block-editor/src/components/block-removal-warning-modal/index.js index 08f3deccb5ae08..a6de602bcdda81 100644 --- a/packages/block-editor/src/components/block-removal-warning-modal/index.js +++ b/packages/block-editor/src/components/block-removal-warning-modal/index.js @@ -8,7 +8,7 @@ import { Button, __experimentalHStack as HStack, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; +import { __, _n } from '@wordpress/i18n'; /** * Internal dependencies @@ -48,22 +48,15 @@ export function BlockRemovalWarningModal( { rules } ) { return ( <Modal - title={ __( 'Are you sure?' ) } + title={ __( 'Be careful!' ) } onRequestClose={ clearBlockRemovalPrompt } > - { blockNamesForPrompt.length === 1 ? ( - <p>{ rules[ blockNamesForPrompt[ 0 ] ] }</p> - ) : ( - <ul style={ { listStyleType: 'disc', paddingLeft: '1rem' } }> - { blockNamesForPrompt.map( ( name ) => ( - <li key={ name }>{ rules[ name ] }</li> - ) ) } - </ul> - ) } <p> - { blockNamesForPrompt.length > 1 - ? __( 'Removing these blocks is not advised.' ) - : __( 'Removing this block is not advised.' ) } + { _n( + 'Post or page content will not be displayed if you delete this block.', + 'Post or page content will not be displayed if you delete these blocks.', + blockNamesForPrompt.length + ) } </p> <HStack justify="right"> <Button variant="tertiary" onClick={ clearBlockRemovalPrompt }> diff --git a/test/e2e/specs/site-editor/block-removal.spec.js b/test/e2e/specs/site-editor/block-removal.spec.js index 5c46ac769efd3f..27d3762364b446 100644 --- a/test/e2e/specs/site-editor/block-removal.spec.js +++ b/test/e2e/specs/site-editor/block-removal.spec.js @@ -34,7 +34,9 @@ test.describe( 'Site editor block removal prompt', () => { // Expect the block removal prompt to have appeared await expect( - page.getByText( 'Query Loop displays a list of posts or pages.' ) + page.getByText( + 'Post or page content will not be displayed if you delete these blocks.' + ) ).toBeVisible(); } ); @@ -57,7 +59,7 @@ test.describe( 'Site editor block removal prompt', () => { // Expect the block removal prompt to have appeared await expect( page.getByText( - 'Post Template displays each post or page in a Query Loop.' + 'Post or page content will not be displayed if you delete this block.' ) ).toBeVisible(); } ); From 1783a4e15a0d0806151ed3b88fec8cded2886d5a Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Sat, 9 Dec 2023 22:16:57 +0100 Subject: [PATCH 105/325] Editor: Unify device preview styles (#56904) --- .../src/components/visual-editor/index.js | 78 +++---------------- packages/edit-post/src/editor.native.js | 7 +- .../components/block-editor/editor-canvas.js | 52 +++++-------- .../header-edit-mode/document-tools/index.js | 2 - .../src/components/editor-canvas/index.js | 26 +++++-- 5 files changed, 50 insertions(+), 115 deletions(-) diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index 5d309947a37c2c..b929e03bc453a4 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -10,12 +10,8 @@ import { store as editorStore, privateApis as editorPrivateApis, } from '@wordpress/editor'; -import { - BlockTools, - __experimentalUseResizeCanvas as useResizeCanvas, -} from '@wordpress/block-editor'; +import { BlockTools } from '@wordpress/block-editor'; import { useRef, useMemo } from '@wordpress/element'; -import { __unstableMotion as motion } from '@wordpress/components'; import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; @@ -31,20 +27,17 @@ const isGutenbergPlugin = process.env.IS_GUTENBERG_PLUGIN ? true : false; export default function VisualEditor( { styles } ) { const { - deviceType, isWelcomeGuideVisible, renderingMode, isBlockBasedTheme, hasV3BlocksOnly, } = useSelect( ( select ) => { const { isFeatureActive } = select( editPostStore ); - const { getEditorSettings, getRenderingMode, getDeviceType } = - select( editorStore ); + const { getEditorSettings, getRenderingMode } = select( editorStore ); const { getBlockTypes } = select( blocksStore ); const editorSettings = getEditorSettings(); return { - deviceType: getDeviceType(), isWelcomeGuideVisible: isFeatureActive( 'welcomeGuide' ), renderingMode: getRenderingMode(), isBlockBasedTheme: editorSettings.__unstableIsBlockBasedTheme, @@ -57,43 +50,12 @@ export default function VisualEditor( { styles } ) { ( select ) => select( editPostStore ).hasMetaBoxes(), [] ); - const desktopCanvasStyles = { - height: '100%', - width: '100%', - marginLeft: 'auto', - marginRight: 'auto', - display: 'flex', - flexFlow: 'column', - // Default background color so that grey - // .edit-post-editor-regions__content color doesn't show through. - background: 'white', - }; - const templateModeStyles = { - ...desktopCanvasStyles, - borderRadius: '2px 2px 0 0', - border: '1px solid #ddd', - borderBottom: 0, - }; - const resizedCanvasStyles = useResizeCanvas( deviceType ); - const previewMode = 'is-' + deviceType.toLowerCase() + '-preview'; - - let animatedStyles = - renderingMode === 'template-only' - ? templateModeStyles - : desktopCanvasStyles; - if ( resizedCanvasStyles ) { - animatedStyles = resizedCanvasStyles; - } let paddingBottom; // Add a constant padding for the typewritter effect. When typing at the // bottom, there needs to be room to scroll up. - if ( - ! hasMetaBoxes && - ! resizedCanvasStyles && - renderingMode === 'post-only' - ) { + if ( ! hasMetaBoxes && renderingMode === 'post-only' ) { paddingBottom = '40vh'; } @@ -115,9 +77,7 @@ export default function VisualEditor( { styles } ) { const isToBeIframed = ( ( hasV3BlocksOnly || ( isGutenbergPlugin && isBlockBasedTheme ) ) && ! hasMetaBoxes ) || - renderingMode === 'template-only' || - deviceType === 'Tablet' || - deviceType === 'Mobile'; + renderingMode === 'template-only'; return ( <BlockTools @@ -127,28 +87,14 @@ export default function VisualEditor( { styles } ) { 'has-inline-canvas': ! isToBeIframed, } ) } > - <motion.div - className="edit-post-visual-editor__content-area" - animate={ { - padding: - renderingMode === 'template-only' ? '48px 48px 0' : 0, - } } - > - <motion.div - animate={ animatedStyles } - initial={ desktopCanvasStyles } - className={ previewMode } - > - <EditorCanvas - ref={ ref } - disableIframe={ ! isToBeIframed } - styles={ styles } - // We should auto-focus the canvas (title) on load. - // eslint-disable-next-line jsx-a11y/no-autofocus - autoFocus={ ! isWelcomeGuideVisible } - /> - </motion.div> - </motion.div> + <EditorCanvas + ref={ ref } + disableIframe={ ! isToBeIframed } + styles={ styles } + // We should auto-focus the canvas (title) on load. + // eslint-disable-next-line jsx-a11y/no-autofocus + autoFocus={ ! isWelcomeGuideVisible } + /> </BlockTools> ); } diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 49b6022d6c05b1..17e2ad1780f1d7 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -9,7 +9,7 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { EditorProvider, store as editorStore } from '@wordpress/editor'; +import { EditorProvider } from '@wordpress/editor'; import { parse, serialize, store as blocksStore } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -195,12 +195,9 @@ export default compose( [ const { isFeatureActive, getEditorMode, getHiddenBlockTypes } = select( editPostStore ); const { getBlockTypes } = select( blocksStore ); - const { getDeviceType } = select( editorStore ); return { - hasFixedToolbar: - isFeatureActive( 'fixedToolbar' ) || - getDeviceType() !== 'Desktop', + hasFixedToolbar: isFeatureActive( 'fixedToolbar' ), focusMode: isFeatureActive( 'focusMode' ), mode: getEditorMode(), hiddenBlockTypes: getHiddenBlockTypes(), diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index bf40c655b4477d..d7dbf6fb07a7ab 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -6,18 +6,12 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { - __experimentalUseResizeCanvas as useResizeCanvas, - store as blockEditorStore, -} from '@wordpress/block-editor'; +import { store as blockEditorStore } from '@wordpress/block-editor'; import { useSelect, useDispatch } from '@wordpress/data'; import { ENTER, SPACE } from '@wordpress/keycodes'; import { useState, useEffect, useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; -import { - privateApis as editorPrivateApis, - store as editorStore, -} from '@wordpress/editor'; +import { privateApis as editorPrivateApis } from '@wordpress/editor'; /** * Internal dependencies @@ -38,33 +32,24 @@ function EditorCanvas( { contentRef, ...props } ) { - const { - hasBlocks, - isFocusMode, - templateType, - canvasMode, - deviceType, - isZoomOutMode, - } = useSelect( ( select ) => { - const { getBlockCount, __unstableGetEditorMode } = - select( blockEditorStore ); - const { getEditedPostType, getCanvasMode } = unlock( - select( editSiteStore ) - ); - const { getDeviceType } = select( editorStore ); - const _templateType = getEditedPostType(); + const { hasBlocks, isFocusMode, templateType, canvasMode, isZoomOutMode } = + useSelect( ( select ) => { + const { getBlockCount, __unstableGetEditorMode } = + select( blockEditorStore ); + const { getEditedPostType, getCanvasMode } = unlock( + select( editSiteStore ) + ); + const _templateType = getEditedPostType(); - return { - templateType: _templateType, - isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), - deviceType: getDeviceType(), - isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', - canvasMode: getCanvasMode(), - hasBlocks: !! getBlockCount(), - }; - }, [] ); + return { + templateType: _templateType, + isFocusMode: FOCUSABLE_ENTITIES.includes( _templateType ), + isZoomOutMode: __unstableGetEditorMode() === 'zoom-out', + canvasMode: getCanvasMode(), + hasBlocks: !! getBlockCount(), + }; + }, [] ); const { setCanvasMode } = unlock( useDispatch( editSiteStore ) ); - const deviceStyles = useResizeCanvas( deviceType ); const [ isFocused, setIsFocused ] = useState( false ); useEffect( () => { @@ -132,7 +117,6 @@ function EditorCanvas( { expand: isZoomOutMode, scale: isZoomOutMode ? 0.45 : undefined, frameSize: isZoomOutMode ? 100 : undefined, - style: enableResizing ? {} : deviceStyles, className: classnames( 'edit-site-visual-editor__editor-canvas', { diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index eefc6668b5b95d..53d7de6fba18fa 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -42,13 +42,11 @@ export default function DocumentTools( { useSelect( ( select ) => { const { isInserterOpened, isListViewOpened, getEditorMode } = select( editSiteStore ); - const { getDeviceType } = select( editorStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); return { - deviceType: getDeviceType(), isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index a0e82694b10449..921f3ce23c0ee4 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -14,6 +14,7 @@ import { useSettings, __experimentalRecursionProvider as RecursionProvider, privateApis as blockEditorPrivateApis, + __experimentalUseResizeCanvas as useResizeCanvas, } from '@wordpress/block-editor'; import { useEffect, useRef, useMemo, forwardRef } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; @@ -90,6 +91,7 @@ function EditorCanvas( editedPostTemplate = {}, wrapperBlockName, wrapperUniqueId, + deviceType, } = useSelect( ( select ) => { const { getCurrentPostId, @@ -97,7 +99,10 @@ function EditorCanvas( getCurrentTemplateId, getEditorSettings, getRenderingMode, + getDeviceType, } = select( editorStore ); + const { getPostType, canUser, getEditedEntityRecord } = + select( coreStore ); const postTypeSlug = getCurrentPostType(); const _renderingMode = getRenderingMode(); let _wrapperBlockName; @@ -110,14 +115,11 @@ function EditorCanvas( const editorSettings = getEditorSettings(); const supportsTemplateMode = editorSettings.supportsTemplateMode; - const postType = select( coreStore ).getPostType( postTypeSlug ); - const canEditTemplate = select( coreStore ).canUser( - 'create', - 'templates' - ); + const postType = getPostType( postTypeSlug ); + const canEditTemplate = canUser( 'create', 'templates' ); const currentTemplateId = getCurrentTemplateId(); const template = currentTemplateId - ? select( coreStore ).getEditedEntityRecord( + ? getEditedEntityRecord( 'postType', 'wp_template', currentTemplateId @@ -135,6 +137,7 @@ function EditorCanvas( : undefined, wrapperBlockName: _wrapperBlockName, wrapperUniqueId: getCurrentPostId(), + deviceType: getDeviceType(), }; }, [] ); const { isCleanNewPost } = useSelect( editorStore ); @@ -152,6 +155,7 @@ function EditorCanvas( }; }, [] ); + const deviceStyles = useResizeCanvas( deviceType ); const [ globalLayoutSettings ] = useSettings( 'layout' ); // fallbackLayout is used if there is no Post Content, @@ -292,11 +296,16 @@ function EditorCanvas( return ( <BlockCanvas - shouldIframe={ ! disableIframe } + shouldIframe={ + ! disableIframe || [ 'Tablet', 'Mobile' ].includes( deviceType ) + } contentRef={ contentRef } styles={ styles } height="100%" - iframeProps={ iframeProps } + iframeProps={ { + ...iframeProps, + style: { ...iframeProps?.style, ...deviceStyles }, + } } > { themeSupportsLayout && ! themeHasDisabledLayoutStyles && @@ -348,6 +357,7 @@ function EditorCanvas( <BlockList className={ classnames( className, + 'is-' + deviceType.toLowerCase() + '-preview', renderingMode !== 'post-only' ? 'wp-site-blocks' : `${ blockListLayoutClass } wp-block-post-content` // Ensure root level blocks receive default/flow blockGap styling rules. From 69890452bfa4befc0a02206e6324947678b61ffb Mon Sep 17 00:00:00 2001 From: Andy Peatling <apeatling@users.noreply.github.com> Date: Sat, 9 Dec 2023 19:38:36 -0800 Subject: [PATCH 106/325] Revert "Doc: Spinner - add Storybook link (#56818)" (#56913) This reverts commit 25cd2468f825dc5bf1f92185aef27c5173d0d22e. --- packages/components/src/search-control/README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/components/src/search-control/README.md b/packages/components/src/search-control/README.md index 8656cc597c7add..bd12580f3c8780 100644 --- a/packages/components/src/search-control/README.md +++ b/packages/components/src/search-control/README.md @@ -8,8 +8,6 @@ SearchControl components let users display a search control. 1. [Development guidelines](#development-guidelines) 2. [Related components](#related-components) - Check out the [Storybook page](https://wordpress.github.io/gutenberg/?path=/docs/components-spinner--docs) for a visual exploration of this component. - ## Development guidelines ### Usage From 419d754285f9f3ead73b960e511cd9da8a7adf2d Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sun, 10 Dec 2023 23:10:08 +0900 Subject: [PATCH 107/325] Don't render undefined classname in useBlockProps hook (#56923) * Remove undefined classname in useBlockProps hook * Consider when block default classname is undefined --- .../block-list/use-block-props/index.js | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index cd7c66174c3ff9..593beafa06d83f 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -172,29 +172,32 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { _isSelected && __unstableGetEditorMode() === 'edit' ? getSelectedBlocksInitialCaretPosition() : undefined, - classNames: classnames( { - 'is-selected': _isSelected, - 'is-highlighted': isBlockHighlighted( clientId ), - 'is-multi-selected': isMultiSelected, - 'is-partially-selected': - isMultiSelected && - ! __unstableIsFullySelected() && - ! __unstableSelectionHasUnmergeableBlock(), - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': isBlockBeingDragged( clientId ), - 'has-child-selected': isAncestorOfSelectedBlock, - 'remove-outline': _isSelected && outlineMode && typing, - 'is-block-moving-mode': !! movingClientId, - 'can-insert-moving-block': - movingClientId && - canInsertBlockType( - getBlockName( movingClientId ), - getBlockRootClientId( clientId ) - ), - [ attributes.className ]: hasLightBlockWrapper, - [ getBlockDefaultClassName( blockName ) ]: - hasLightBlockWrapper, - } ), + classNames: classnames( + { + 'is-selected': _isSelected, + 'is-highlighted': isBlockHighlighted( clientId ), + 'is-multi-selected': isMultiSelected, + 'is-partially-selected': + isMultiSelected && + ! __unstableIsFullySelected() && + ! __unstableSelectionHasUnmergeableBlock(), + 'is-reusable': isReusableBlock( blockType ), + 'is-dragging': isBlockBeingDragged( clientId ), + 'has-child-selected': isAncestorOfSelectedBlock, + 'remove-outline': _isSelected && outlineMode && typing, + 'is-block-moving-mode': !! movingClientId, + 'can-insert-moving-block': + movingClientId && + canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + }, + hasLightBlockWrapper ? attributes.className : undefined, + hasLightBlockWrapper + ? getBlockDefaultClassName( blockName ) + : undefined + ), }; }, [ clientId ] From 5137da0ec6422610bbfee7f2f6792c9b4504060b Mon Sep 17 00:00:00 2001 From: Dennis Snell <dennis.snell@automattic.com> Date: Sun, 10 Dec 2023 19:13:12 +0100 Subject: [PATCH 108/325] HTML API: Backport updates from Core, fix Interactivity API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix: Update Interactivity API Use of Private HTML API Classes to avoid Crash Previously the HTML API was using a mixture of approaches in its helper classes for describing spans of text. One was (start, end) and the other was (start, length). In WordPress/wordpress-develop#5721 these were all normalized to (start, length) but the Interactivity API was not updated in concert with that change. In this patch the `inner_html` methods are updated to use the appropriate formats. this PR also includes the necessary back port of the HTML API to provide the new methods so that older WP installs don’t crash --- ...ass-gutenberg-html-attribute-token-6-5.php | 116 + .../class-gutenberg-html-span-6-5.php | 56 + ...class-gutenberg-html-tag-processor-6-5.php | 2483 +++++++++++++++++ ...ss-gutenberg-html-text-replacement-6-5.php | 64 + .../class-wp-directive-processor.php | 8 +- lib/load.php | 4 + 6 files changed, 2727 insertions(+), 4 deletions(-) create mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php create mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php create mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php create mode 100644 lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php new file mode 100644 index 00000000000000..70359ea339d669 --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php @@ -0,0 +1,116 @@ +<?php +/** + * HTML API: WP_HTML_Attribute_Token class + * + * @package WordPress + * @subpackage HTML-API + * @since 6.2.0 + */ + +/** + * Core class used by the HTML tag processor as a data structure for the attribute token, + * allowing to drastically improve performance. + * + * This class is for internal usage of the WP_HTML_Tag_Processor class. + * + * @access private + * @since 6.2.0 + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @see WP_HTML_Tag_Processor + */ +class Gutenberg_HTML_Attribute_Token_6_5 { + /** + * Attribute name. + * + * @since 6.2.0 + * + * @var string + */ + public $name; + + /** + * Attribute value. + * + * @since 6.2.0 + * + * @var int + */ + public $value_starts_at; + + /** + * How many bytes the value occupies in the input HTML. + * + * @since 6.2.0 + * + * @var int + */ + public $value_length; + + /** + * The string offset where the attribute name starts. + * + * @since 6.2.0 + * + * @var int + */ + public $start; + + /** + * Byte length of text spanning the attribute inside a tag. + * + * This span starts at the first character of the attribute name + * and it ends after one of three cases: + * + * - at the end of the attribute name for boolean attributes. + * - at the end of the value for unquoted attributes. + * - at the final single or double quote for quoted attributes. + * + * Example: + * + * <div class="post"> + * ------------ length is 12, including quotes + * + * <input type="checked" checked id="selector"> + * ------- length is 6 + * + * <a rel=noopener> + * ------------ length is 11 + * + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @var int + */ + public $length; + + /** + * Whether the attribute is a boolean attribute with value `true`. + * + * @since 6.2.0 + * + * @var bool + */ + public $is_true; + + /** + * Constructor. + * + * @since 6.2.0 + * @since 6.5.0 Replaced `end` with `length` to more closely match `substr()`. + * + * @param string $name Attribute name. + * @param int $value_start Attribute value. + * @param int $value_length Number of bytes attribute value spans. + * @param int $start The string offset where the attribute name starts. + * @param int $length Byte length of the entire attribute name or name and value pair expression. + * @param bool $is_true Whether the attribute is a boolean attribute with true value. + */ + public function __construct( $name, $value_start, $value_length, $start, $length, $is_true ) { + $this->name = $name; + $this->value_starts_at = $value_start; + $this->value_length = $value_length; + $this->start = $start; + $this->length = $length; + $this->is_true = $is_true; + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php new file mode 100644 index 00000000000000..ed596f1266ab5d --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php @@ -0,0 +1,56 @@ +<?php +/** + * HTML API: WP_HTML_Span class + * + * @package WordPress + * @subpackage HTML-API + * @since 6.2.0 + */ + +/** + * Core class used by the HTML tag processor to represent a textual span + * inside an HTML document. + * + * This is a two-tuple in disguise, used to avoid the memory overhead + * involved in using an array for the same purpose. + * + * This class is for internal usage of the WP_HTML_Tag_Processor class. + * + * @access private + * @since 6.2.0 + * @since 6.5.0 Replaced `end` with `length` to more closely align with `substr()`. + * + * @see WP_HTML_Tag_Processor + */ +class Gutenberg_HTML_Span_6_5 { + /** + * Byte offset into document where span begins. + * + * @since 6.2.0 + * + * @var int + */ + public $start; + + /** + * Byte length of this span. + * + * @since 6.5.0 + * + * @var int + */ + public $length; + + /** + * Constructor. + * + * @since 6.2.0 + * + * @param int $start Byte offset into document where replacement span begins. + * @param int $length Byte length of span. + */ + public function __construct( $start, $length ) { + $this->start = $start; + $this->length = $length; + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php new file mode 100644 index 00000000000000..5594110f0d1c8e --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php @@ -0,0 +1,2483 @@ +<?php +/** + * HTML API: WP_HTML_Tag_Processor class + * + * Scans through an HTML document to find specific tags, then + * transforms those tags by adding, removing, or updating the + * values of the HTML attributes within that tag (opener). + * + * Does not fully parse HTML or _recurse_ into the HTML structure + * Instead this scans linearly through a document and only parses + * the HTML tag openers. + * + * ### Possible future direction for this module + * + * - Prune the whitespace when removing classes/attributes: e.g. "a b c" -> "c" not " c". + * This would increase the size of the changes for some operations but leave more + * natural-looking output HTML. + * - Decode HTML character references within class names when matching. E.g. match having + * class `1<"2` needs to recognize `class="1&lt;&quot;2"`. Currently the Tag Processor + * will fail to find the right tag if the class name is encoded as such. + * - Properly decode HTML character references in `get_attribute()`. PHP's + * `html_entity_decode()` is wrong in a couple ways: it doesn't account for the + * no-ambiguous-ampersand rule, and it improperly handles the way semicolons may + * or may not terminate a character reference. + * + * @package WordPress + * @subpackage HTML-API + * @since 6.2.0 + */ + +/** + * Core class used to modify attributes in an HTML document for tags matching a query. + * + * ## Usage + * + * Use of this class requires three steps: + * + * 1. Create a new class instance with your input HTML document. + * 2. Find the tag(s) you are looking for. + * 3. Request changes to the attributes in those tag(s). + * + * Example: + * + * $tags = new WP_HTML_Tag_Processor( $html ); + * if ( $tags->next_tag( 'option' ) ) { + * $tags->set_attribute( 'selected', true ); + * } + * + * ### Finding tags + * + * The `next_tag()` function moves the internal cursor through + * your input HTML document until it finds a tag meeting any of + * the supplied restrictions in the optional query argument. If + * no argument is provided then it will find the next HTML tag, + * regardless of what kind it is. + * + * If you want to _find whatever the next tag is_: + * + * $tags->next_tag(); + * + * | Goal | Query | + * |-----------------------------------------------------------|---------------------------------------------------------------------------------| + * | Find any tag. | `$tags->next_tag();` | + * | Find next image tag. | `$tags->next_tag( array( 'tag_name' => 'img' ) );` | + * | Find next image tag (without passing the array). | `$tags->next_tag( 'img' );` | + * | Find next tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'class_name' => 'fullwidth' ) );` | + * | Find next image tag containing the `fullwidth` CSS class. | `$tags->next_tag( array( 'tag_name' => 'img', 'class_name' => 'fullwidth' ) );` | + * + * If a tag was found meeting your criteria then `next_tag()` + * will return `true` and you can proceed to modify it. If it + * returns `false`, however, it failed to find the tag and + * moved the cursor to the end of the file. + * + * Once the cursor reaches the end of the file the processor + * is done and if you want to reach an earlier tag you will + * need to recreate the processor and start over, as it's + * unable to back up or move in reverse. + * + * See the section on bookmarks for an exception to this + * no-backing-up rule. + * + * #### Custom queries + * + * Sometimes it's necessary to further inspect an HTML tag than + * the query syntax here permits. In these cases one may further + * inspect the search results using the read-only functions + * provided by the processor or external state or variables. + * + * Example: + * + * // Paint up to the first five DIV or SPAN tags marked with the "jazzy" style. + * $remaining_count = 5; + * while ( $remaining_count > 0 && $tags->next_tag() ) { + * if ( + * ( 'DIV' === $tags->get_tag() || 'SPAN' === $tags->get_tag() ) && + * 'jazzy' === $tags->get_attribute( 'data-style' ) + * ) { + * $tags->add_class( 'theme-style-everest-jazz' ); + * $remaining_count--; + * } + * } + * + * `get_attribute()` will return `null` if the attribute wasn't present + * on the tag when it was called. It may return `""` (the empty string) + * in cases where the attribute was present but its value was empty. + * For boolean attributes, those whose name is present but no value is + * given, it will return `true` (the only way to set `false` for an + * attribute is to remove it). + * + * ### Modifying HTML attributes for a found tag + * + * Once you've found the start of an opening tag you can modify + * any number of the attributes on that tag. You can set a new + * value for an attribute, remove the entire attribute, or do + * nothing and move on to the next opening tag. + * + * Example: + * + * if ( $tags->next_tag( array( 'class_name' => 'wp-group-block' ) ) ) { + * $tags->set_attribute( 'title', 'This groups the contained content.' ); + * $tags->remove_attribute( 'data-test-id' ); + * } + * + * If `set_attribute()` is called for an existing attribute it will + * overwrite the existing value. Similarly, calling `remove_attribute()` + * for a non-existing attribute has no effect on the document. Both + * of these methods are safe to call without knowing if a given attribute + * exists beforehand. + * + * ### Modifying CSS classes for a found tag + * + * The tag processor treats the `class` attribute as a special case. + * Because it's a common operation to add or remove CSS classes, this + * interface adds helper methods to make that easier. + * + * As with attribute values, adding or removing CSS classes is a safe + * operation that doesn't require checking if the attribute or class + * exists before making changes. If removing the only class then the + * entire `class` attribute will be removed. + * + * Example: + * + * // from `<span>Yippee!</span>` + * // to `<span class="is-active">Yippee!</span>` + * $tags->add_class( 'is-active' ); + * + * // from `<span class="excited">Yippee!</span>` + * // to `<span class="excited is-active">Yippee!</span>` + * $tags->add_class( 'is-active' ); + * + * // from `<span class="is-active heavy-accent">Yippee!</span>` + * // to `<span class="is-active heavy-accent">Yippee!</span>` + * $tags->add_class( 'is-active' ); + * + * // from `<input type="text" class="is-active rugby not-disabled" length="24">` + * // to `<input type="text" class="is-active not-disabled" length="24"> + * $tags->remove_class( 'rugby' ); + * + * // from `<input type="text" class="rugby" length="24">` + * // to `<input type="text" length="24"> + * $tags->remove_class( 'rugby' ); + * + * // from `<input type="text" length="24">` + * // to `<input type="text" length="24"> + * $tags->remove_class( 'rugby' ); + * + * When class changes are enqueued but a direct change to `class` is made via + * `set_attribute` then the changes to `set_attribute` (or `remove_attribute`) + * will take precedence over those made through `add_class` and `remove_class`. + * + * ### Bookmarks + * + * While scanning through the input HTMl document it's possible to set + * a named bookmark when a particular tag is found. Later on, after + * continuing to scan other tags, it's possible to `seek` to one of + * the set bookmarks and then proceed again from that point forward. + * + * Because bookmarks create processing overhead one should avoid + * creating too many of them. As a rule, create only bookmarks + * of known string literal names; avoid creating "mark_{$index}" + * and so on. It's fine from a performance standpoint to create a + * bookmark and update it frequently, such as within a loop. + * + * $total_todos = 0; + * while ( $p->next_tag( array( 'tag_name' => 'UL', 'class_name' => 'todo' ) ) ) { + * $p->set_bookmark( 'list-start' ); + * while ( $p->next_tag( array( 'tag_closers' => 'visit' ) ) ) { + * if ( 'UL' === $p->get_tag() && $p->is_tag_closer() ) { + * $p->set_bookmark( 'list-end' ); + * $p->seek( 'list-start' ); + * $p->set_attribute( 'data-contained-todos', (string) $total_todos ); + * $total_todos = 0; + * $p->seek( 'list-end' ); + * break; + * } + * + * if ( 'LI' === $p->get_tag() && ! $p->is_tag_closer() ) { + * $total_todos++; + * } + * } + * } + * + * ## Design and limitations + * + * The Tag Processor is designed to linearly scan HTML documents and tokenize + * HTML tags and their attributes. It's designed to do this as efficiently as + * possible without compromising parsing integrity. Therefore it will be + * slower than some methods of modifying HTML, such as those incorporating + * over-simplified PCRE patterns, but will not introduce the defects and + * failures that those methods bring in, which lead to broken page renders + * and often to security vulnerabilities. On the other hand, it will be faster + * than full-blown HTML parsers such as DOMDocument and use considerably + * less memory. It requires a negligible memory overhead, enough to consider + * it a zero-overhead system. + * + * The performance characteristics are maintained by avoiding tree construction + * and semantic cleanups which are specified in HTML5. Because of this, for + * example, it's not possible for the Tag Processor to associate any given + * opening tag with its corresponding closing tag, or to return the inner markup + * inside an element. Systems may be built on top of the Tag Processor to do + * this, but the Tag Processor is and should be constrained so it can remain an + * efficient, low-level, and reliable HTML scanner. + * + * The Tag Processor's design incorporates a "garbage-in-garbage-out" philosophy. + * HTML5 specifies that certain invalid content be transformed into different forms + * for display, such as removing null bytes from an input document and replacing + * invalid characters with the Unicode replacement character `U+FFFD` (visually "�"). + * Where errors or transformations exist within the HTML5 specification, the Tag Processor + * leaves those invalid inputs untouched, passing them through to the final browser + * to handle. While this implies that certain operations will be non-spec-compliant, + * such as reading the value of an attribute with invalid content, it also preserves a + * simplicity and efficiency for handling those error cases. + * + * Most operations within the Tag Processor are designed to minimize the difference + * between an input and output document for any given change. For example, the + * `add_class` and `remove_class` methods preserve whitespace and the class ordering + * within the `class` attribute; and when encountering tags with duplicated attributes, + * the Tag Processor will leave those invalid duplicate attributes where they are but + * update the proper attribute which the browser will read for parsing its value. An + * exception to this rule is that all attribute updates store their values as + * double-quoted strings, meaning that attributes on input with single-quoted or + * unquoted values will appear in the output with double-quotes. + * + * @since 6.2.0 + * @since 6.2.1 Fix: Support for various invalid comments; attribute updates are case-insensitive. + * @since 6.3.2 Fix: Skip HTML-like content inside rawtext elements such as STYLE. + */ +class Gutenberg_HTML_Tag_Processor_6_5 { + /** + * The maximum number of bookmarks allowed to exist at + * any given time. + * + * @since 6.2.0 + * @var int + * + * @see WP_HTML_Tag_Processor::set_bookmark() + */ + const MAX_BOOKMARKS = 10; + + /** + * Maximum number of times seek() can be called. + * Prevents accidental infinite loops. + * + * @since 6.2.0 + * @var int + * + * @see WP_HTML_Tag_Processor::seek() + */ + const MAX_SEEK_OPS = 1000; + + /** + * The HTML document to parse. + * + * @since 6.2.0 + * @var string + */ + protected $html; + + /** + * The last query passed to next_tag(). + * + * @since 6.2.0 + * @var array|null + */ + private $last_query; + + /** + * The tag name this processor currently scans for. + * + * @since 6.2.0 + * @var string|null + */ + private $sought_tag_name; + + /** + * The CSS class name this processor currently scans for. + * + * @since 6.2.0 + * @var string|null + */ + private $sought_class_name; + + /** + * The match offset this processor currently scans for. + * + * @since 6.2.0 + * @var int|null + */ + private $sought_match_offset; + + /** + * Whether to visit tag closers, e.g. </div>, when walking an input document. + * + * @since 6.2.0 + * @var bool + */ + private $stop_on_tag_closers; + + /** + * How many bytes from the original HTML document have been read and parsed. + * + * This value points to the latest byte offset in the input document which + * has been already parsed. It is the internal cursor for the Tag Processor + * and updates while scanning through the HTML tokens. + * + * @since 6.2.0 + * @var int + */ + private $bytes_already_parsed = 0; + + /** + * Byte offset in input document where current token starts. + * + * Example: + * + * <div id="test">... + * 01234 + * - token starts at 0 + * + * @since 6.5.0 + * + * @var int|null + */ + private $token_starts_at; + + /** + * Byte length of current token. + * + * Example: + * + * <div id="test">... + * 012345678901234 + * - token length is 14 - 0 = 14 + * + * a <!-- comment --> is a token. + * 0123456789 123456789 123456789 + * - token length is 17 - 2 = 15 + * + * @since 6.5.0 + * + * @var int|null + */ + private $token_length; + + /** + * Byte offset in input document where current tag name starts. + * + * Example: + * + * <div id="test">... + * 01234 + * - tag name starts at 1 + * + * @since 6.2.0 + * + * @var int|null + */ + private $tag_name_starts_at; + + /** + * Byte length of current tag name. + * + * Example: + * + * <div id="test">... + * 01234 + * --- tag name length is 3 + * + * @since 6.2.0 + * + * @var int|null + */ + private $tag_name_length; + + /** + * Whether the current tag is an opening tag, e.g. <div>, or a closing tag, e.g. </div>. + * + * @var bool + */ + private $is_closing_tag; + + /** + * Lazily-built index of attributes found within an HTML tag, keyed by the attribute name. + * + * Example: + * + * // Supposing the parser is working through this content + * // and stops after recognizing the `id` attribute. + * // <div id="test-4" class=outline title="data:text/plain;base64=asdk3nk1j3fo8"> + * // ^ parsing will continue from this point. + * $this->attributes = array( + * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ) + * ); + * + * // When picking up parsing again, or when asking to find the + * // `class` attribute we will continue and add to this array. + * $this->attributes = array( + * 'id' => new WP_HTML_Attribute_Token( 'id', 9, 6, 5, 11, false ), + * 'class' => new WP_HTML_Attribute_Token( 'class', 23, 7, 17, 13, false ) + * ); + * + * // Note that only the `class` attribute value is stored in the index. + * // That's because it is the only value used by this class at the moment. + * + * @since 6.2.0 + * @var WP_HTML_Attribute_Token[] + */ + private $attributes = array(); + + /** + * Tracks spans of duplicate attributes on a given tag, used for removing + * all copies of an attribute when calling `remove_attribute()`. + * + * @since 6.3.2 + * + * @var (WP_HTML_Span[])[]|null + */ + private $duplicate_attributes = null; + + /** + * Which class names to add or remove from a tag. + * + * These are tracked separately from attribute updates because they are + * semantically distinct, whereas this interface exists for the common + * case of adding and removing class names while other attributes are + * generally modified as with DOM `setAttribute` calls. + * + * When modifying an HTML document these will eventually be collapsed + * into a single `set_attribute( 'class', $changes )` call. + * + * Example: + * + * // Add the `wp-block-group` class, remove the `wp-group` class. + * $classname_updates = array( + * // Indexed by a comparable class name. + * 'wp-block-group' => WP_HTML_Tag_Processor::ADD_CLASS, + * 'wp-group' => WP_HTML_Tag_Processor::REMOVE_CLASS + * ); + * + * @since 6.2.0 + * @var bool[] + */ + private $classname_updates = array(); + + /** + * Tracks a semantic location in the original HTML which + * shifts with updates as they are applied to the document. + * + * @since 6.2.0 + * @var WP_HTML_Span[] + */ + protected $bookmarks = array(); + + const ADD_CLASS = true; + const REMOVE_CLASS = false; + const SKIP_CLASS = null; + + /** + * Lexical replacements to apply to input HTML document. + * + * "Lexical" in this class refers to the part of this class which + * operates on pure text _as text_ and not as HTML. There's a line + * between the public interface, with HTML-semantic methods like + * `set_attribute` and `add_class`, and an internal state that tracks + * text offsets in the input document. + * + * When higher-level HTML methods are called, those have to transform their + * operations (such as setting an attribute's value) into text diffing + * operations (such as replacing the sub-string from indices A to B with + * some given new string). These text-diffing operations are the lexical + * updates. + * + * As new higher-level methods are added they need to collapse their + * operations into these lower-level lexical updates since that's the + * Tag Processor's internal language of change. Any code which creates + * these lexical updates must ensure that they do not cross HTML syntax + * boundaries, however, so these should never be exposed outside of this + * class or any classes which intentionally expand its functionality. + * + * These are enqueued while editing the document instead of being immediately + * applied to avoid processing overhead, string allocations, and string + * copies when applying many updates to a single document. + * + * Example: + * + * // Replace an attribute stored with a new value, indices + * // sourced from the lazily-parsed HTML recognizer. + * $start = $attributes['src']->start; + * $length = $attributes['src']->length; + * $modifications[] = new WP_HTML_Text_Replacement( $start, $length, $new_value ); + * + * // Correspondingly, something like this will appear in this array. + * $lexical_updates = array( + * WP_HTML_Text_Replacement( 14, 28, 'https://my-site.my-domain/wp-content/uploads/2014/08/kittens.jpg' ) + * ); + * + * @since 6.2.0 + * @var WP_HTML_Text_Replacement[] + */ + protected $lexical_updates = array(); + + /** + * Tracks and limits `seek()` calls to prevent accidental infinite loops. + * + * @since 6.2.0 + * @var int + * + * @see WP_HTML_Tag_Processor::seek() + */ + protected $seek_count = 0; + + /** + * Constructor. + * + * @since 6.2.0 + * + * @param string $html HTML to process. + */ + public function __construct( $html ) { + $this->html = $html; + } + + /** + * Finds the next tag matching the $query. + * + * @since 6.2.0 + * + * @param array|string|null $query { + * Optional. Which tag name to find, having which class, etc. Default is to find any tag. + * + * @type string|null $tag_name Which tag to find, or `null` for "any tag." + * @type int|null $match_offset Find the Nth tag matching all search criteria. + * 1 for "first" tag, 3 for "third," etc. + * Defaults to first tag. + * @type string|null $class_name Tag must contain this whole class name to match. + * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g. </div>. + * } + * @return bool Whether a tag was matched. + */ + public function next_tag( $query = null ) { + $this->parse_query( $query ); + $already_found = 0; + + do { + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + // Find the next tag if it exists. + if ( false === $this->parse_next_tag() ) { + $this->bytes_already_parsed = strlen( $this->html ); + + return false; + } + + // Parse all of its attributes. + while ( $this->parse_next_attribute() ) { + continue; + } + + // Ensure that the tag closes before the end of the document. + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); + if ( false === $tag_ends_at ) { + return false; + } + $this->token_length = $tag_ends_at - $this->token_starts_at; + $this->bytes_already_parsed = $tag_ends_at; + + // Finally, check if the parsed tag and its attributes match the search query. + if ( $this->matches() ) { + ++$already_found; + } + + /* + * For non-DATA sections which might contain text that looks like HTML tags but + * isn't, scan with the appropriate alternative mode. Looking at the first letter + * of the tag name as a pre-check avoids a string allocation when it's not needed. + */ + $t = $this->html[ $this->tag_name_starts_at ]; + if ( + ! $this->is_closing_tag && + ( + 'i' === $t || 'I' === $t || + 'n' === $t || 'N' === $t || + 's' === $t || 'S' === $t || + 't' === $t || 'T' === $t + ) ) { + $tag_name = $this->get_tag(); + + if ( 'SCRIPT' === $tag_name && ! $this->skip_script_data() ) { + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } elseif ( + ( 'TEXTAREA' === $tag_name || 'TITLE' === $tag_name ) && + ! $this->skip_rcdata( $tag_name ) + ) { + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } elseif ( + ( + 'IFRAME' === $tag_name || + 'NOEMBED' === $tag_name || + 'NOFRAMES' === $tag_name || + 'NOSCRIPT' === $tag_name || + 'STYLE' === $tag_name + ) && + ! $this->skip_rawtext( $tag_name ) + ) { + /* + * "XMP" should be here too but its rules are more complicated and require the + * complexity of the HTML Processor (it needs to close out any open P element, + * meaning it can't be skipped here or else the HTML Processor will lose its + * place). For now, it can be ignored as it's a rare HTML tag in practice and + * any normative HTML should be using PRE instead. + */ + $this->bytes_already_parsed = strlen( $this->html ); + return false; + } + } + } while ( $already_found < $this->sought_match_offset ); + + return true; + } + + + /** + * Generator for a foreach loop to step through each class name for the matched tag. + * + * This generator function is designed to be used inside a "foreach" loop. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( "<div class='free &lt;egg&lt;\tlang-en'>" ); + * $p->next_tag(); + * foreach ( $p->class_list() as $class_name ) { + * echo "{$class_name} "; + * } + * // Outputs: "free <egg> lang-en " + * + * @since 6.4.0 + */ + public function class_list() { + /** @var string $class contains the string value of the class attribute, with character references decoded. */ + $class = $this->get_attribute( 'class' ); + + if ( ! is_string( $class ) ) { + return; + } + + $seen = array(); + + $at = 0; + while ( $at < strlen( $class ) ) { + // Skip past any initial boundary characters. + $at += strspn( $class, " \t\f\r\n", $at ); + if ( $at >= strlen( $class ) ) { + return; + } + + // Find the byte length until the next boundary. + $length = strcspn( $class, " \t\f\r\n", $at ); + if ( 0 === $length ) { + return; + } + + /* + * CSS class names are case-insensitive in the ASCII range. + * + * @see https://www.w3.org/TR/CSS2/syndata.html#x1 + */ + $name = strtolower( substr( $class, $at, $length ) ); + $at += $length; + + /* + * It's expected that the number of class names for a given tag is relatively small. + * Given this, it is probably faster overall to scan an array for a value rather + * than to use the class name as a key and check if it's a key of $seen. + */ + if ( in_array( $name, $seen, true ) ) { + continue; + } + + $seen[] = $name; + yield $name; + } + } + + + /** + * Returns if a matched tag contains the given ASCII case-insensitive class name. + * + * @since 6.4.0 + * + * @param string $wanted_class Look for this CSS class name, ASCII case-insensitive. + * @return bool|null Whether the matched tag contains the given class name, or null if not matched. + */ + public function has_class( $wanted_class ) { + if ( ! $this->tag_name_starts_at ) { + return null; + } + + $wanted_class = strtolower( $wanted_class ); + + foreach ( $this->class_list() as $class_name ) { + if ( $class_name === $wanted_class ) { + return true; + } + } + + return false; + } + + + /** + * Sets a bookmark in the HTML document. + * + * Bookmarks represent specific places or tokens in the HTML + * document, such as a tag opener or closer. When applying + * edits to a document, such as setting an attribute, the + * text offsets of that token may shift; the bookmark is + * kept updated with those shifts and remains stable unless + * the entire span of text in which the token sits is removed. + * + * Release bookmarks when they are no longer needed. + * + * Example: + * + * <main><h2>Surprising fact you may not know!</h2></main> + * ^ ^ + * \-|-- this `H2` opener bookmark tracks the token + * + * <main class="clickbait"><h2>Surprising fact you may no… + * ^ ^ + * \-|-- it shifts with edits + * + * Bookmarks provide the ability to seek to a previously-scanned + * place in the HTML document. This avoids the need to re-scan + * the entire document. + * + * Example: + * + * <ul><li>One</li><li>Two</li><li>Three</li></ul> + * ^^^^ + * want to note this last item + * + * $p = new WP_HTML_Tag_Processor( $html ); + * $in_list = false; + * while ( $p->next_tag( array( 'tag_closers' => $in_list ? 'visit' : 'skip' ) ) ) { + * if ( 'UL' === $p->get_tag() ) { + * if ( $p->is_tag_closer() ) { + * $in_list = false; + * $p->set_bookmark( 'resume' ); + * if ( $p->seek( 'last-li' ) ) { + * $p->add_class( 'last-li' ); + * } + * $p->seek( 'resume' ); + * $p->release_bookmark( 'last-li' ); + * $p->release_bookmark( 'resume' ); + * } else { + * $in_list = true; + * } + * } + * + * if ( 'LI' === $p->get_tag() ) { + * $p->set_bookmark( 'last-li' ); + * } + * } + * + * Bookmarks intentionally hide the internal string offsets + * to which they refer. They are maintained internally as + * updates are applied to the HTML document and therefore + * retain their "position" - the location to which they + * originally pointed. The inability to use bookmarks with + * functions like `substr` is therefore intentional to guard + * against accidentally breaking the HTML. + * + * Because bookmarks allocate memory and require processing + * for every applied update, they are limited and require + * a name. They should not be created with programmatically-made + * names, such as "li_{$index}" with some loop. As a general + * rule they should only be created with string-literal names + * like "start-of-section" or "last-paragraph". + * + * Bookmarks are a powerful tool to enable complicated behavior. + * Consider double-checking that you need this tool if you are + * reaching for it, as inappropriate use could lead to broken + * HTML structure or unwanted processing overhead. + * + * @since 6.2.0 + * + * @param string $name Identifies this particular bookmark. + * @return bool Whether the bookmark was successfully created. + */ + public function set_bookmark( $name ) { + if ( null === $this->tag_name_starts_at ) { + return false; + } + + if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) >= static::MAX_BOOKMARKS ) { + _doing_it_wrong( + __METHOD__, + __( 'Too many bookmarks: cannot create any more.' ), + '6.2.0' + ); + return false; + } + + $this->bookmarks[ $name ] = new Gutenberg_HTML_Span_6_5( $this->token_starts_at, $this->token_length ); + + return true; + } + + + /** + * Removes a bookmark that is no longer needed. + * + * Releasing a bookmark frees up the small + * performance overhead it requires. + * + * @param string $name Name of the bookmark to remove. + * @return bool Whether the bookmark already existed before removal. + */ + public function release_bookmark( $name ) { + if ( ! array_key_exists( $name, $this->bookmarks ) ) { + return false; + } + + unset( $this->bookmarks[ $name ] ); + + return true; + } + + /** + * Skips contents of generic rawtext elements. + * + * @since 6.3.2 + * + * @see https://html.spec.whatwg.org/#generic-raw-text-element-parsing-algorithm + * + * @param string $tag_name The uppercase tag name which will close the RAWTEXT region. + * @return bool Whether an end to the RAWTEXT region was found before the end of the document. + */ + private function skip_rawtext( $tag_name ) { + /* + * These two functions distinguish themselves on whether character references are + * decoded, and since functionality to read the inner markup isn't supported, it's + * not necessary to implement these two functions separately. + */ + return $this->skip_rcdata( $tag_name ); + } + + /** + * Skips contents of RCDATA elements, namely title and textarea tags. + * + * @since 6.2.0 + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#rcdata-state + * + * @param string $tag_name The uppercase tag name which will close the RCDATA region. + * @return bool Whether an end to the RCDATA region was found before the end of the document. + */ + private function skip_rcdata( $tag_name ) { + $html = $this->html; + $doc_length = strlen( $html ); + $tag_length = strlen( $tag_name ); + + $at = $this->bytes_already_parsed; + + while ( false !== $at && $at < $doc_length ) { + $at = strpos( $this->html, '</', $at ); + + // Fail if there is no possible tag closer. + if ( false === $at || ( $at + $tag_length ) >= $doc_length ) { + $this->bytes_already_parsed = $doc_length; + return false; + } + + $closer_potentially_starts_at = $at; + $at += 2; + + /* + * Find a case-insensitive match to the tag name. + * + * Because tag names are limited to US-ASCII there is no + * need to perform any kind of Unicode normalization when + * comparing; any character which could be impacted by such + * normalization could not be part of a tag name. + */ + for ( $i = 0; $i < $tag_length; $i++ ) { + $tag_char = $tag_name[ $i ]; + $html_char = $html[ $at + $i ]; + + if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { + $at += $i; + continue 2; + } + } + + $at += $tag_length; + $this->bytes_already_parsed = $at; + + /* + * Ensure that the tag name terminates to avoid matching on + * substrings of a longer tag name. For example, the sequence + * "</textarearug" should not match for "</textarea" even + * though "textarea" is found within the text. + */ + $c = $html[ $at ]; + if ( ' ' !== $c && "\t" !== $c && "\r" !== $c && "\n" !== $c && '/' !== $c && '>' !== $c ) { + continue; + } + + while ( $this->parse_next_attribute() ) { + continue; + } + $at = $this->bytes_already_parsed; + if ( $at >= strlen( $this->html ) ) { + return false; + } + + if ( '>' === $html[ $at ] || '/' === $html[ $at ] ) { + $this->bytes_already_parsed = $closer_potentially_starts_at; + return true; + } + } + + return false; + } + + /** + * Skips contents of script tags. + * + * @since 6.2.0 + * + * @return bool Whether the script tag was closed before the end of the document. + */ + private function skip_script_data() { + $state = 'unescaped'; + $html = $this->html; + $doc_length = strlen( $html ); + $at = $this->bytes_already_parsed; + + while ( false !== $at && $at < $doc_length ) { + $at += strcspn( $html, '-<', $at ); + + /* + * For all script states a "-->" transitions + * back into the normal unescaped script mode, + * even if that's the current state. + */ + if ( + $at + 2 < $doc_length && + '-' === $html[ $at ] && + '-' === $html[ $at + 1 ] && + '>' === $html[ $at + 2 ] + ) { + $at += 3; + $state = 'unescaped'; + continue; + } + + // Everything of interest past here starts with "<". + if ( $at + 1 >= $doc_length || '<' !== $html[ $at++ ] ) { + continue; + } + + /* + * Unlike with "-->", the "<!--" only transitions + * into the escaped mode if not already there. + * + * Inside the escaped modes it will be ignored; and + * should never break out of the double-escaped + * mode and back into the escaped mode. + * + * While this requires a mode change, it does not + * impact the parsing otherwise, so continue + * parsing after updating the state. + */ + if ( + $at + 2 < $doc_length && + '!' === $html[ $at ] && + '-' === $html[ $at + 1 ] && + '-' === $html[ $at + 2 ] + ) { + $at += 3; + $state = 'unescaped' === $state ? 'escaped' : $state; + continue; + } + + if ( '/' === $html[ $at ] ) { + $closer_potentially_starts_at = $at - 1; + $is_closing = true; + ++$at; + } else { + $is_closing = false; + } + + /* + * At this point the only remaining state-changes occur with the + * <script> and </script> tags; unless one of these appears next, + * proceed scanning to the next potential token in the text. + */ + if ( ! ( + $at + 6 < $doc_length && + ( 's' === $html[ $at ] || 'S' === $html[ $at ] ) && + ( 'c' === $html[ $at + 1 ] || 'C' === $html[ $at + 1 ] ) && + ( 'r' === $html[ $at + 2 ] || 'R' === $html[ $at + 2 ] ) && + ( 'i' === $html[ $at + 3 ] || 'I' === $html[ $at + 3 ] ) && + ( 'p' === $html[ $at + 4 ] || 'P' === $html[ $at + 4 ] ) && + ( 't' === $html[ $at + 5 ] || 'T' === $html[ $at + 5 ] ) + ) ) { + ++$at; + continue; + } + + /* + * Ensure that the script tag terminates to avoid matching on + * substrings of a non-match. For example, the sequence + * "<script123" should not end a script region even though + * "<script" is found within the text. + */ + if ( $at + 6 >= $doc_length ) { + continue; + } + $at += 6; + $c = $html[ $at ]; + if ( ' ' !== $c && "\t" !== $c && "\r" !== $c && "\n" !== $c && '/' !== $c && '>' !== $c ) { + ++$at; + continue; + } + + if ( 'escaped' === $state && ! $is_closing ) { + $state = 'double-escaped'; + continue; + } + + if ( 'double-escaped' === $state && $is_closing ) { + $state = 'escaped'; + continue; + } + + if ( $is_closing ) { + $this->bytes_already_parsed = $closer_potentially_starts_at; + if ( $this->bytes_already_parsed >= $doc_length ) { + return false; + } + + while ( $this->parse_next_attribute() ) { + continue; + } + + if ( '>' === $html[ $this->bytes_already_parsed ] ) { + $this->bytes_already_parsed = $closer_potentially_starts_at; + return true; + } + } + + ++$at; + } + + return false; + } + + /** + * Parses the next tag. + * + * This will find and start parsing the next tag, including + * the opening `<`, the potential closer `/`, and the tag + * name. It does not parse the attributes or scan to the + * closing `>`; these are left for other methods. + * + * @since 6.2.0 + * @since 6.2.1 Support abruptly-closed comments, invalid-tag-closer-comments, and empty elements. + * + * @return bool Whether a tag was found before the end of the document. + */ + private function parse_next_tag() { + $this->after_tag(); + + $html = $this->html; + $doc_length = strlen( $html ); + $at = $this->bytes_already_parsed; + + while ( false !== $at && $at < $doc_length ) { + $at = strpos( $html, '<', $at ); + if ( false === $at ) { + return false; + } + + $this->token_starts_at = $at; + + if ( '/' === $this->html[ $at + 1 ] ) { + $this->is_closing_tag = true; + ++$at; + } else { + $this->is_closing_tag = false; + } + + /* + * HTML tag names must start with [a-zA-Z] otherwise they are not tags. + * For example, "<3" is rendered as text, not a tag opener. If at least + * one letter follows the "<" then _it is_ a tag, but if the following + * character is anything else it _is not a tag_. + * + * It's not uncommon to find non-tags starting with `<` in an HTML + * document, so it's good for performance to make this pre-check before + * continuing to attempt to parse a tag name. + * + * Reference: + * * https://html.spec.whatwg.org/multipage/parsing.html#data-state + * * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + $tag_name_prefix_length = strspn( $html, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ', $at + 1 ); + if ( $tag_name_prefix_length > 0 ) { + ++$at; + $this->tag_name_length = $tag_name_prefix_length + strcspn( $html, " \t\f\r\n/>", $at + $tag_name_prefix_length ); + $this->tag_name_starts_at = $at; + $this->bytes_already_parsed = $at + $this->tag_name_length; + return true; + } + + /* + * Abort if no tag is found before the end of + * the document. There is nothing left to parse. + */ + if ( $at + 1 >= strlen( $html ) ) { + return false; + } + + /* + * <! transitions to markup declaration open state + * https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state + */ + if ( '!' === $html[ $at + 1 ] ) { + /* + * <!-- transitions to a bogus comment state – skip to the nearest --> + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 3 && + '-' === $html[ $at + 2 ] && + '-' === $html[ $at + 3 ] + ) { + $closer_at = $at + 4; + // If it's not possible to close the comment then there is nothing more to scan. + if ( strlen( $html ) <= $closer_at ) { + return false; + } + + // Abruptly-closed empty comments are a sequence of dashes followed by `>`. + $span_of_dashes = strspn( $html, '-', $closer_at ); + if ( '>' === $html[ $closer_at + $span_of_dashes ] ) { + $at = $closer_at + $span_of_dashes + 1; + continue; + } + + /* + * Comments may be closed by either a --> or an invalid --!>. + * The first occurrence closes the comment. + * + * See https://html.spec.whatwg.org/#parse-error-incorrectly-closed-comment + */ + --$closer_at; // Pre-increment inside condition below reduces risk of accidental infinite looping. + while ( ++$closer_at < strlen( $html ) ) { + $closer_at = strpos( $html, '--', $closer_at ); + if ( false === $closer_at ) { + return false; + } + + if ( $closer_at + 2 < strlen( $html ) && '>' === $html[ $closer_at + 2 ] ) { + $at = $closer_at + 3; + continue 2; + } + + if ( $closer_at + 3 < strlen( $html ) && '!' === $html[ $closer_at + 2 ] && '>' === $html[ $closer_at + 3 ] ) { + $at = $closer_at + 4; + continue 2; + } + } + } + + /* + * <![CDATA[ transitions to CDATA section state – skip to the nearest ]]> + * The CDATA is case-sensitive. + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 8 && + '[' === $html[ $at + 2 ] && + 'C' === $html[ $at + 3 ] && + 'D' === $html[ $at + 4 ] && + 'A' === $html[ $at + 5 ] && + 'T' === $html[ $at + 6 ] && + 'A' === $html[ $at + 7 ] && + '[' === $html[ $at + 8 ] + ) { + $closer_at = strpos( $html, ']]>', $at + 9 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 3; + continue; + } + + /* + * <!DOCTYPE transitions to DOCTYPE state – skip to the nearest > + * These are ASCII-case-insensitive. + * https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( + strlen( $html ) > $at + 8 && + ( 'D' === $html[ $at + 2 ] || 'd' === $html[ $at + 2 ] ) && + ( 'O' === $html[ $at + 3 ] || 'o' === $html[ $at + 3 ] ) && + ( 'C' === $html[ $at + 4 ] || 'c' === $html[ $at + 4 ] ) && + ( 'T' === $html[ $at + 5 ] || 't' === $html[ $at + 5 ] ) && + ( 'Y' === $html[ $at + 6 ] || 'y' === $html[ $at + 6 ] ) && + ( 'P' === $html[ $at + 7 ] || 'p' === $html[ $at + 7 ] ) && + ( 'E' === $html[ $at + 8 ] || 'e' === $html[ $at + 8 ] ) + ) { + $closer_at = strpos( $html, '>', $at + 9 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + /* + * Anything else here is an incorrectly-opened comment and transitions + * to the bogus comment state - skip to the nearest >. + */ + $at = strpos( $html, '>', $at + 1 ); + continue; + } + + /* + * </> is a missing end tag name, which is ignored. + * + * See https://html.spec.whatwg.org/#parse-error-missing-end-tag-name + */ + if ( '>' === $html[ $at + 1 ] ) { + ++$at; + continue; + } + + /* + * <? transitions to a bogus comment state – skip to the nearest > + * See https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state + */ + if ( '?' === $html[ $at + 1 ] ) { + $closer_at = strpos( $html, '>', $at + 2 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + /* + * If a non-alpha starts the tag name in a tag closer it's a comment. + * Find the first `>`, which closes the comment. + * + * See https://html.spec.whatwg.org/#parse-error-invalid-first-character-of-tag-name + */ + if ( $this->is_closing_tag ) { + $closer_at = strpos( $html, '>', $at + 3 ); + if ( false === $closer_at ) { + return false; + } + + $at = $closer_at + 1; + continue; + } + + ++$at; + } + + return false; + } + + /** + * Parses the next attribute. + * + * @since 6.2.0 + * + * @return bool Whether an attribute was found before the end of the document. + */ + private function parse_next_attribute() { + // Skip whitespace and slashes. + $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n/", $this->bytes_already_parsed ); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + /* + * Treat the equal sign as a part of the attribute + * name if it is the first encountered byte. + * + * @see https://html.spec.whatwg.org/multipage/parsing.html#before-attribute-name-state + */ + $name_length = '=' === $this->html[ $this->bytes_already_parsed ] + ? 1 + strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed + 1 ) + : strcspn( $this->html, "=/> \t\f\r\n", $this->bytes_already_parsed ); + + // No attribute, just tag closer. + if ( 0 === $name_length || $this->bytes_already_parsed + $name_length >= strlen( $this->html ) ) { + return false; + } + + $attribute_start = $this->bytes_already_parsed; + $attribute_name = substr( $this->html, $attribute_start, $name_length ); + $this->bytes_already_parsed += $name_length; + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $this->skip_whitespace(); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + $has_value = '=' === $this->html[ $this->bytes_already_parsed ]; + if ( $has_value ) { + ++$this->bytes_already_parsed; + $this->skip_whitespace(); + if ( $this->bytes_already_parsed >= strlen( $this->html ) ) { + return false; + } + + switch ( $this->html[ $this->bytes_already_parsed ] ) { + case "'": + case '"': + $quote = $this->html[ $this->bytes_already_parsed ]; + $value_start = $this->bytes_already_parsed + 1; + $value_length = strcspn( $this->html, $quote, $value_start ); + $attribute_end = $value_start + $value_length + 1; + $this->bytes_already_parsed = $attribute_end; + break; + + default: + $value_start = $this->bytes_already_parsed; + $value_length = strcspn( $this->html, "> \t\f\r\n", $value_start ); + $attribute_end = $value_start + $value_length; + $this->bytes_already_parsed = $attribute_end; + } + } else { + $value_start = $this->bytes_already_parsed; + $value_length = 0; + $attribute_end = $attribute_start + $name_length; + } + + if ( $attribute_end >= strlen( $this->html ) ) { + return false; + } + + if ( $this->is_closing_tag ) { + return true; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $comparable_name = strtolower( $attribute_name ); + + // If an attribute is listed many times, only use the first declaration and ignore the rest. + if ( ! array_key_exists( $comparable_name, $this->attributes ) ) { + $this->attributes[ $comparable_name ] = new Gutenberg_HTML_Attribute_Token_6_5( + $attribute_name, + $value_start, + $value_length, + $attribute_start, + $attribute_end - $attribute_start, + ! $has_value + ); + + return true; + } + + /* + * Track the duplicate attributes so if we remove it, all disappear together. + * + * While `$this->duplicated_attributes` could always be stored as an `array()`, + * which would simplify the logic here, storing a `null` and only allocating + * an array when encountering duplicates avoids needless allocations in the + * normative case of parsing tags with no duplicate attributes. + */ + $duplicate_span = new Gutenberg_HTML_Span_6_5( $attribute_start, $attribute_end - $attribute_start ); + if ( null === $this->duplicate_attributes ) { + $this->duplicate_attributes = array( $comparable_name => array( $duplicate_span ) ); + } elseif ( ! array_key_exists( $comparable_name, $this->duplicate_attributes ) ) { + $this->duplicate_attributes[ $comparable_name ] = array( $duplicate_span ); + } else { + $this->duplicate_attributes[ $comparable_name ][] = $duplicate_span; + } + + return true; + } + + /** + * Move the internal cursor past any immediate successive whitespace. + * + * @since 6.2.0 + */ + private function skip_whitespace() { + $this->bytes_already_parsed += strspn( $this->html, " \t\f\r\n", $this->bytes_already_parsed ); + } + + /** + * Applies attribute updates and cleans up once a tag is fully parsed. + * + * @since 6.2.0 + */ + private function after_tag() { + $this->get_updated_html(); + $this->token_starts_at = null; + $this->token_length = null; + $this->tag_name_starts_at = null; + $this->tag_name_length = null; + $this->is_closing_tag = null; + $this->attributes = array(); + $this->duplicate_attributes = null; + } + + /** + * Converts class name updates into tag attributes updates + * (they are accumulated in different data formats for performance). + * + * @since 6.2.0 + * + * @see WP_HTML_Tag_Processor::$lexical_updates + * @see WP_HTML_Tag_Processor::$classname_updates + */ + private function class_name_updates_to_attributes_updates() { + if ( count( $this->classname_updates ) === 0 ) { + return; + } + + $existing_class = $this->get_enqueued_attribute_value( 'class' ); + if ( null === $existing_class || true === $existing_class ) { + $existing_class = ''; + } + + if ( false === $existing_class && isset( $this->attributes['class'] ) ) { + $existing_class = substr( + $this->html, + $this->attributes['class']->value_starts_at, + $this->attributes['class']->value_length + ); + } + + if ( false === $existing_class ) { + $existing_class = ''; + } + + /** + * Updated "class" attribute value. + * + * This is incrementally built while scanning through the existing class + * attribute, skipping removed classes on the way, and then appending + * added classes at the end. Only when finished processing will the + * value contain the final new value. + + * @var string $class + */ + $class = ''; + + /** + * Tracks the cursor position in the existing + * class attribute value while parsing. + * + * @var int $at + */ + $at = 0; + + /** + * Indicates if there's any need to modify the existing class attribute. + * + * If a call to `add_class()` and `remove_class()` wouldn't impact + * the `class` attribute value then there's no need to rebuild it. + * For example, when adding a class that's already present or + * removing one that isn't. + * + * This flag enables a performance optimization when none of the enqueued + * class updates would impact the `class` attribute; namely, that the + * processor can continue without modifying the input document, as if + * none of the `add_class()` or `remove_class()` calls had been made. + * + * This flag is set upon the first change that requires a string update. + * + * @var bool $modified + */ + $modified = false; + + // Remove unwanted classes by only copying the new ones. + $existing_class_length = strlen( $existing_class ); + while ( $at < $existing_class_length ) { + // Skip to the first non-whitespace character. + $ws_at = $at; + $ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at ); + $at += $ws_length; + + // Capture the class name – it's everything until the next whitespace. + $name_length = strcspn( $existing_class, " \t\f\r\n", $at ); + if ( 0 === $name_length ) { + // If no more class names are found then that's the end. + break; + } + + $name = substr( $existing_class, $at, $name_length ); + $at += $name_length; + + // If this class is marked for removal, start processing the next one. + $remove_class = ( + isset( $this->classname_updates[ $name ] ) && + self::REMOVE_CLASS === $this->classname_updates[ $name ] + ); + + // If a class has already been seen then skip it; it should not be added twice. + if ( ! $remove_class ) { + $this->classname_updates[ $name ] = self::SKIP_CLASS; + } + + if ( $remove_class ) { + $modified = true; + continue; + } + + /* + * Otherwise, append it to the new "class" attribute value. + * + * There are options for handling whitespace between tags. + * Preserving the existing whitespace produces fewer changes + * to the HTML content and should clarify the before/after + * content when debugging the modified output. + * + * This approach contrasts normalizing the inter-class + * whitespace to a single space, which might appear cleaner + * in the output HTML but produce a noisier change. + */ + $class .= substr( $existing_class, $ws_at, $ws_length ); + $class .= $name; + } + + // Add new classes by appending those which haven't already been seen. + foreach ( $this->classname_updates as $name => $operation ) { + if ( self::ADD_CLASS === $operation ) { + $modified = true; + + $class .= strlen( $class ) > 0 ? ' ' : ''; + $class .= $name; + } + } + + $this->classname_updates = array(); + if ( ! $modified ) { + return; + } + + if ( strlen( $class ) > 0 ) { + $this->set_attribute( 'class', $class ); + } else { + $this->remove_attribute( 'class' ); + } + } + + /** + * Applies attribute updates to HTML document. + * + * @since 6.2.0 + * @since 6.2.1 Accumulates shift for internal cursor and passed pointer. + * @since 6.3.0 Invalidate any bookmarks whose targets are overwritten. + * + * @param int $shift_this_point Accumulate and return shift for this position. + * @return int How many bytes the given pointer moved in response to the updates. + */ + private function apply_attributes_updates( $shift_this_point = 0 ) { + if ( ! count( $this->lexical_updates ) ) { + return 0; + } + + $accumulated_shift_for_given_point = 0; + + /* + * Attribute updates can be enqueued in any order but updates + * to the document must occur in lexical order; that is, each + * replacement must be made before all others which follow it + * at later string indices in the input document. + * + * Sorting avoid making out-of-order replacements which + * can lead to mangled output, partially-duplicated + * attributes, and overwritten attributes. + */ + usort( $this->lexical_updates, array( self::class, 'sort_start_ascending' ) ); + + $bytes_already_copied = 0; + $output_buffer = ''; + foreach ( $this->lexical_updates as $diff ) { + $shift = strlen( $diff->text ) - $diff->length; + + // Adjust the cursor position by however much an update affects it. + if ( $diff->start <= $this->bytes_already_parsed ) { + $this->bytes_already_parsed += $shift; + } + + // Accumulate shift of the given pointer within this function call. + if ( $diff->start <= $shift_this_point ) { + $accumulated_shift_for_given_point += $shift; + } + + $output_buffer .= substr( $this->html, $bytes_already_copied, $diff->start - $bytes_already_copied ); + $output_buffer .= $diff->text; + $bytes_already_copied = $diff->start + $diff->length; + } + + $this->html = $output_buffer . substr( $this->html, $bytes_already_copied ); + + /* + * Adjust bookmark locations to account for how the text + * replacements adjust offsets in the input document. + */ + foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { + $bookmark_end = $bookmark->start + $bookmark->length; + + /* + * Each lexical update which appears before the bookmark's endpoints + * might shift the offsets for those endpoints. Loop through each change + * and accumulate the total shift for each bookmark, then apply that + * shift after tallying the full delta. + */ + $head_delta = 0; + $tail_delta = 0; + + foreach ( $this->lexical_updates as $diff ) { + $diff_end = $diff->start + $diff->length; + + if ( $bookmark->start < $diff->start && $bookmark_end < $diff->start ) { + break; + } + + if ( $bookmark->start >= $diff->start && $bookmark_end < $diff_end ) { + $this->release_bookmark( $bookmark_name ); + continue 2; + } + + $delta = strlen( $diff->text ) - $diff->length; + + if ( $bookmark->start >= $diff->start ) { + $head_delta += $delta; + } + + if ( $bookmark_end >= $diff_end ) { + $tail_delta += $delta; + } + } + + $bookmark->start += $head_delta; + $bookmark->length += $tail_delta - $head_delta; + } + + $this->lexical_updates = array(); + + return $accumulated_shift_for_given_point; + } + + /** + * Checks whether a bookmark with the given name exists. + * + * @since 6.3.0 + * + * @param string $bookmark_name Name to identify a bookmark that potentially exists. + * @return bool Whether that bookmark exists. + */ + public function has_bookmark( $bookmark_name ) { + return array_key_exists( $bookmark_name, $this->bookmarks ); + } + + /** + * Move the internal cursor in the Tag Processor to a given bookmark's location. + * + * In order to prevent accidental infinite loops, there's a + * maximum limit on the number of times seek() can be called. + * + * @since 6.2.0 + * + * @param string $bookmark_name Jump to the place in the document identified by this bookmark name. + * @return bool Whether the internal cursor was successfully moved to the bookmark's location. + */ + public function seek( $bookmark_name ) { + if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Unknown bookmark name.' ), + '6.2.0' + ); + return false; + } + + if ( ++$this->seek_count > static::MAX_SEEK_OPS ) { + _doing_it_wrong( + __METHOD__, + __( 'Too many calls to seek() - this can lead to performance issues.' ), + '6.2.0' + ); + return false; + } + + // Flush out any pending updates to the document. + $this->get_updated_html(); + + // Point this tag processor before the sought tag opener and consume it. + $this->bytes_already_parsed = $this->bookmarks[ $bookmark_name ]->start; + return $this->next_tag( array( 'tag_closers' => 'visit' ) ); + } + + /** + * Compare two WP_HTML_Text_Replacement objects. + * + * @since 6.2.0 + * + * @param WP_HTML_Text_Replacement $a First attribute update. + * @param WP_HTML_Text_Replacement $b Second attribute update. + * @return int Comparison value for string order. + */ + private static function sort_start_ascending( $a, $b ) { + $by_start = $a->start - $b->start; + if ( 0 !== $by_start ) { + return $by_start; + } + + $by_text = isset( $a->text, $b->text ) ? strcmp( $a->text, $b->text ) : 0; + if ( 0 !== $by_text ) { + return $by_text; + } + + /* + * This code should be unreachable, because it implies the two replacements + * start at the same location and contain the same text. + */ + return $a->length - $b->length; + } + + /** + * Return the enqueued value for a given attribute, if one exists. + * + * Enqueued updates can take different data types: + * - If an update is enqueued and is boolean, the return will be `true` + * - If an update is otherwise enqueued, the return will be the string value of that update. + * - If an attribute is enqueued to be removed, the return will be `null` to indicate that. + * - If no updates are enqueued, the return will be `false` to differentiate from "removed." + * + * @since 6.2.0 + * + * @param string $comparable_name The attribute name in its comparable form. + * @return string|boolean|null Value of enqueued update if present, otherwise false. + */ + private function get_enqueued_attribute_value( $comparable_name ) { + if ( ! isset( $this->lexical_updates[ $comparable_name ] ) ) { + return false; + } + + $enqueued_text = $this->lexical_updates[ $comparable_name ]->text; + + // Removed attributes erase the entire span. + if ( '' === $enqueued_text ) { + return null; + } + + /* + * Boolean attribute updates are just the attribute name without a corresponding value. + * + * This value might differ from the given comparable name in that there could be leading + * or trailing whitespace, and that the casing follows the name given in `set_attribute`. + * + * Example: + * + * $p->set_attribute( 'data-TEST-id', 'update' ); + * 'update' === $p->get_enqueued_attribute_value( 'data-test-id' ); + * + * Detect this difference based on the absence of the `=`, which _must_ exist in any + * attribute containing a value, e.g. `<input type="text" enabled />`. + * ¹ ² + * 1. Attribute with a string value. + * 2. Boolean attribute whose value is `true`. + */ + $equals_at = strpos( $enqueued_text, '=' ); + if ( false === $equals_at ) { + return true; + } + + /* + * Finally, a normal update's value will appear after the `=` and + * be double-quoted, as performed incidentally by `set_attribute`. + * + * e.g. `type="text"` + * ¹² ³ + * 1. Equals is here. + * 2. Double-quoting starts one after the equals sign. + * 3. Double-quoting ends at the last character in the update. + */ + $enqueued_value = substr( $enqueued_text, $equals_at + 2, -1 ); + return html_entity_decode( $enqueued_value ); + } + + /** + * Returns the value of a requested attribute from a matched tag opener if that attribute exists. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '<div enabled class="test" data-test-id="14">Test</div>' ); + * $p->next_tag( array( 'class_name' => 'test' ) ) === true; + * $p->get_attribute( 'data-test-id' ) === '14'; + * $p->get_attribute( 'enabled' ) === true; + * $p->get_attribute( 'aria-label' ) === null; + * + * $p->next_tag() === false; + * $p->get_attribute( 'class' ) === null; + * + * @since 6.2.0 + * + * @param string $name Name of attribute whose value is requested. + * @return string|true|null Value of attribute or `null` if not available. Boolean attributes return `true`. + */ + public function get_attribute( $name ) { + if ( null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $name ); + + /* + * For every attribute other than `class` it's possible to perform a quick check if + * there's an enqueued lexical update whose value takes priority over what's found in + * the input document. + * + * The `class` attribute is special though because of the exposed helpers `add_class` + * and `remove_class`. These form a builder for the `class` attribute, so an additional + * check for enqueued class changes is required in addition to the check for any enqueued + * attribute values. If any exist, those enqueued class changes must first be flushed out + * into an attribute value update. + */ + if ( 'class' === $name ) { + $this->class_name_updates_to_attributes_updates(); + } + + // Return any enqueued attribute value updates if they exist. + $enqueued_value = $this->get_enqueued_attribute_value( $comparable ); + if ( false !== $enqueued_value ) { + return $enqueued_value; + } + + if ( ! isset( $this->attributes[ $comparable ] ) ) { + return null; + } + + $attribute = $this->attributes[ $comparable ]; + + /* + * This flag distinguishes an attribute with no value + * from an attribute with an empty string value. For + * unquoted attributes this could look very similar. + * It refers to whether an `=` follows the name. + * + * e.g. <div boolean-attribute empty-attribute=></div> + * ¹ ² + * 1. Attribute `boolean-attribute` is `true`. + * 2. Attribute `empty-attribute` is `""`. + */ + if ( true === $attribute->is_true ) { + return true; + } + + $raw_value = substr( $this->html, $attribute->value_starts_at, $attribute->value_length ); + + return html_entity_decode( $raw_value ); + } + + /** + * Gets lowercase names of all attributes matching a given prefix in the current tag. + * + * Note that matching is case-insensitive. This is in accordance with the spec: + * + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '<div data-ENABLED class="test" DATA-test-id="14">Test</div>' ); + * $p->next_tag( array( 'class_name' => 'test' ) ) === true; + * $p->get_attribute_names_with_prefix( 'data-' ) === array( 'data-enabled', 'data-test-id' ); + * + * $p->next_tag() === false; + * $p->get_attribute_names_with_prefix( 'data-' ) === null; + * + * @since 6.2.0 + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + * + * @param string $prefix Prefix of requested attribute names. + * @return array|null List of attribute names, or `null` when no tag opener is matched. + */ + public function get_attribute_names_with_prefix( $prefix ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return null; + } + + $comparable = strtolower( $prefix ); + + $matches = array(); + foreach ( array_keys( $this->attributes ) as $attr_name ) { + if ( str_starts_with( $attr_name, $comparable ) ) { + $matches[] = $attr_name; + } + } + return $matches; + } + + /** + * Returns the uppercase name of the matched tag. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '<div class="test">Test</div>' ); + * $p->next_tag() === true; + * $p->get_tag() === 'DIV'; + * + * $p->next_tag() === false; + * $p->get_tag() === null; + * + * @since 6.2.0 + * + * @return string|null Name of currently matched tag in input HTML, or `null` if none found. + */ + public function get_tag() { + if ( null === $this->tag_name_starts_at ) { + return null; + } + + $tag_name = substr( $this->html, $this->tag_name_starts_at, $this->tag_name_length ); + + return strtoupper( $tag_name ); + } + + /** + * Indicates if the currently matched tag contains the self-closing flag. + * + * No HTML elements ought to have the self-closing flag and for those, the self-closing + * flag will be ignored. For void elements this is benign because they "self close" + * automatically. For non-void HTML elements though problems will appear if someone + * intends to use a self-closing element in place of that element with an empty body. + * For HTML foreign elements and custom elements the self-closing flag determines if + * they self-close or not. + * + * This function does not determine if a tag is self-closing, + * but only if the self-closing flag is present in the syntax. + * + * @since 6.3.0 + * + * @return bool Whether the currently matched tag contains the self-closing flag. + */ + public function has_self_closing_flag() { + if ( ! $this->tag_name_starts_at ) { + return false; + } + + /* + * The self-closing flag is the solidus at the _end_ of the tag, not the beginning. + * + * Example: + * + * <figure /> + * ^ this appears one character before the end of the closing ">". + */ + return '/' === $this->html[ $this->token_starts_at + $this->token_length - 1 ]; + } + + /** + * Indicates if the current tag token is a tag closer. + * + * Example: + * + * $p = new WP_HTML_Tag_Processor( '<div></div>' ); + * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); + * $p->is_tag_closer() === false; + * + * $p->next_tag( array( 'tag_name' => 'div', 'tag_closers' => 'visit' ) ); + * $p->is_tag_closer() === true; + * + * @since 6.2.0 + * + * @return bool Whether the current tag is a tag closer. + */ + public function is_tag_closer() { + return $this->is_closing_tag; + } + + /** + * Updates or creates a new attribute on the currently matched tag with the passed value. + * + * For boolean attributes special handling is provided: + * - When `true` is passed as the value, then only the attribute name is added to the tag. + * - When `false` is passed, the attribute gets removed if it existed before. + * + * For string attributes, the value is escaped using the `esc_attr` function. + * + * @since 6.2.0 + * @since 6.2.1 Fix: Only create a single update for multiple calls with case-variant attribute names. + * + * @param string $name The attribute name to target. + * @param string|bool $value The new attribute value. + * @return bool Whether an attribute value was set. + */ + public function set_attribute( $name, $value ) { + if ( $this->is_closing_tag || null === $this->tag_name_starts_at ) { + return false; + } + + /* + * WordPress rejects more characters than are strictly forbidden + * in HTML5. This is to prevent additional security risks deeper + * in the WordPress and plugin stack. Specifically the + * less-than (<) greater-than (>) and ampersand (&) aren't allowed. + * + * The use of a PCRE match enables looking for specific Unicode + * code points without writing a UTF-8 decoder. Whereas scanning + * for one-byte characters is trivial (with `strcspn`), scanning + * for the longer byte sequences would be more complicated. Given + * that this shouldn't be in the hot path for execution, it's a + * reasonable compromise in efficiency without introducing a + * noticeable impact on the overall system. + * + * @see https://html.spec.whatwg.org/#attributes-2 + * + * @todo As the only regex pattern maybe we should take it out? + * Are Unicode patterns available broadly in Core? + */ + if ( preg_match( + '~[' . + // Syntax-like characters. + '"\'>&</ =' . + // Control characters. + '\x{00}-\x{1F}' . + // HTML noncharacters. + '\x{FDD0}-\x{FDEF}' . + '\x{FFFE}\x{FFFF}\x{1FFFE}\x{1FFFF}\x{2FFFE}\x{2FFFF}\x{3FFFE}\x{3FFFF}' . + '\x{4FFFE}\x{4FFFF}\x{5FFFE}\x{5FFFF}\x{6FFFE}\x{6FFFF}\x{7FFFE}\x{7FFFF}' . + '\x{8FFFE}\x{8FFFF}\x{9FFFE}\x{9FFFF}\x{AFFFE}\x{AFFFF}\x{BFFFE}\x{BFFFF}' . + '\x{CFFFE}\x{CFFFF}\x{DFFFE}\x{DFFFF}\x{EFFFE}\x{EFFFF}\x{FFFFE}\x{FFFFF}' . + '\x{10FFFE}\x{10FFFF}' . + ']~Ssu', + $name + ) ) { + _doing_it_wrong( + __METHOD__, + __( 'Invalid attribute name.' ), + '6.2.0' + ); + + return false; + } + + /* + * > The values "true" and "false" are not allowed on boolean attributes. + * > To represent a false value, the attribute has to be omitted altogether. + * - HTML5 spec, https://html.spec.whatwg.org/#boolean-attributes + */ + if ( false === $value ) { + return $this->remove_attribute( $name ); + } + + if ( true === $value ) { + $updated_attribute = $name; + } else { + $escaped_new_value = esc_attr( $value ); + $updated_attribute = "{$name}=\"{$escaped_new_value}\""; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $comparable_name = strtolower( $name ); + + if ( isset( $this->attributes[ $comparable_name ] ) ) { + /* + * Update an existing attribute. + * + * Example – set attribute id to "new" in <div id="initial_id" />: + * + * <div id="initial_id"/> + * ^-------------^ + * start end + * replacement: `id="new"` + * + * Result: <div id="new"/> + */ + $existing_attribute = $this->attributes[ $comparable_name ]; + $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $existing_attribute->start, + $existing_attribute->length, + $updated_attribute + ); + } else { + /* + * Create a new attribute at the tag's name end. + * + * Example – add attribute id="new" to <div />: + * + * <div/> + * ^ + * start and end + * replacement: ` id="new"` + * + * Result: <div id="new"/> + */ + $this->lexical_updates[ $comparable_name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->tag_name_starts_at + $this->tag_name_length, + 0, + ' ' . $updated_attribute + ); + } + + /* + * Any calls to update the `class` attribute directly should wipe out any + * enqueued class changes from `add_class` and `remove_class`. + */ + if ( 'class' === $comparable_name && ! empty( $this->classname_updates ) ) { + $this->classname_updates = array(); + } + + return true; + } + + /** + * Remove an attribute from the currently-matched tag. + * + * @since 6.2.0 + * + * @param string $name The attribute name to remove. + * @return bool Whether an attribute was removed. + */ + public function remove_attribute( $name ) { + if ( $this->is_closing_tag ) { + return false; + } + + /* + * > There must never be two or more attributes on + * > the same start tag whose names are an ASCII + * > case-insensitive match for each other. + * - HTML 5 spec + * + * @see https://html.spec.whatwg.org/multipage/syntax.html#attributes-2:ascii-case-insensitive + */ + $name = strtolower( $name ); + + /* + * Any calls to update the `class` attribute directly should wipe out any + * enqueued class changes from `add_class` and `remove_class`. + */ + if ( 'class' === $name && count( $this->classname_updates ) !== 0 ) { + $this->classname_updates = array(); + } + + /* + * If updating an attribute that didn't exist in the input + * document, then remove the enqueued update and move on. + * + * For example, this might occur when calling `remove_attribute()` + * after calling `set_attribute()` for the same attribute + * and when that attribute wasn't originally present. + */ + if ( ! isset( $this->attributes[ $name ] ) ) { + if ( isset( $this->lexical_updates[ $name ] ) ) { + unset( $this->lexical_updates[ $name ] ); + } + return false; + } + + /* + * Removes an existing tag attribute. + * + * Example – remove the attribute id from <div id="main"/>: + * <div id="initial_id"/> + * ^-------------^ + * start end + * replacement: `` + * + * Result: <div /> + */ + $this->lexical_updates[ $name ] = new Gutenberg_HTML_Text_Replacement_6_5( + $this->attributes[ $name ]->start, + $this->attributes[ $name ]->length, + '' + ); + + // Removes any duplicated attributes if they were also present. + if ( null !== $this->duplicate_attributes && array_key_exists( $name, $this->duplicate_attributes ) ) { + foreach ( $this->duplicate_attributes[ $name ] as $attribute_token ) { + $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( + $attribute_token->start, + $attribute_token->length, + '' + ); + } + } + + return true; + } + + /** + * Adds a new class name to the currently matched tag. + * + * @since 6.2.0 + * + * @param string $class_name The class name to add. + * @return bool Whether the class was set to be added. + */ + public function add_class( $class_name ) { + if ( $this->is_closing_tag ) { + return false; + } + + if ( null !== $this->tag_name_starts_at ) { + $this->classname_updates[ $class_name ] = self::ADD_CLASS; + } + + return true; + } + + /** + * Removes a class name from the currently matched tag. + * + * @since 6.2.0 + * + * @param string $class_name The class name to remove. + * @return bool Whether the class was set to be removed. + */ + public function remove_class( $class_name ) { + if ( $this->is_closing_tag ) { + return false; + } + + if ( null !== $this->tag_name_starts_at ) { + $this->classname_updates[ $class_name ] = self::REMOVE_CLASS; + } + + return true; + } + + /** + * Returns the string representation of the HTML Tag Processor. + * + * @since 6.2.0 + * + * @see WP_HTML_Tag_Processor::get_updated_html() + * + * @return string The processed HTML. + */ + public function __toString() { + return $this->get_updated_html(); + } + + /** + * Returns the string representation of the HTML Tag Processor. + * + * @since 6.2.0 + * @since 6.2.1 Shifts the internal cursor corresponding to the applied updates. + * @since 6.4.0 No longer calls subclass method `next_tag()` after updating HTML. + * + * @return string The processed HTML. + */ + public function get_updated_html() { + $requires_no_updating = 0 === count( $this->classname_updates ) && 0 === count( $this->lexical_updates ); + + /* + * When there is nothing more to update and nothing has already been + * updated, return the original document and avoid a string copy. + */ + if ( $requires_no_updating ) { + return $this->html; + } + + /* + * Keep track of the position right before the current tag. This will + * be necessary for reparsing the current tag after updating the HTML. + */ + $before_current_tag = $this->token_starts_at; + + /* + * 1. Apply the enqueued edits and update all the pointers to reflect those changes. + */ + $this->class_name_updates_to_attributes_updates(); + $before_current_tag += $this->apply_attributes_updates( $before_current_tag ); + + /* + * 2. Rewind to before the current tag and reparse to get updated attributes. + * + * At this point the internal cursor points to the end of the tag name. + * Rewind before the tag name starts so that it's as if the cursor didn't + * move; a call to `next_tag()` will reparse the recently-updated attributes + * and additional calls to modify the attributes will apply at this same + * location, but in order to avoid issues with subclasses that might add + * behaviors to `next_tag()`, the internal methods should be called here + * instead. + * + * It's important to note that in this specific place there will be no change + * because the processor was already at a tag when this was called and it's + * rewinding only to the beginning of this very tag before reprocessing it + * and its attributes. + * + * <p>Previous HTML<em>More HTML</em></p> + * ↑ │ back up by the length of the tag name plus the opening < + * └←─┘ back up by strlen("em") + 1 ==> 3 + */ + $this->bytes_already_parsed = $before_current_tag; + $this->parse_next_tag(); + // Reparse the attributes. + while ( $this->parse_next_attribute() ) { + continue; + } + + $tag_ends_at = strpos( $this->html, '>', $this->bytes_already_parsed ); + $this->token_length = $tag_ends_at - $this->token_starts_at; + $this->bytes_already_parsed = $tag_ends_at; + + return $this->html; + } + + /** + * Parses tag query input into internal search criteria. + * + * @since 6.2.0 + * + * @param array|string|null $query { + * Optional. Which tag name to find, having which class, etc. Default is to find any tag. + * + * @type string|null $tag_name Which tag to find, or `null` for "any tag." + * @type int|null $match_offset Find the Nth tag matching all search criteria. + * 1 for "first" tag, 3 for "third," etc. + * Defaults to first tag. + * @type string|null $class_name Tag must contain this class name to match. + * @type string $tag_closers "visit" or "skip": whether to stop on tag closers, e.g. </div>. + * } + */ + private function parse_query( $query ) { + if ( null !== $query && $query === $this->last_query ) { + return; + } + + $this->last_query = $query; + $this->sought_tag_name = null; + $this->sought_class_name = null; + $this->sought_match_offset = 1; + $this->stop_on_tag_closers = false; + + // A single string value means "find the tag of this name". + if ( is_string( $query ) ) { + $this->sought_tag_name = $query; + return; + } + + // An empty query parameter applies no restrictions on the search. + if ( null === $query ) { + return; + } + + // If not using the string interface, an associative array is required. + if ( ! is_array( $query ) ) { + _doing_it_wrong( + __METHOD__, + __( 'The query argument must be an array or a tag name.' ), + '6.2.0' + ); + return; + } + + if ( isset( $query['tag_name'] ) && is_string( $query['tag_name'] ) ) { + $this->sought_tag_name = $query['tag_name']; + } + + if ( isset( $query['class_name'] ) && is_string( $query['class_name'] ) ) { + $this->sought_class_name = $query['class_name']; + } + + if ( isset( $query['match_offset'] ) && is_int( $query['match_offset'] ) && 0 < $query['match_offset'] ) { + $this->sought_match_offset = $query['match_offset']; + } + + if ( isset( $query['tag_closers'] ) ) { + $this->stop_on_tag_closers = 'visit' === $query['tag_closers']; + } + } + + + /** + * Checks whether a given tag and its attributes match the search criteria. + * + * @since 6.2.0 + * + * @return bool Whether the given tag and its attribute match the search criteria. + */ + private function matches() { + if ( $this->is_closing_tag && ! $this->stop_on_tag_closers ) { + return false; + } + + // Does the tag name match the requested tag name in a case-insensitive manner? + if ( null !== $this->sought_tag_name ) { + /* + * String (byte) length lookup is fast. If they aren't the + * same length then they can't be the same string values. + */ + if ( strlen( $this->sought_tag_name ) !== $this->tag_name_length ) { + return false; + } + + /* + * Check each character to determine if they are the same. + * Defer calls to `strtoupper()` to avoid them when possible. + * Calling `strcasecmp()` here tested slowed than comparing each + * character, so unless benchmarks show otherwise, it should + * not be used. + * + * It's expected that most of the time that this runs, a + * lower-case tag name will be supplied and the input will + * contain lower-case tag names, thus normally bypassing + * the case comparison code. + */ + for ( $i = 0; $i < $this->tag_name_length; $i++ ) { + $html_char = $this->html[ $this->tag_name_starts_at + $i ]; + $tag_char = $this->sought_tag_name[ $i ]; + + if ( $html_char !== $tag_char && strtoupper( $html_char ) !== $tag_char ) { + return false; + } + } + } + + if ( null !== $this->sought_class_name && ! $this->has_class( $this->sought_class_name ) ) { + return false; + } + + return true; + } +} diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php new file mode 100644 index 00000000000000..6409255833c818 --- /dev/null +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php @@ -0,0 +1,64 @@ +<?php +/** + * HTML API: WP_HTML_Text_Replacement class + * + * @package WordPress + * @subpackage HTML-API + * @since 6.2.0 + */ + +/** + * Core class used by the HTML tag processor as a data structure for replacing + * existing content from start to end, allowing to drastically improve performance. + * + * This class is for internal usage of the WP_HTML_Tag_Processor class. + * + * @access private + * @since 6.2.0 + * @since 6.5.0 Replace `end` with `length` to more closely match `substr()`. + * + * @see WP_HTML_Tag_Processor + */ +class Gutenberg_HTML_Text_Replacement_6_5 { + /** + * Byte offset into document where replacement span begins. + * + * @since 6.2.0 + * + * @var int + */ + public $start; + + /** + * Byte length of span being replaced. + * + * @since 6.5.0 + * + * @var int + */ + public $length; + + /** + * Span of text to insert in document to replace existing content from start to end. + * + * @since 6.2.0 + * + * @var string + */ + public $text; + + /** + * Constructor. + * + * @since 6.2.0 + * + * @param int $start Byte offset into document where replacement span begins. + * @param int $length Byte length of span in document being replaced. + * @param string $text Span of text to insert in document to replace existing content from start to end. + */ + public function __construct( $start, $length, $text ) { + $this->start = $start; + $this->length = $length; + $this->text = $text; + } +} diff --git a/lib/experimental/interactivity-api/class-wp-directive-processor.php b/lib/experimental/interactivity-api/class-wp-directive-processor.php index e717b2e5539431..cf55a048bb9fa5 100644 --- a/lib/experimental/interactivity-api/class-wp-directive-processor.php +++ b/lib/experimental/interactivity-api/class-wp-directive-processor.php @@ -20,7 +20,7 @@ * available. Please restrain from investing unnecessary time and effort trying * to improve this code. */ -class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_4 { +class WP_Directive_Processor extends Gutenberg_HTML_Tag_Processor_6_5 { /** * An array of root blocks. @@ -195,7 +195,7 @@ public function get_inner_html() { } list( $start_name, $end_name ) = $bookmarks; - $start = $this->bookmarks[ $start_name ]->end + 1; + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; $end = $this->bookmarks[ $end_name ]->start; $this->seek( $start_name ); // Return to original position. @@ -225,14 +225,14 @@ public function set_inner_html( $new_html ) { } list( $start_name, $end_name ) = $bookmarks; - $start = $this->bookmarks[ $start_name ]->end + 1; + $start = $this->bookmarks[ $start_name ]->start + $this->bookmarks[ $start_name ]->length + 1; $end = $this->bookmarks[ $end_name ]->start; $this->seek( $start_name ); // Return to original position. $this->release_bookmark( $start_name ); $this->release_bookmark( $end_name ); - $this->lexical_updates[] = new WP_HTML_Text_Replacement( $start, $end, $new_html ); + $this->lexical_updates[] = new Gutenberg_HTML_Text_Replacement_6_5( $start, $end - $start, $new_html ); return true; } diff --git a/lib/load.php b/lib/load.php index 9c7618dbfc678b..59fb75541ac41e 100644 --- a/lib/load.php +++ b/lib/load.php @@ -76,6 +76,10 @@ function gutenberg_is_experiment_enabled( $name ) { * always be loaded so that Gutenberg code can run the newest version of the Tag Processor. */ require __DIR__ . '/compat/wordpress-6.4/html-api/class-gutenberg-html-tag-processor-6-4.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-attribute-token-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-span-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-text-replacement-6-5.php'; +require __DIR__ . '/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php'; /* * The HTML Processor appeared after WordPress 6.3. If Gutenberg is running on a version of From 336e76fae26eaf78b3ae79df08e8367f9241bef6 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:11:48 +0900 Subject: [PATCH 109/325] Patterns: Keep synced pattern when added via drag and drop (#56924) * Patterns: Keep synced pattern when added via drag and drop * Fix comment typo --- .../components/block-patterns-list/index.js | 2 +- .../inserter-draggable-blocks/index.js | 23 +- .../editor/various/inserting-blocks.spec.js | 204 ++++++++++++------ 3 files changed, 159 insertions(+), 70 deletions(-) diff --git a/packages/block-editor/src/components/block-patterns-list/index.js b/packages/block-editor/src/components/block-patterns-list/index.js index a392d2687017ff..c79d6927c00f07 100644 --- a/packages/block-editor/src/components/block-patterns-list/index.js +++ b/packages/block-editor/src/components/block-patterns-list/index.js @@ -56,7 +56,7 @@ function BlockPattern( { <InserterDraggableBlocks isEnabled={ isDraggable } blocks={ blocks } - isPattern={ !! pattern } + pattern={ pattern } > { ( { draggable, onDragStart, onDragEnd } ) => ( <div diff --git a/packages/block-editor/src/components/inserter-draggable-blocks/index.js b/packages/block-editor/src/components/inserter-draggable-blocks/index.js index 42cae8c7bcde98..c6f008426288e1 100644 --- a/packages/block-editor/src/components/inserter-draggable-blocks/index.js +++ b/packages/block-editor/src/components/inserter-draggable-blocks/index.js @@ -2,19 +2,24 @@ * WordPress dependencies */ import { Draggable } from '@wordpress/components'; -import { serialize, store as blocksStore } from '@wordpress/blocks'; +import { + createBlock, + serialize, + store as blocksStore, +} from '@wordpress/blocks'; import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ import BlockDraggableChip from '../block-draggable/draggable-chip'; +import { PATTERN_TYPES } from '../inserter/block-patterns-tab/utils'; const InserterDraggableBlocks = ( { isEnabled, blocks, icon, children, - isPattern, + pattern, } ) => { const transferData = { type: 'inserter', @@ -36,13 +41,21 @@ const InserterDraggableBlocks = ( { __experimentalTransferDataType="wp-blocks" transferData={ transferData } onDragStart={ ( event ) => { - event.dataTransfer.setData( 'text/html', serialize( blocks ) ); + const parsedBlocks = + pattern?.type === PATTERN_TYPES.user && + pattern?.syncStatus !== 'unsynced' + ? [ createBlock( 'core/block', { ref: pattern.id } ) ] + : blocks; + event.dataTransfer.setData( + 'text/html', + serialize( parsedBlocks ) + ); } } __experimentalDragComponent={ <BlockDraggableChip count={ blocks.length } - icon={ icon || ( ! isPattern && blockTypeIcon ) } - isPattern={ isPattern } + icon={ icon || ( ! pattern && blockTypeIcon ) } + isPattern={ !! pattern } /> } > diff --git a/test/e2e/specs/editor/various/inserting-blocks.spec.js b/test/e2e/specs/editor/various/inserting-blocks.spec.js index a48fe117c97a2c..4d26a198fc3455 100644 --- a/test/e2e/specs/editor/various/inserting-blocks.spec.js +++ b/test/e2e/specs/editor/various/inserting-blocks.spec.js @@ -17,6 +17,13 @@ test.use( { test.describe( 'Inserting blocks (@firefox, @webkit)', () => { test.afterAll( async ( { requestUtils } ) => { await requestUtils.deleteAllPosts(); + await requestUtils.deleteAllBlocks(); + await requestUtils.deleteAllPatternCategories(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllBlocks(); + await requestUtils.deleteAllPatternCategories(); } ); test( 'inserts blocks by dragging and dropping from the global inserter', async ( { @@ -58,34 +65,16 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const paragraphBoundingBox = await paragraphBlock.boundingBox(); await expect( insertingBlocksUtils.indicator ).toBeVisible(); - // Expect the indicator to be below the paragraph block. - await expect - .poll( () => - insertingBlocksUtils.indicator - .boundingBox() - .then( ( { y } ) => y ) - ) - .toBeGreaterThan( paragraphBoundingBox.y ); + await insertingBlocksUtils.expectIndicatorBelowParagraph( + paragraphBoundingBox + ); await page.mouse.down(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - // Hover on the right side of the block to avoid collapsing with the preview. - paragraphBoundingBox.x + paragraphBoundingBox.width - 1, - // Hover on the bottom of the paragraph block. - paragraphBoundingBox.y + paragraphBoundingBox.height - 1 - ); - } - // Expect the indicator to be below the paragraph block. - await expect - .poll( () => - insertingBlocksUtils.indicator - .boundingBox() - .then( ( { y } ) => y ) - ) - .toBeGreaterThan( paragraphBoundingBox.y ); + + await insertingBlocksUtils.dragOver( paragraphBoundingBox ); + await insertingBlocksUtils.expectIndicatorBelowParagraph( + paragraphBoundingBox + ); // Expect the draggable-chip to appear. await expect( insertingBlocksUtils.draggableChip ).toBeVisible(); @@ -139,16 +128,8 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const paragraphBoundingBox = await paragraphBlock.boundingBox(); await page.mouse.down(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - // Hover on the right side of the block to avoid collapsing with the preview. - paragraphBoundingBox.x + paragraphBoundingBox.width - 1, - // Hover on the bottom of the paragraph block. - paragraphBoundingBox.y + paragraphBoundingBox.height - 1 - ); - } + + await insertingBlocksUtils.dragOver( paragraphBoundingBox ); await expect( insertingBlocksUtils.indicator ).toBeVisible(); await expect( insertingBlocksUtils.draggableChip ).toBeVisible(); @@ -210,26 +191,13 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const paragraphBoundingBox = await paragraphBlock.boundingBox(); await page.mouse.down(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - // Hover on the right side of the block to avoid collapsing with the preview. - paragraphBoundingBox.x + paragraphBoundingBox.width - 1, - // Hover on the bottom of the paragraph block. - paragraphBoundingBox.y + paragraphBoundingBox.height - 1 - ); - } + + await insertingBlocksUtils.dragOver( paragraphBoundingBox ); await expect( insertingBlocksUtils.indicator ).toBeVisible(); - // Expect the indicator to be below the paragraph block. - await expect - .poll( () => - insertingBlocksUtils.indicator - .boundingBox() - .then( ( { y } ) => y ) - ) - .toBeGreaterThan( paragraphBoundingBox.y ); + await insertingBlocksUtils.expectIndicatorBelowParagraph( + paragraphBoundingBox + ); await expect( insertingBlocksUtils.draggableChip ).toBeVisible(); @@ -238,6 +206,103 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { expect( await editor.getEditedPostContent() ).toMatchSnapshot(); } ); + test( 'inserts synced patterns by dragging and dropping from the global inserter', async ( { + admin, + page, + editor, + insertingBlocksUtils, + }, testInfo ) => { + testInfo.fixme( + testInfo.project.name === 'firefox', + 'The clientX value is always 0 in firefox, see https://github.com/microsoft/playwright/issues/17761 for more info.' + ); + const PATTERN_NAME = 'My synced pattern'; + + await admin.createNewPost(); + await editor.switchToLegacyCanvas(); + + // We need a dummy block in place to display the drop indicator due to a bug. + // @see https://github.com/WordPress/gutenberg/issues/44064 + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'Dummy text' }, + } ); + + const paragraphBlock = page.locator( + '[data-type="core/paragraph"] >> text=Dummy text' + ); + + // Create a synced pattern from the paragraph block. + await editor.insertBlock( { + name: 'core/paragraph', + attributes: { content: 'A useful paragraph to reuse' }, + } ); + await editor.showBlockToolbar(); + await page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Options' } ) + .click(); + await page.getByRole( 'menuitem', { name: 'Create pattern' } ).click(); + const createPatternDialog = page.getByRole( 'dialog', { + name: 'Create pattern', + } ); + await createPatternDialog + .getByRole( 'textbox', { name: 'Name' } ) + .fill( PATTERN_NAME ); + await createPatternDialog + .getByRole( 'checkbox', { name: 'Synced' } ) + .setChecked( true ); + await createPatternDialog + .getByRole( 'button', { name: 'Create' } ) + .click(); + const patternBlock = page.getByRole( 'document', { + name: 'Block: Pattern', + } ); + await expect( patternBlock ).toBeFocused(); + + // Insert a synced pattern. + await page.click( + 'role=region[name="Editor top bar"i] >> role=button[name="Toggle block inserter"i]' + ); + await page.fill( + 'role=region[name="Block Library"i] >> role=searchbox[name="Search for blocks and patterns"i]', + PATTERN_NAME + ); + await page.hover( + `role=listbox[name="Block Patterns"i] >> role=option[name="${ PATTERN_NAME }"i]` + ); + + const paragraphBoundingBox = await paragraphBlock.boundingBox(); + + await page.mouse.down(); + + await insertingBlocksUtils.dragOver( paragraphBoundingBox ); + await expect( insertingBlocksUtils.indicator ).toBeVisible(); + await insertingBlocksUtils.expectIndicatorBelowParagraph( + paragraphBoundingBox + ); + await expect( insertingBlocksUtils.draggableChip ).toBeVisible(); + + await page.mouse.up(); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/paragraph', + attributes: { + content: 'Dummy text', + }, + }, + { + name: 'core/block', + attributes: { ref: expect.any( Number ) }, + }, + { + name: 'core/block', + attributes: { ref: expect.any( Number ) }, + }, + ] ); + } ); + test( 'cancels dragging patterns from the global inserter by pressing Escape', async ( { admin, page, @@ -278,16 +343,8 @@ test.describe( 'Inserting blocks (@firefox, @webkit)', () => { const paragraphBoundingBox = await paragraphBlock.boundingBox(); await page.mouse.down(); - // Call the move function twice to make sure the `dragOver` event is sent. - // @see https://github.com/microsoft/playwright/issues/17153 - for ( let i = 0; i < 2; i += 1 ) { - await page.mouse.move( - // Hover on the right side of the block to avoid collapsing with the preview. - paragraphBoundingBox.x + paragraphBoundingBox.width - 1, - // Hover on the bottom of the paragraph block. - paragraphBoundingBox.y + paragraphBoundingBox.height - 1 - ); - } + + await insertingBlocksUtils.dragOver( paragraphBoundingBox ); await expect( insertingBlocksUtils.indicator ).toBeVisible(); await expect( insertingBlocksUtils.draggableChip ).toBeVisible(); @@ -388,4 +445,23 @@ class InsertingBlocksUtils { 'data-testid=block-draggable-chip >> visible=true' ); } + async dragOver( boundingBox ) { + // Call the move function twice to make sure the `dragOver` event is sent. + // @see https://github.com/microsoft/playwright/issues/17153 + for ( let i = 0; i < 2; i += 1 ) { + await this.page.mouse.move( + // Hover on the right side of the block to avoid collapsing with the preview. + boundingBox.x + boundingBox.width - 1, + // Hover on the bottom of the paragraph block. + boundingBox.y + boundingBox.height - 1 + ); + } + } + + async expectIndicatorBelowParagraph( paragraphBoundingBox ) { + // Expect the indicator to be below the paragraph block. + await expect + .poll( () => this.indicator.boundingBox().then( ( { y } ) => y ) ) + .toBeGreaterThan( paragraphBoundingBox.y ); + } } From 2642697ff65f6f011044f8939f9f7ed70af2945c Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:53:52 +0900 Subject: [PATCH 110/325] Site Editor Sidebar: Add "Areas" details panel to all templates and update icon (#55677) * Site Editor Sidebar: Add "Areas" details panel to all templates * Use symbolFilled icon for general template parts * Remove unnecessary code --- .../home-template-details.js | 97 +------------ .../index.js | 10 +- .../template-areas.js | 135 ++++++++++++++++++ 3 files changed, 144 insertions(+), 98 deletions(-) create mode 100644 packages/edit-site/src/components/sidebar-navigation-screen-template/template-areas.js diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js index c798067aca9c57..57a13377617533 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/home-template-details.js @@ -9,12 +9,8 @@ import { CheckboxControl, __experimentalInputControl as InputControl, __experimentalNumberControl as NumberControl, - __experimentalTruncate as Truncate, - __experimentalItemGroup as ItemGroup, } from '@wordpress/components'; -import { header, footer, layout } from '@wordpress/icons'; -import { useMemo, useState, useEffect } from '@wordpress/element'; -import { decodeEntities } from '@wordpress/html-entities'; +import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -23,58 +19,19 @@ import { SidebarNavigationScreenDetailsPanel, SidebarNavigationScreenDetailsPanelRow, } from '../sidebar-navigation-screen-details-panel'; -import { unlock } from '../../lock-unlock'; -import { store as editSiteStore } from '../../store'; -import { useLink } from '../routes/link'; -import SidebarNavigationItem from '../sidebar-navigation-item'; -import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; const EMPTY_OBJECT = {}; -function TemplateAreaButton( { postId, icon, title } ) { - const icons = { - header, - footer, - }; - const linkInfo = useLink( { - postType: TEMPLATE_PART_POST_TYPE, - postId, - } ); - - return ( - <SidebarNavigationItem - className="edit-site-sidebar-navigation-screen-template__template-area-button" - { ...linkInfo } - icon={ icons[ icon ] ?? layout } - withChevron - > - <Truncate - limit={ 20 } - ellipsizeMode="tail" - numberOfLines={ 1 } - className="edit-site-sidebar-navigation-screen-template__template-area-label-text" - > - { decodeEntities( title ) } - </Truncate> - </SidebarNavigationItem> - ); -} - export default function HomeTemplateDetails() { const { editEntityRecord } = useDispatch( coreStore ); const { allowCommentsOnNewPosts, - templatePartAreas, postsPerPage, postsPageTitle, postsPageId, - currentTemplateParts, } = useSelect( ( select ) => { const { getEntityRecord } = select( coreStore ); - const { getSettings, getCurrentTemplateTemplateParts } = unlock( - select( editSiteStore ) - ); const siteSettings = getEntityRecord( 'root', 'site' ); const _postsPageRecord = siteSettings?.page_for_posts ? getEntityRecord( @@ -90,8 +47,6 @@ export default function HomeTemplateDetails() { postsPageTitle: _postsPageRecord?.title?.rendered, postsPageId: _postsPageRecord?.id, postsPerPage: siteSettings?.posts_per_page, - templatePartAreas: getSettings()?.defaultTemplatePartAreas, - currentTemplateParts: getCurrentTemplateTemplateParts(), }; }, [] ); @@ -111,36 +66,6 @@ export default function HomeTemplateDetails() { setPostsCountValue( postsPerPage ); }, [ postsPageTitle, allowCommentsOnNewPosts, postsPerPage ] ); - /* - * Merge data in currentTemplateParts with templatePartAreas, - * which contains the template icon and fallback labels - */ - const templateAreas = useMemo( () => { - // Keep track of template part IDs that have already been added to the array. - const templatePartIds = new Set(); - const filterOutDuplicateTemplateParts = ( currentTemplatePart ) => { - // If the template part has already been added to the array, skip it. - if ( templatePartIds.has( currentTemplatePart.templatePart.id ) ) { - return; - } - // Add to the array of template part IDs. - templatePartIds.add( currentTemplatePart.templatePart.id ); - return currentTemplatePart; - }; - - return currentTemplateParts.length && templatePartAreas - ? currentTemplateParts - .filter( filterOutDuplicateTemplateParts ) - .map( ( { templatePart, block } ) => ( { - ...templatePartAreas?.find( - ( { area } ) => area === templatePart?.area - ), - ...templatePart, - clientId: block.clientId, - } ) ) - : []; - }, [ currentTemplateParts, templatePartAreas ] ); - const setAllowCommentsOnNewPosts = ( newValue ) => { setCommentsOnNewPostsValue( newValue ); editEntityRecord( 'root', 'site', undefined, { @@ -214,26 +139,6 @@ export default function HomeTemplateDetails() { /> </SidebarNavigationScreenDetailsPanelRow> </SidebarNavigationScreenDetailsPanel> - <SidebarNavigationScreenDetailsPanel - title={ __( 'Areas' ) } - spacing={ 3 } - > - <ItemGroup> - { templateAreas.map( - ( { clientId, label, icon, theme, slug, title } ) => ( - <SidebarNavigationScreenDetailsPanelRow - key={ clientId } - > - <TemplateAreaButton - postId={ `${ theme }//${ slug }` } - title={ title?.rendered || label } - icon={ icon } - /> - </SidebarNavigationScreenDetailsPanelRow> - ) - ) } - </ItemGroup> - </SidebarNavigationScreenDetailsPanel> </> ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js index fd60d0a509ace2..e6ead651fbbfc9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/index.js @@ -12,6 +12,7 @@ import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies */ +import TemplateAreas from './template-areas'; import SidebarNavigationScreen from '../sidebar-navigation-screen'; import useEditedEntityRecord from '../use-edited-entity-record'; import { unlock } from '../../lock-unlock'; @@ -45,8 +46,13 @@ function useTemplateDetails( postType, postId ) { const content = record?.slug === 'home' || record?.slug === 'index' ? ( - <HomeTemplateDetails /> - ) : null; + <> + <HomeTemplateDetails /> + <TemplateAreas /> + </> + ) : ( + <TemplateAreas /> + ); const footer = record?.modified ? ( <SidebarNavigationScreenDetailsFooter record={ record } /> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-template/template-areas.js b/packages/edit-site/src/components/sidebar-navigation-screen-template/template-areas.js new file mode 100644 index 00000000000000..db59f6886124be --- /dev/null +++ b/packages/edit-site/src/components/sidebar-navigation-screen-template/template-areas.js @@ -0,0 +1,135 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; +import { + __experimentalTruncate as Truncate, + __experimentalItemGroup as ItemGroup, +} from '@wordpress/components'; +import { store as editorStore } from '@wordpress/editor'; +import { useMemo } from '@wordpress/element'; +import { decodeEntities } from '@wordpress/html-entities'; + +/** + * Internal dependencies + */ +import { + SidebarNavigationScreenDetailsPanel, + SidebarNavigationScreenDetailsPanelRow, +} from '../sidebar-navigation-screen-details-panel'; +import { unlock } from '../../lock-unlock'; +import { store as editSiteStore } from '../../store'; +import { useLink } from '../routes/link'; +import SidebarNavigationItem from '../sidebar-navigation-item'; +import { TEMPLATE_PART_POST_TYPE } from '../../utils/constants'; + +function TemplateAreaButton( { postId, area, title } ) { + const templatePartArea = useSelect( + ( select ) => { + const defaultAreas = + select( + editorStore + ).__experimentalGetDefaultTemplatePartAreas(); + + return defaultAreas.find( + ( defaultArea ) => defaultArea.area === area + ); + }, + [ area ] + ); + const linkInfo = useLink( { + postType: TEMPLATE_PART_POST_TYPE, + postId, + } ); + + return ( + <SidebarNavigationItem + className="edit-site-sidebar-navigation-screen-template__template-area-button" + { ...linkInfo } + icon={ templatePartArea?.icon } + withChevron + > + <Truncate + limit={ 20 } + ellipsizeMode="tail" + numberOfLines={ 1 } + className="edit-site-sidebar-navigation-screen-template__template-area-label-text" + > + { decodeEntities( title ) } + </Truncate> + </SidebarNavigationItem> + ); +} + +export default function TemplateAreas() { + const { templatePartAreas, currentTemplateParts } = useSelect( + ( select ) => { + const { getSettings, getCurrentTemplateTemplateParts } = unlock( + select( editSiteStore ) + ); + return { + templatePartAreas: getSettings()?.defaultTemplatePartAreas, + currentTemplateParts: getCurrentTemplateTemplateParts(), + }; + }, + [] + ); + + /* + * Merge data in currentTemplateParts with templatePartAreas, + * which contains the template icon and fallback labels + */ + const templateAreas = useMemo( () => { + // Keep track of template part IDs that have already been added to the array. + const templatePartIds = new Set(); + const filterOutDuplicateTemplateParts = ( currentTemplatePart ) => { + // If the template part has already been added to the array, skip it. + if ( templatePartIds.has( currentTemplatePart.templatePart.id ) ) { + return; + } + // Add to the array of template part IDs. + templatePartIds.add( currentTemplatePart.templatePart.id ); + return currentTemplatePart; + }; + + return currentTemplateParts.length && templatePartAreas + ? currentTemplateParts + .filter( filterOutDuplicateTemplateParts ) + .map( ( { templatePart, block } ) => ( { + ...templatePartAreas?.find( + ( { area } ) => area === templatePart?.area + ), + ...templatePart, + clientId: block.clientId, + } ) ) + : []; + }, [ currentTemplateParts, templatePartAreas ] ); + + if ( ! templateAreas.length ) { + return null; + } + + return ( + <SidebarNavigationScreenDetailsPanel + title={ __( 'Areas' ) } + spacing={ 3 } + > + <ItemGroup> + { templateAreas.map( + ( { clientId, label, area, theme, slug, title } ) => ( + <SidebarNavigationScreenDetailsPanelRow + key={ clientId } + > + <TemplateAreaButton + postId={ `${ theme }//${ slug }` } + title={ title?.rendered || label } + area={ area } + /> + </SidebarNavigationScreenDetailsPanelRow> + ) + ) } + </ItemGroup> + </SidebarNavigationScreenDetailsPanel> + ); +} From 068a6cfe090d2ff7079f32320a7012588224387e Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 11 Dec 2023 09:16:38 +0100 Subject: [PATCH 111/325] Platform Docs: Add trusted by section (#56749) --- platform-docs/docs/advanced/_category_.json | 10 ---- platform-docs/docs/advanced/create-format.md | 5 -- platform-docs/docs/advanced/dynamic.md | 5 -- platform-docs/docs/advanced/interactivity.md | 5 -- platform-docs/docs/advanced/wordpress.md | 5 -- .../docs/create-block/writing-flow.md | 5 -- .../src/components/HomepageFeatures/index.js | 13 ++--- .../HomepageFeatures/styles.module.css | 11 +++- .../src/components/HomepageTrustedBy/index.js | 50 ++++++++++++++++++ .../HomepageTrustedBy/styles.module.css | 27 ++++++++++ platform-docs/src/pages/index.js | 2 + platform-docs/static/img/dayone.png | Bin 0 -> 3122 bytes platform-docs/static/img/tumblr.png | Bin 0 -> 12340 bytes platform-docs/static/img/wordpress.png | Bin 0 -> 102930 bytes 14 files changed, 94 insertions(+), 44 deletions(-) delete mode 100644 platform-docs/docs/advanced/_category_.json delete mode 100644 platform-docs/docs/advanced/create-format.md delete mode 100644 platform-docs/docs/advanced/dynamic.md delete mode 100644 platform-docs/docs/advanced/interactivity.md delete mode 100644 platform-docs/docs/advanced/wordpress.md delete mode 100644 platform-docs/docs/create-block/writing-flow.md create mode 100644 platform-docs/src/components/HomepageTrustedBy/index.js create mode 100644 platform-docs/src/components/HomepageTrustedBy/styles.module.css create mode 100644 platform-docs/static/img/dayone.png create mode 100644 platform-docs/static/img/tumblr.png create mode 100644 platform-docs/static/img/wordpress.png diff --git a/platform-docs/docs/advanced/_category_.json b/platform-docs/docs/advanced/_category_.json deleted file mode 100644 index a5787a8693001a..00000000000000 --- a/platform-docs/docs/advanced/_category_.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "position": 4, - "label": "Advanced", - "collapsible": true, - "collapsed": true, - "link": { - "type": "generated-index", - "title": "Advanced" - } -} diff --git a/platform-docs/docs/advanced/create-format.md b/platform-docs/docs/advanced/create-format.md deleted file mode 100644 index 3779c5a85e5bef..00000000000000 --- a/platform-docs/docs/advanced/create-format.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a RichText format \ No newline at end of file diff --git a/platform-docs/docs/advanced/dynamic.md b/platform-docs/docs/advanced/dynamic.md deleted file mode 100644 index 369670cb1af490..00000000000000 --- a/platform-docs/docs/advanced/dynamic.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Augmenting blocks \ No newline at end of file diff --git a/platform-docs/docs/advanced/interactivity.md b/platform-docs/docs/advanced/interactivity.md deleted file mode 100644 index ba35f82cb21446..00000000000000 --- a/platform-docs/docs/advanced/interactivity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Interactivity API \ No newline at end of file diff --git a/platform-docs/docs/advanced/wordpress.md b/platform-docs/docs/advanced/wordpress.md deleted file mode 100644 index ce2e40f7c702eb..00000000000000 --- a/platform-docs/docs/advanced/wordpress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Gutenberg and WordPress \ No newline at end of file diff --git a/platform-docs/docs/create-block/writing-flow.md b/platform-docs/docs/create-block/writing-flow.md deleted file mode 100644 index 452e060f01b1a8..00000000000000 --- a/platform-docs/docs/create-block/writing-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Writing Flow and Pasting \ No newline at end of file diff --git a/platform-docs/src/components/HomepageFeatures/index.js b/platform-docs/src/components/HomepageFeatures/index.js index 40aa82d49bd78c..9f42ddd6425e16 100644 --- a/platform-docs/src/components/HomepageFeatures/index.js +++ b/platform-docs/src/components/HomepageFeatures/index.js @@ -2,7 +2,6 @@ * External dependencies */ import React from 'react'; -import clsx from 'clsx'; /** * Internal dependencies @@ -40,7 +39,7 @@ const FeatureList = [ function Feature( { Svg, title, description } ) { return ( - <div className={ clsx( 'col col--4' ) }> + <div className={ styles.feature }> <div className="text--center"> <Svg className={ styles.featureSvg } role="img" /> </div> @@ -55,12 +54,10 @@ function Feature( { Svg, title, description } ) { export default function HomepageFeatures() { return ( <section className={ styles.features }> - <div className="container"> - <div className="row"> - { FeatureList.map( ( props, idx ) => ( - <Feature key={ idx } { ...props } /> - ) ) } - </div> + <div className="row"> + { FeatureList.map( ( props, idx ) => ( + <Feature key={ idx } { ...props } /> + ) ) } </div> </section> ); diff --git a/platform-docs/src/components/HomepageFeatures/styles.module.css b/platform-docs/src/components/HomepageFeatures/styles.module.css index 1f0d53211e56ca..1d42ee1c02cc9b 100644 --- a/platform-docs/src/components/HomepageFeatures/styles.module.css +++ b/platform-docs/src/components/HomepageFeatures/styles.module.css @@ -2,11 +2,20 @@ display: flex; align-items: center; padding: 2rem 0; - width: 100%; background: var(--ifm-color-secondary); } +.features > div { + max-width: var(--ifm-container-width); + margin: auto; + justify-content: space-between; +} + .featureSvg { height: 200px; width: 200px; } + +.feature { + max-width: 30%; +} diff --git a/platform-docs/src/components/HomepageTrustedBy/index.js b/platform-docs/src/components/HomepageTrustedBy/index.js new file mode 100644 index 00000000000000..f48205e0a057c4 --- /dev/null +++ b/platform-docs/src/components/HomepageTrustedBy/index.js @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import React from 'react'; + +/** + * Internal dependencies + */ +import styles from './styles.module.css'; + +const Users = [ + { + title: 'WordPress', + img: require( '@site/static/img/wordpress.png' ).default, + height: 60, + }, + { + title: 'Tumblr', + img: require( '@site/static/img/tumblr.png' ).default, + height: 18, + }, + { + title: 'Day One', + img: require( '@site/static/img/dayone.png' ).default, + height: 100, + }, +]; + +function User( { img, title, height } ) { + return ( + <div className={ styles.col }> + <img src={ img } alt={ title } style={ { height } } /> + </div> + ); +} + +export default function HomepageTrustedBy() { + return ( + <section className={ styles.container }> + <div> + <h2 className={ styles.title }>Trusted by</h2> + <div className={ styles.row }> + { Users.map( ( props, idx ) => ( + <User key={ idx } { ...props } /> + ) ) } + </div> + </div> + </section> + ); +} diff --git a/platform-docs/src/components/HomepageTrustedBy/styles.module.css b/platform-docs/src/components/HomepageTrustedBy/styles.module.css new file mode 100644 index 00000000000000..8481a032b43386 --- /dev/null +++ b/platform-docs/src/components/HomepageTrustedBy/styles.module.css @@ -0,0 +1,27 @@ +.container { + padding: 5rem 0 2rem; + background: #fff; + color: var(--ifm-color-gray-800); +} + +.container > div { + margin: auto; + max-width: var(--ifm-container-width); +} + +.title { + text-align: center; + margin-bottom: 0; + flex-shrink: 0; +} + +.row { + display: flex; + justify-content: center; + align-items: center; + gap: 2rem; +} + +.col > img { + filter: grayscale(1); +} diff --git a/platform-docs/src/pages/index.js b/platform-docs/src/pages/index.js index a61a9254c40297..bb1226f7d8bd64 100644 --- a/platform-docs/src/pages/index.js +++ b/platform-docs/src/pages/index.js @@ -12,6 +12,7 @@ import HomepageFeatures from '@site/src/components/HomepageFeatures'; * Internal dependencies */ import styles from './index.module.css'; +import HomepageTrustedBy from '../components/HomepageTrustedBy'; function HomepageHeader() { const { siteConfig } = useDocusaurusContext(); @@ -43,6 +44,7 @@ export default function Home() { <HomepageHeader /> <main> <HomepageFeatures /> + <HomepageTrustedBy /> </main> </Layout> ); diff --git a/platform-docs/static/img/dayone.png b/platform-docs/static/img/dayone.png new file mode 100644 index 0000000000000000000000000000000000000000..7fab9ab6ec2b463552de2bc673b3ada5c21a81fd GIT binary patch literal 3122 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 literal 0 HcmV?d00001 diff --git a/platform-docs/static/img/tumblr.png b/platform-docs/static/img/tumblr.png new file mode 100644 index 0000000000000000000000000000000000000000..333aac82b419a45e18794b10c827acd4361daf07 GIT binary patch literal 12340 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 CADSEh literal 0 HcmV?d00001 diff --git a/platform-docs/static/img/wordpress.png b/platform-docs/static/img/wordpress.png new file mode 100644 index 0000000000000000000000000000000000000000..eb195a12c14bf9b3e1439e208e2fc0f2c27a99bd GIT binary patch literal 102930 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%0Dy`siDJb=wT2Qn`+OF_rPnd*YFXJITj9Z3U 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 PF9}<#%a`gbeZKn#a6t8; literal 0 HcmV?d00001 From b7dfe56ef6d72308f44298c3006893c5745cadf8 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Dec 2023 10:31:47 +0200 Subject: [PATCH 112/325] Site editor: fix typing performance by not rendering sidebar (#56927) --- .../edit-site/src/components/layout/index.js | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 9e4153938d40ad..71d99b9a4bcbbd 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -278,26 +278,27 @@ export default function Layout() { ariaLabel={ __( 'Navigation' ) } className="edit-site-layout__sidebar-region" > - <motion.div - // The sidebar is needed for routing on mobile - // (https://github.com/WordPress/gutenberg/pull/51558/files#r1231763003), - // so we can't remove the element entirely. Using `inert` will make - // it inaccessible to screen readers and keyboard navigation. - inert={ showSidebar ? undefined : 'true' } - animate={ { opacity: showSidebar ? 1 : 0 } } - transition={ { - type: 'tween', - duration: - // Disable transition in mobile to emulate a full page transition. - disableMotion || isMobileViewport - ? 0 - : ANIMATION_DURATION, - ease: 'easeOut', - } } - className="edit-site-layout__sidebar" - > - <Sidebar /> - </motion.div> + <AnimatePresence> + { showSidebar && ( + <motion.div + initial={ { opacity: 0 } } + animate={ { opacity: 1 } } + exit={ { opacity: 0 } } + transition={ { + type: 'tween', + duration: + // Disable transition in mobile to emulate a full page transition. + disableMotion || isMobileViewport + ? 0 + : ANIMATION_DURATION, + ease: 'easeOut', + } } + className="edit-site-layout__sidebar" + > + <Sidebar /> + </motion.div> + ) } + </AnimatePresence> </NavigableRegion> <SavePanel /> From e2e7fced3a96388ac520ee0200fac884c1157252 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 11 Dec 2023 18:20:46 +0900 Subject: [PATCH 113/325] Tweak table block placeholder with __next40pxDefaultSize props (#56935) --- packages/block-library/src/table/edit.js | 4 +++- packages/block-library/src/table/editor.scss | 15 +-------------- 2 files changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/table/edit.js b/packages/block-library/src/table/edit.js index 17a9e1ecfdd5bf..b8f239a01095d9 100644 --- a/packages/block-library/src/table/edit.js +++ b/packages/block-library/src/table/edit.js @@ -555,6 +555,7 @@ function TableEdit( { > <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize type="number" label={ __( 'Column count' ) } value={ initialColumnCount } @@ -564,6 +565,7 @@ function TableEdit( { /> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize type="number" label={ __( 'Row count' ) } value={ initialRowCount } @@ -572,7 +574,7 @@ function TableEdit( { className="blocks-table__placeholder-input" /> <Button - className="blocks-table__placeholder-button" + __next40pxDefaultSize variant="primary" type="submit" > diff --git a/packages/block-library/src/table/editor.scss b/packages/block-library/src/table/editor.scss index ef7e1bdcd9aa38..0367ed0a9c5d95 100644 --- a/packages/block-library/src/table/editor.scss +++ b/packages/block-library/src/table/editor.scss @@ -58,27 +58,14 @@ display: flex; flex-direction: column; align-items: flex-start; - - > * { - margin-bottom: $grid-unit-10; - } + gap: $grid-unit-10; @include break-medium() { flex-direction: row; align-items: flex-end; - - > * { - margin-bottom: 0; - } } } .blocks-table__placeholder-input { width: $grid-unit-10 * 14; - margin-right: $grid-unit-10; - margin-bottom: 0; - - input { - height: $button-size; - } } From 823d086722b78b50fca478caa3fbae9b02dbd216 Mon Sep 17 00:00:00 2001 From: Jonathan Bossenger <jonathanbossenger@gmail.com> Date: Mon, 11 Dec 2023 11:33:25 +0200 Subject: [PATCH 114/325] Update Getting Started Guide for Gutenberg 17.2 (#56674) * Update create-block scaffold command to use latest Updates the create-block command to use the @latest dist-tag, which matches the create-block documentation. This helps if folks have an older version of create-block cached locally, which doesn't include the newer templating and changes to scaffolding, like automatically setting the render property in block.json * Updates to Getting Started Guide Includes a note about adding the view.js file to the package.json until wp-scripts is updated Updates the minimum requirements of Gutenberg to version 17.2, which uses the new store --- .../interactivity/docs/1-getting-started.md | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/packages/interactivity/docs/1-getting-started.md b/packages/interactivity/docs/1-getting-started.md index 0b4708b78b3509..85af2021807351 100644 --- a/packages/interactivity/docs/1-getting-started.md +++ b/packages/interactivity/docs/1-getting-started.md @@ -6,13 +6,13 @@ To get started with the Interactivity API, you can follow this [**Quick Start Gu - [Quick Start Guide](#quick-start-guide) - [1. Scaffold an interactive block](#1-scaffold-an-interactive-block) - - [2. Generate the build](#2-generate-the-build) + - [2. Generate the build](#2-generate-the-build) - [3. Use it in your WordPress installation ](#3-use-it-in-your-wordpress-installation) - [Requirements of the Interactivity API](#requirements-of-the-interactivity-aPI) - [A local WordPress installation](#a-local-wordpress-installation) - [Latest vesion of Gutenberg](#latest-vesion-of-gutenberg) - [Node.js](#nodejs) - - [Code requirements](#code-requirements) + - [Code requirements](#code-requirements) - [Add `interactivity` support to `block.json`](#add-interactivity-support-to-blockjson) - [Add `wp-interactive` directive to a DOM element](#add-wp-interactive-directive-to-a-dom-element) @@ -23,26 +23,38 @@ To get started with the Interactivity API, you can follow this [**Quick Start Gu We can scaffold a WordPress plugin that registers an interactive block (using the Interactivity API) by using a [template](https://www.npmjs.com/package/@wordpress/create-block-interactive-template) with the `@wordpress/create-block` command. ``` -npx @wordpress/create-block my-first-interactive-block --template @wordpress/create-block-interactive-template +npx @wordpress/create-block@latest my-first-interactive-block --template @wordpress/create-block-interactive-template ``` -#### 2. Generate the build +> **Note** +> The Interactivity API recently switched from [using modules instead of scripts in the frontend](https://github.com/WordPress/gutenberg/pull/56143). Therefore, in order to test this scaffolded block, you will need to add the following line to the `package.json` file of the generated plugin: + +```json +"files": [ + "src/view.js" +] +``` +> This should be updated in the [scripts package](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/) soon. + -When the plugin folder is generated, we should launch the build process to get the final version of the interactive block that can be used from WordPress. + +#### 2. Generate the build + +When the plugin folder is generated, we should launch the build process to get the final version of the interactive block that can be used from WordPress. ``` cd my-first-interactive-block && npm start ``` -#### 3. Use it in your WordPress installation +#### 3. Use it in your WordPress installation If you have a local WordPress installation already running, you can launch the commands above inside the `plugins` folder of that installation. If not, you can use [`wp-now`](https://github.com/WordPress/playground-tools/tree/trunk/packages/wp-now) to launch a WordPress site with the plugin installed by executing from the generated folder (and from a different terminal window or tab) the following command ``` -npx @wp-now/wp-now start +npx @wp-now/wp-now start ``` -At this point you should be able to insert the "My First Interactive Block" block into any post, and see how it behaves in the frontend when published. +At this point you should be able to insert the "My First Interactive Block" block into any post, and see how it behaves in the frontend when published. > **Note** > We recommend you to also check the [API Reference](./2-api-reference.md) docs for your first exploration of the Interactivity API @@ -53,19 +65,19 @@ To start working with the Interactivity API you'll need to have a [proper WordPr #### A local WordPress installation -You can use [the tools to set your local WordPress environment](https://developer.wordpress.org/block-editor/getting-started/devenv/#wordpress-development-site) you feel more comfortable with. +You can use [the tools to set your local WordPress environment](https://developer.wordpress.org/block-editor/getting-started/devenv/#wordpress-development-site) you feel more comfortable with. -To get quickly started, [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now) is the easiest way to get a WordPress site up and running locally. +To get quickly started, [`wp-now`](https://www.npmjs.com/package/@wp-now/wp-now) is the easiest way to get a WordPress site up and running locally. #### Latest vesion of Gutenberg -The Interactivity API is currently only available as an experimental feature from Gutenberg 16.2, so you'll need to have Gutenberg 16.2 or higher version installed and activated in your WordPress installation. +The Interactivity API is currently only available as an experimental feature from Gutenberg 17.2, so you'll need to have Gutenberg 17.2 or higher version installed and activated in your WordPress installation. #### Node.js Block development requires [Node](https://nodejs.org/en), so you'll need to have Node installed and running on your machine. Any version modern should work, but please check the minimum version requirements if you run into any issues with any of the Node.js tools used in WordPress development. -#### Code requirements +#### Code requirements ##### Add `interactivity` support to `block.json` @@ -86,4 +98,4 @@ To "activate" the Interactivity API in a DOM element (and its children) we add t <div data-wp-interactive='{ "namespace": "myPlugin" }'> <!-- Interactivity API zone --> </div> -``` \ No newline at end of file +``` From b6860f0d8da1a1eacae40bfec168cb907a1f68c8 Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.richards@automattic.com> Date: Mon, 11 Dec 2023 17:33:59 +0800 Subject: [PATCH 115/325] Fix error when using a navigation block that returns an empty fallback result (#56629) * Fix fatal error when using a navigation block fallback that returns an empty result * Fix get_inner_blocks_from_unstable_location function * Use an empty WP_Block_List as a return value * Add a unit test for get_inner_blocks_from_navigation_post --- .../class-wp-navigation-block-renderer.php | 4 ++-- .../block-library/src/navigation/index.php | 2 +- ...lass-wp-navigation-block-renderer-test.php | 19 +++++++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php index 9c2314ebe6890c..189e5a695c23a7 100644 --- a/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php +++ b/lib/compat/wordpress-6.5/class-wp-navigation-block-renderer.php @@ -185,7 +185,7 @@ private static function get_inner_blocks_html( $attributes, $inner_blocks ) { private static function get_inner_blocks_from_navigation_post( $attributes ) { $navigation_post = get_post( $attributes['ref'] ); if ( ! isset( $navigation_post ) ) { - return ''; + return new WP_Block_List( array(), $attributes ); } // Only published posts are valid. If this is changed then a corresponding change @@ -214,7 +214,7 @@ private static function get_inner_blocks_from_fallback( $attributes ) { // Fallback my have been filtered so do basic test for validity. if ( empty( $fallback_blocks ) || ! is_array( $fallback_blocks ) ) { - return ''; + return new WP_Block_List( array(), $attributes ); } return new WP_Block_List( $fallback_blocks, $attributes ); diff --git a/packages/block-library/src/navigation/index.php b/packages/block-library/src/navigation/index.php index eb06e731ccb8f9..3af85afd92522f 100644 --- a/packages/block-library/src/navigation/index.php +++ b/packages/block-library/src/navigation/index.php @@ -75,7 +75,7 @@ function block_core_navigation_sort_menu_items_by_parent_id( $menu_items ) { function block_core_navigation_get_inner_blocks_from_unstable_location( $attributes ) { $menu_items = block_core_navigation_get_menu_items_at_location( $attributes['__unstableLocation'] ); if ( empty( $menu_items ) ) { - return ''; + return new WP_Block_List( array(), $attributes ); } $menu_items_by_parent_id = block_core_navigation_sort_menu_items_by_parent_id( $menu_items ); diff --git a/phpunit/class-wp-navigation-block-renderer-test.php b/phpunit/class-wp-navigation-block-renderer-test.php index 73f3c89d798838..124e0fe91bd1e6 100644 --- a/phpunit/class-wp-navigation-block-renderer-test.php +++ b/phpunit/class-wp-navigation-block-renderer-test.php @@ -62,4 +62,23 @@ public function test_gutenberg_get_markup_for_inner_block_site_title() { $expected = '<li class="wp-block-navigation-item"><h1 class="wp-block-site-title"><a href="http://' . WP_TESTS_DOMAIN . '" target="_self" rel="home">Test Blog</a></h1></li>'; $this->assertEquals( $expected, $result ); } + + /** + * Test that the `get_inner_blocks_from_navigation_post` method returns an empty block list for a non-existent post. + * + * @group navigation-renderer + * + * @covers WP_Navigation_Block_Renderer::get_inner_blocks_from_navigation_post + */ + public function test_gutenberg_get_inner_blocks_from_navigation_post_returns_empty_block_list() { + $reflection = new ReflectionClass( 'WP_Navigation_Block_Renderer' ); + $method = $reflection->getMethod( 'get_inner_blocks_from_navigation_post' ); + $method->setAccessible( true ); + $attributes = array( 'ref' => 0 ); + + $actual = $method->invoke( $reflection, $attributes ); + $expected = new WP_Block_List( array(), $attributes ); + $this->assertEquals( $actual, $expected ); + $this->assertCount( 0, $actual ); + } } From 7358f66b08d1276d1bc919dd3f52be91020c0220 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Dec 2023 11:09:10 +0100 Subject: [PATCH 116/325] Block editor: remove 4 useSelect in favour of context (#56915) --- .../src/components/block-controls/hook.js | 12 +++-- .../components/block-controls/test/index.js | 6 +-- .../src/components/block-edit/context.js | 3 ++ .../src/components/block-edit/index.js | 46 +++++++++++++++---- .../components/block-info-slot-fill/index.js | 9 ++-- .../src/components/block-list/block.js | 24 +++++++++- .../src/components/block-list/block.native.js | 11 +++++ .../src/components/inspector-controls/fill.js | 9 ++-- .../inspector-controls/fill.native.js | 9 ++-- .../use-display-block-controls/index.js | 42 ----------------- .../index.native.js | 33 ------------- packages/block-editor/src/hooks/utils.js | 14 ++++-- .../src/cover/test/edit.native.js | 8 +++- .../src/text-color/test/index.native.js | 2 +- 14 files changed, 119 insertions(+), 109 deletions(-) delete mode 100644 packages/block-editor/src/components/use-display-block-controls/index.js delete mode 100644 packages/block-editor/src/components/use-display-block-controls/index.native.js diff --git a/packages/block-editor/src/components/block-controls/hook.js b/packages/block-editor/src/components/block-controls/hook.js index e3f69c8bec3b25..4e9592471cb298 100644 --- a/packages/block-editor/src/components/block-controls/hook.js +++ b/packages/block-editor/src/components/block-controls/hook.js @@ -6,14 +6,18 @@ * Internal dependencies */ import groups from './groups'; -import useDisplayBlockControls from '../use-display-block-controls'; +import { + useBlockEditContext, + mayDisplayControlsKey, + mayDisplayParentControlsKey, +} from '../block-edit/context'; export default function useBlockControlsFill( group, shareWithChildBlocks ) { - const { isDisplayed, isParentDisplayed } = useDisplayBlockControls(); - if ( isDisplayed ) { + const context = useBlockEditContext(); + if ( context[ mayDisplayControlsKey ] ) { return groups[ group ]?.Fill; } - if ( isParentDisplayed && shareWithChildBlocks ) { + if ( context[ mayDisplayParentControlsKey ] && shareWithChildBlocks ) { return groups.parent.Fill; } return null; diff --git a/packages/block-editor/src/components/block-controls/test/index.js b/packages/block-editor/src/components/block-controls/test/index.js index cbfcb3c1873a75..bbd987cbeb20e8 100644 --- a/packages/block-editor/src/components/block-controls/test/index.js +++ b/packages/block-editor/src/components/block-controls/test/index.js @@ -59,7 +59,7 @@ describe( 'BlockControls', () => { it( 'should render a dynamic toolbar of controls', () => { render( <SlotFillProvider> - <BlockEdit name="core/test-block" isSelected> + <BlockEdit name="core/test-block" mayDisplayControls> <BlockControls controls={ controls }> <p>Child</p> </BlockControls> @@ -84,7 +84,7 @@ describe( 'BlockControls', () => { it( 'should render its children', () => { render( <SlotFillProvider> - <BlockEdit name="core/test-block" isSelected> + <BlockEdit name="core/test-block" mayDisplayControls> <BlockControls controls={ controls }> <p>Child</p> </BlockControls> @@ -99,7 +99,7 @@ describe( 'BlockControls', () => { it( 'should a dynamic toolbar when passed as children', () => { render( <SlotFillProvider> - <BlockEdit name="core/test-block" isSelected> + <BlockEdit name="core/test-block" mayDisplayControls> <BlockControls> <ToolbarGroup controls={ controls } /> </BlockControls> diff --git a/packages/block-editor/src/components/block-edit/context.js b/packages/block-editor/src/components/block-edit/context.js index d8b9c829484102..6b0b1af9ea22dd 100644 --- a/packages/block-editor/src/components/block-edit/context.js +++ b/packages/block-editor/src/components/block-edit/context.js @@ -3,6 +3,9 @@ */ import { createContext, useContext } from '@wordpress/element'; +export const mayDisplayControlsKey = Symbol( 'mayDisplayControls' ); +export const mayDisplayParentControlsKey = Symbol( 'mayDisplayParentControls' ); + export const DEFAULT_BLOCK_EDIT_CONTEXT = { name: '', isSelected: false, diff --git a/packages/block-editor/src/components/block-edit/index.js b/packages/block-editor/src/components/block-edit/index.js index 6faefbc6261929..bbef47b27c5790 100644 --- a/packages/block-editor/src/components/block-edit/index.js +++ b/packages/block-editor/src/components/block-edit/index.js @@ -8,7 +8,12 @@ import { hasBlockSupport } from '@wordpress/blocks'; * Internal dependencies */ import Edit from './edit'; -import { BlockEditContextProvider, useBlockEditContext } from './context'; +import { + BlockEditContextProvider, + useBlockEditContext, + mayDisplayControlsKey, + mayDisplayParentControlsKey, +} from './context'; /** * The `useBlockEditContext` hook provides information about the block this hook is being used in. @@ -20,7 +25,13 @@ import { BlockEditContextProvider, useBlockEditContext } from './context'; */ export { useBlockEditContext }; -export default function BlockEdit( props ) { +export default function BlockEdit( { + mayDisplayControls, + mayDisplayParentControls, + // The remaining props are passed through the BlockEdit filters and are thus + // public API! + ...props +} ) { const { name, isSelected, @@ -32,19 +43,34 @@ export default function BlockEdit( props ) { const layoutSupport = hasBlockSupport( name, 'layout', false ) || hasBlockSupport( name, '__experimentalLayout', false ); - const context = { - name, - isSelected, - clientId, - layout: layoutSupport ? layout : null, - __unstableLayoutClassNames, - }; return ( <BlockEditContextProvider // It is important to return the same object if props haven't // changed to avoid unnecessary rerenders. // See https://reactjs.org/docs/context.html#caveats. - value={ useMemo( () => context, Object.values( context ) ) } + value={ useMemo( + () => ( { + name, + isSelected, + clientId, + layout: layoutSupport ? layout : null, + __unstableLayoutClassNames, + // We use symbols in favour of an __unstable prefix to avoid + // usage outside of the package (this context is exposed). + [ mayDisplayControlsKey ]: mayDisplayControls, + [ mayDisplayParentControlsKey ]: mayDisplayParentControls, + } ), + [ + name, + isSelected, + clientId, + layoutSupport, + layout, + __unstableLayoutClassNames, + mayDisplayControls, + mayDisplayParentControls, + ] + ) } > <Edit { ...props } /> </BlockEditContextProvider> diff --git a/packages/block-editor/src/components/block-info-slot-fill/index.js b/packages/block-editor/src/components/block-info-slot-fill/index.js index 8e16757f3ebbc1..8c9503313d754c 100644 --- a/packages/block-editor/src/components/block-info-slot-fill/index.js +++ b/packages/block-editor/src/components/block-info-slot-fill/index.js @@ -7,14 +7,17 @@ import { privateApis as componentsPrivateApis } from '@wordpress/components'; * Internal dependencies */ import { unlock } from '../../lock-unlock'; -import useDisplayBlockControls from '../use-display-block-controls'; +import { + useBlockEditContext, + mayDisplayControlsKey, +} from '../block-edit/context'; const { createPrivateSlotFill } = unlock( componentsPrivateApis ); const { Fill, Slot } = createPrivateSlotFill( 'BlockInformation' ); const BlockInfo = ( props ) => { - const { isDisplayed } = useDisplayBlockControls(); - if ( ! isDisplayed ) { + const context = useBlockEditContext(); + if ( ! context[ mayDisplayControlsKey ] ) { return null; } return <Fill { ...props } />; diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index ff5915981acdf9..b1a97237ab630d 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -15,6 +15,7 @@ import { switchToBlockType, getDefaultBlockName, isUnmodifiedBlock, + store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; import { @@ -95,21 +96,40 @@ function BlockListBlock( { themeSupportsLayout, isTemporarilyEditingAsBlocks, blockEditingMode, + mayDisplayControls, + mayDisplayParentControls, } = useSelect( ( select ) => { const { getSettings, __unstableGetTemporarilyEditingAsBlocks, getBlockEditingMode, + getBlockName, + isFirstMultiSelectedBlock, + getMultiSelectedBlockClientIds, + hasSelectedInnerBlock, } = select( blockEditorStore ); + const { hasBlockSupport } = select( blocksStore ); return { themeSupportsLayout: getSettings().supportsLayout, isTemporarilyEditingAsBlocks: __unstableGetTemporarilyEditingAsBlocks() === clientId, blockEditingMode: getBlockEditingMode( clientId ), + mayDisplayControls: + isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === name + ) ), + mayDisplayParentControls: + hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && hasSelectedInnerBlock( clientId ), }; }, - [ clientId ] + [ clientId, isSelected, name ] ); const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); @@ -137,6 +157,8 @@ function BlockListBlock( { __unstableParentLayout={ Object.keys( parentLayout ).length ? parentLayout : undefined } + mayDisplayControls={ mayDisplayControls } + mayDisplayParentControls={ mayDisplayParentControls } /> ); 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 03a84d530ba12a..2daaf846443b5a 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -148,6 +148,7 @@ function BlockListBlock( { isDescendentBlockSelected, isParentSelected, order, + mayDisplayControls, } = useSelect( ( select ) => { const { @@ -158,6 +159,9 @@ function BlockListBlock( { getSelectedBlockClientId, getSettings, hasSelectedInnerBlock, + getBlockName, + isFirstMultiSelectedBlock, + getMultiSelectedBlockClientIds, } = select( blockEditorStore ); const currentBlockType = getBlockType( name || 'core/missing' ); const currentBlockCategory = currentBlockType?.category; @@ -205,6 +209,12 @@ function BlockListBlock( { isDescendentBlockSelected: descendentBlockSelected, isParentSelected: parentSelected, order: blockOrder, + mayDisplayControls: + isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === name + ) ), }; }, [ clientId, isSelected, name, rootClientId ] @@ -346,6 +356,7 @@ function BlockListBlock( { : undefined } wrapperProps={ wrapperProps } + mayDisplayControls={ mayDisplayControls } /> <View onLayout={ onLayout } /> </GlobalStylesContext.Provider> diff --git a/packages/block-editor/src/components/inspector-controls/fill.js b/packages/block-editor/src/components/inspector-controls/fill.js index fdb0d44f0eccb6..456b33af9137fe 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.js +++ b/packages/block-editor/src/components/inspector-controls/fill.js @@ -12,7 +12,10 @@ import { useEffect, useContext } from '@wordpress/element'; /** * Internal dependencies */ -import useDisplayBlockControls from '../use-display-block-controls'; +import { + useBlockEditContext, + mayDisplayControlsKey, +} from '../block-edit/context'; import groups from './groups'; export default function InspectorControlsFill( { @@ -33,13 +36,13 @@ export default function InspectorControlsFill( { group = __experimentalGroup; } - const { isDisplayed } = useDisplayBlockControls(); + const context = useBlockEditContext(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { warning( `Unknown InspectorControls group "${ group }" provided.` ); return null; } - if ( ! isDisplayed ) { + if ( ! context[ mayDisplayControlsKey ] ) { return null; } diff --git a/packages/block-editor/src/components/inspector-controls/fill.native.js b/packages/block-editor/src/components/inspector-controls/fill.native.js index f1ee5a14cd18e1..98b6698721e1ce 100644 --- a/packages/block-editor/src/components/inspector-controls/fill.native.js +++ b/packages/block-editor/src/components/inspector-controls/fill.native.js @@ -15,7 +15,10 @@ import deprecated from '@wordpress/deprecated'; * Internal dependencies */ import groups from './groups'; -import useDisplayBlockControls from '../use-display-block-controls'; +import { + useBlockEditContext, + mayDisplayControlsKey, +} from '../block-edit/context'; import { BlockSettingsButton } from '../block-settings'; export default function InspectorControlsFill( { @@ -35,14 +38,14 @@ export default function InspectorControlsFill( { ); group = __experimentalGroup; } - const { isDisplayed } = useDisplayBlockControls(); + const context = useBlockEditContext(); const Fill = groups[ group ]?.Fill; if ( ! Fill ) { warning( `Unknown InspectorControls group "${ group }" provided.` ); return null; } - if ( ! isDisplayed ) { + if ( ! context[ mayDisplayControlsKey ] ) { return null; } diff --git a/packages/block-editor/src/components/use-display-block-controls/index.js b/packages/block-editor/src/components/use-display-block-controls/index.js deleted file mode 100644 index ef27479593a736..00000000000000 --- a/packages/block-editor/src/components/use-display-block-controls/index.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { store as blocksStore } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; -import { store as blockEditorStore } from '../../store'; - -export default function useDisplayBlockControls() { - const { isSelected, clientId, name } = useBlockEditContext(); - return useSelect( - ( select ) => { - const { - getBlockName, - isFirstMultiSelectedBlock, - getMultiSelectedBlockClientIds, - hasSelectedInnerBlock, - } = select( blockEditorStore ); - const { hasBlockSupport } = select( blocksStore ); - - return { - isDisplayed: - isSelected || - ( isFirstMultiSelectedBlock( clientId ) && - getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ) ), - isParentDisplayed: - hasBlockSupport( - getBlockName( clientId ), - '__experimentalExposeControlsToChildren', - false - ) && hasSelectedInnerBlock( clientId ), - }; - }, - [ clientId, isSelected, name ] - ); -} diff --git a/packages/block-editor/src/components/use-display-block-controls/index.native.js b/packages/block-editor/src/components/use-display-block-controls/index.native.js deleted file mode 100644 index d865ed6d9d7b26..00000000000000 --- a/packages/block-editor/src/components/use-display-block-controls/index.native.js +++ /dev/null @@ -1,33 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { hasBlockSupport } from '@wordpress/blocks'; - -/** - * Internal dependencies - */ -import { useBlockEditContext } from '../block-edit/context'; -import { store as blockEditorStore } from '../../store'; - -export default function useDisplayBlockControls() { - const { isSelected, clientId, name } = useBlockEditContext(); - return useSelect( - ( select ) => { - const { getBlockName, getBlockRootClientId } = - select( blockEditorStore ); - - const parentId = getBlockRootClientId( clientId ); - const parentBlockName = getBlockName( parentId ); - - const hideControls = hasBlockSupport( - parentBlockName, - '__experimentalHideChildBlockControls', - false - ); - - return { isDisplayed: ! hideControls && isSelected }; - }, - [ clientId, isSelected, name ] - ); -} diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 76260ed0d4a63c..c6076b822545a9 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -10,7 +10,11 @@ import { addFilter } from '@wordpress/hooks'; /** * Internal dependencies */ -import useDisplayBlockControls from '../components/use-display-block-controls'; +import { + useBlockEditContext, + mayDisplayControlsKey, + mayDisplayParentControlsKey, +} from '../components/block-edit/context'; import { useSettings } from '../components'; import { useSettingsForBlockElement } from '../components/global-styles/hooks'; import { getValueFromObjectPath, setImmutably } from '../utils/object'; @@ -379,8 +383,7 @@ export function createBlockEditFilter( features ) { } ); const withBlockEditHooks = createHigherOrderComponent( ( OriginalBlockEdit ) => ( props ) => { - const { isDisplayed, isParentDisplayed } = - useDisplayBlockControls(); + const context = useBlockEditContext(); // CAUTION: code added before this line will be executed for all // blocks, not just those that support the feature! Code added // above this line should be carefully evaluated for its impact on @@ -394,8 +397,9 @@ export function createBlockEditFilter( features ) { shareWithChildBlocks, } = feature; const shouldDisplayControls = - isDisplayed || - ( isParentDisplayed && shareWithChildBlocks ); + context[ mayDisplayControlsKey ] || + ( context[ mayDisplayParentControlsKey ] && + shareWithChildBlocks ); if ( ! shouldDisplayControls || diff --git a/packages/block-library/src/cover/test/edit.native.js b/packages/block-library/src/cover/test/edit.native.js index 3ca2755ee1aebb..6925cfe3f62214 100644 --- a/packages/block-library/src/cover/test/edit.native.js +++ b/packages/block-library/src/cover/test/edit.native.js @@ -80,7 +80,13 @@ const MEDIA_OPTIONS = [ // Simplified tree to render Cover edit within slot. const CoverEdit = ( props ) => ( <SlotFillProvider> - <BlockEdit isSelected name={ cover.name } clientId={ 0 } { ...props } /> + <BlockEdit + isSelected + mayDisplayControls + name={ cover.name } + clientId={ 0 } + { ...props } + /> <BottomSheetSettings isVisible /> </SlotFillProvider> ); diff --git a/packages/format-library/src/text-color/test/index.native.js b/packages/format-library/src/text-color/test/index.native.js index 9a148dec4358be..2ff9b5edfac219 100644 --- a/packages/format-library/src/text-color/test/index.native.js +++ b/packages/format-library/src/text-color/test/index.native.js @@ -205,7 +205,7 @@ describe( 'Text color', () => { const { getByLabelText } = render( <SlotFillProvider> - <BlockEdit name="core/test-block" isSelected> + <BlockEdit name="core/test-block" isSelected mayDisplayControls> <TextColorEdit isActive={ true } activeAttributes={ {} } From 70bc49feab55399ec522ed6c52497cfcc113204c Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:18:24 +0900 Subject: [PATCH 117/325] Apply __next40pxDefaultSize to TextControl and Button component in renaming UIs (#56933) * Apply __next40pxDefaultSize to TextControl in renaming UIs * Apply __next40pxDefaultSize to Button in renaming UIs --- .../src/components/block-rename/modal.js | 8 +++++++- packages/block-editor/src/hooks/block-renaming.js | 1 + .../components/page-patterns/rename-menu-item.js | 8 +++++++- .../rename-modal.js | 8 +++++++- .../components/template-actions/rename-menu-item.js | 8 +++++++- .../src/components/rename-pattern-category-modal.js | 8 +++++++- .../patterns/src/components/rename-pattern-modal.js | 13 +++++++++++-- 7 files changed, 47 insertions(+), 7 deletions(-) diff --git a/packages/block-editor/src/components/block-rename/modal.js b/packages/block-editor/src/components/block-rename/modal.js index a1e9193f348fd0..e5ad0c647815f7 100644 --- a/packages/block-editor/src/components/block-rename/modal.js +++ b/packages/block-editor/src/components/block-rename/modal.js @@ -88,6 +88,7 @@ export default function BlockRenameModal( { <VStack spacing="3"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize value={ editedBlockName } label={ __( 'Block name' ) } hideLabelFromVision={ true } @@ -96,11 +97,16 @@ export default function BlockRenameModal( { onFocus={ autoSelectInputText } /> <HStack justify="right"> - <Button variant="tertiary" onClick={ onClose }> + <Button + __next40pxDefaultSize + variant="tertiary" + onClick={ onClose } + > { __( 'Cancel' ) } </Button> <Button + __next40pxDefaultSize aria-disabled={ ! isNameValid } variant="primary" type="submit" diff --git a/packages/block-editor/src/hooks/block-renaming.js b/packages/block-editor/src/hooks/block-renaming.js index 26ada6ba732819..9b8beb3a5e4be3 100644 --- a/packages/block-editor/src/hooks/block-renaming.js +++ b/packages/block-editor/src/hooks/block-renaming.js @@ -50,6 +50,7 @@ function BlockRenameControlPure( { metadata, setAttributes } ) { <InspectorControls group="advanced"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize label={ __( 'Block name' ) } value={ metadata?.name || '' } onChange={ ( newName ) => { diff --git a/packages/edit-site/src/components/page-patterns/rename-menu-item.js b/packages/edit-site/src/components/page-patterns/rename-menu-item.js index 8b1d6771c2e90e..c2b3b960fb6677 100644 --- a/packages/edit-site/src/components/page-patterns/rename-menu-item.js +++ b/packages/edit-site/src/components/page-patterns/rename-menu-item.js @@ -96,6 +96,7 @@ export default function RenameMenuItem( { item, onClose } ) { <VStack spacing="5"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize label={ __( 'Name' ) } value={ title } onChange={ setTitle } @@ -104,6 +105,7 @@ export default function RenameMenuItem( { item, onClose } ) { <HStack justify="right"> <Button + __next40pxDefaultSize variant="tertiary" onClick={ () => { setIsModalOpen( false ); @@ -113,7 +115,11 @@ export default function RenameMenuItem( { item, onClose } ) { { __( 'Cancel' ) } </Button> - <Button variant="primary" type="submit"> + <Button + __next40pxDefaultSize + variant="primary" + type="submit" + > { __( 'Save' ) } </Button> </HStack> diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/rename-modal.js b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/rename-modal.js index 668179755ec35a..a2281804bcb728 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/rename-modal.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-navigation-menu/rename-modal.js @@ -27,16 +27,22 @@ export default function RenameModal( { menuTitle, onClose, onSave } ) { <VStack spacing="3"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize value={ editedMenuTitle } placeholder={ __( 'Navigation title' ) } onChange={ setEditedMenuTitle } /> <HStack justify="right"> - <Button variant="tertiary" onClick={ onClose }> + <Button + __next40pxDefaultSize + variant="tertiary" + onClick={ onClose } + > { __( 'Cancel' ) } </Button> <Button + __next40pxDefaultSize disabled={ ! isEditedMenuTitleValid } variant="primary" type="submit" diff --git a/packages/edit-site/src/components/template-actions/rename-menu-item.js b/packages/edit-site/src/components/template-actions/rename-menu-item.js index d098ea13fa58f8..730bdba803ab55 100644 --- a/packages/edit-site/src/components/template-actions/rename-menu-item.js +++ b/packages/edit-site/src/components/template-actions/rename-menu-item.js @@ -107,6 +107,7 @@ export default function RenameMenuItem( { template, onClose } ) { <VStack spacing="5"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize label={ __( 'Name' ) } value={ editedTitle } onChange={ setEditedTitle } @@ -115,6 +116,7 @@ export default function RenameMenuItem( { template, onClose } ) { <HStack justify="right"> <Button + __next40pxDefaultSize variant="tertiary" onClick={ () => { setIsModalOpen( false ); @@ -123,7 +125,11 @@ export default function RenameMenuItem( { template, onClose } ) { { __( 'Cancel' ) } </Button> - <Button variant="primary" type="submit"> + <Button + __next40pxDefaultSize + variant="primary" + type="submit" + > { __( 'Save' ) } </Button> </HStack> diff --git a/packages/patterns/src/components/rename-pattern-category-modal.js b/packages/patterns/src/components/rename-pattern-category-modal.js index 3e9e90da2f8212..3df57757315b81 100644 --- a/packages/patterns/src/components/rename-pattern-category-modal.js +++ b/packages/patterns/src/components/rename-pattern-category-modal.js @@ -138,6 +138,7 @@ export default function RenamePatternCategoryModal( { <TextControl ref={ textControlRef } __nextHasNoMarginBottom + __next40pxDefaultSize label={ __( 'Name' ) } value={ name } onChange={ onChange } @@ -154,10 +155,15 @@ export default function RenamePatternCategoryModal( { ) } </VStack> <HStack justify="right"> - <Button variant="tertiary" onClick={ onRequestClose }> + <Button + __next40pxDefaultSize + variant="tertiary" + onClick={ onRequestClose } + > { __( 'Cancel' ) } </Button> <Button + __next40pxDefaultSize variant="primary" type="submit" aria-disabled={ diff --git a/packages/patterns/src/components/rename-pattern-modal.js b/packages/patterns/src/components/rename-pattern-modal.js index 9b905c04b1e20e..9c6aef7116530e 100644 --- a/packages/patterns/src/components/rename-pattern-modal.js +++ b/packages/patterns/src/components/rename-pattern-modal.js @@ -93,6 +93,7 @@ export default function RenamePatternModal( { <VStack spacing="5"> <TextControl __nextHasNoMarginBottom + __next40pxDefaultSize label={ __( 'Name' ) } value={ name } onChange={ setName } @@ -100,11 +101,19 @@ export default function RenamePatternModal( { /> <HStack justify="right"> - <Button variant="tertiary" onClick={ onRequestClose }> + <Button + __next40pxDefaultSize + variant="tertiary" + onClick={ onRequestClose } + > { __( 'Cancel' ) } </Button> - <Button variant="primary" type="submit"> + <Button + __next40pxDefaultSize + variant="primary" + type="submit" + > { __( 'Save' ) } </Button> </HStack> From 1f75b41023abfec2d627b5596b2a852dd332c085 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 11 Dec 2023 12:53:09 +0100 Subject: [PATCH 118/325] Editor: Unify the preview dropdown between post and site editors (#56921) --- .../src/components/preview-options/README.md | 94 ------------ .../src/components/preview-options/index.js | 93 +----------- .../src/components/preview-options/style.scss | 64 --------- packages/block-editor/src/style.scss | 1 - .../src/editor/preview.ts | 4 +- packages/e2e-test-utils/src/preview.js | 8 +- .../src/components/device-preview/index.js | 71 --------- .../edit-post/src/components/header/index.js | 15 +- .../src/components/header/style.scss | 33 ++--- .../src/components/header-edit-mode/index.js | 56 ++------ .../src/components/preview-dropdown/index.js | 136 ++++++++++++++++++ .../components/preview-dropdown/style.scss | 5 + packages/editor/src/private-apis.js | 2 + packages/editor/src/style.scss | 1 + .../editor/plugins/block-context.spec.js | 6 +- .../e2e/specs/editor/various/new-post.spec.js | 4 +- test/e2e/specs/editor/various/preview.spec.js | 9 +- 17 files changed, 198 insertions(+), 404 deletions(-) delete mode 100644 packages/block-editor/src/components/preview-options/README.md delete mode 100644 packages/block-editor/src/components/preview-options/style.scss delete mode 100644 packages/edit-post/src/components/device-preview/index.js create mode 100644 packages/editor/src/components/preview-dropdown/index.js create mode 100644 packages/editor/src/components/preview-dropdown/style.scss diff --git a/packages/block-editor/src/components/preview-options/README.md b/packages/block-editor/src/components/preview-options/README.md deleted file mode 100644 index 80182f18d243d7..00000000000000 --- a/packages/block-editor/src/components/preview-options/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# Preview Options - -The `PreviewOptions` component displays the list of different preview options available in the editor. - -It returns a [`DropdownMenu`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/components/src/dropdown-menu) component with these different options. The options currently available in the editor are Desktop, Mobile, Tablet and "Preview in new tab". - -![Preview options dropdown menu](https://make.wordpress.org/core/files/2020/09/preview-options-dropdown-menu.png) - -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - -## Development guidelines - -### Usage - -Renders the previews options of the editor in a dropdown menu. - -```jsx -import { Icon, MenuGroup } from '@wordpress/components'; -import { PostPreviewButton } from '@wordpress/editor'; -import { __experimentalPreviewOptions as PreviewOptions } from '@wordpress/block-editor'; - -const MyPreviewOptions = () => ( - <PreviewOptions - isEnabled={ true } - className="edit-post-post-preview-dropdown" - deviceType={ deviceType } - setDeviceType={ setDeviceType } - > { ( { onClose } ) => ( - <MenuGroup> - <div className="edit-post-header-preview__grouping-external"> - <PostPreviewButton - className="edit-post-header-preview__button-external" - role="menuitem" - forceIsAutosaveable={ hasActiveMetaboxes } - textContent={ - <> - { __( 'Preview in new tab' ) } - <Icon icon={ external } /> - </> - } - onPreview={ onClose } - /> - </div> - </MenuGroup> - ) } - </PreviewOptions> -); -``` - -### Props - -#### className - -The CSS classes added to the component. - -- Type: `String` -- Required: no - -#### isEnabled - -Wheter or not the preview options are enabled for the current post. -And example of when the preview options are not enabled is when the current post is not savable. - -- Type: `boolean` -- Required: no -- Default: true - -#### deviceType - -The device type in the preview options. It can be either Desktop or Tablet or Mobile among others. - -- Type: `String` -- Required: yes - -#### setDeviceType - -Used to set the device type that will be used to display the preview inside the editor. - -- Type: `func` -- Required: yes - -#### children - -A function that returns nodes to be rendered within the dropdown. - -- Type: `Function` -- Required: No - -## Related components - -Block Editor components are components that can be used to compose the UI of your block editor. Thus, they can only be used under a [`BlockEditorProvider`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/provider/README.md) in the components tree. diff --git a/packages/block-editor/src/components/preview-options/index.js b/packages/block-editor/src/components/preview-options/index.js index 91018cc980bb29..8f540c35f64551 100644 --- a/packages/block-editor/src/components/preview-options/index.js +++ b/packages/block-editor/src/components/preview-options/index.js @@ -1,92 +1,11 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { useViewportMatch } from '@wordpress/compose'; -import { DropdownMenu, MenuGroup, MenuItem } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { check, desktop, mobile, tablet } from '@wordpress/icons'; - -export default function PreviewOptions( { - children, - viewLabel, - className, - isEnabled = true, - deviceType, - setDeviceType, - label, - showIconLabels, -} ) { - const isMobile = useViewportMatch( 'medium', '<' ); - if ( isMobile ) return null; - - const popoverProps = { - className: classnames( - className, - 'block-editor-post-preview__dropdown-content' - ), - placement: 'bottom-end', - }; - const toggleProps = { - className: 'block-editor-post-preview__button-toggle', - disabled: ! isEnabled, - __experimentalIsFocusable: ! isEnabled, - children: viewLabel, - size: 'compact', - showTooltip: ! showIconLabels, - }; - const menuProps = { - 'aria-label': __( 'View options' ), - }; - - const deviceIcons = { - mobile, - tablet, - desktop, - }; +import deprecated from '@wordpress/deprecated'; - return ( - <DropdownMenu - className="block-editor-post-preview__dropdown" - popoverProps={ popoverProps } - toggleProps={ toggleProps } - menuProps={ menuProps } - icon={ deviceIcons[ deviceType.toLowerCase() ] } - label={ label || __( 'Preview' ) } - disableOpenOnArrowDown={ ! isEnabled } - > - { ( renderProps ) => ( - <> - <MenuGroup> - <MenuItem - className="block-editor-post-preview__button-resize" - onClick={ () => setDeviceType( 'Desktop' ) } - icon={ deviceType === 'Desktop' && check } - > - { __( 'Desktop' ) } - </MenuItem> - <MenuItem - className="block-editor-post-preview__button-resize" - onClick={ () => setDeviceType( 'Tablet' ) } - icon={ deviceType === 'Tablet' && check } - > - { __( 'Tablet' ) } - </MenuItem> - <MenuItem - className="block-editor-post-preview__button-resize" - onClick={ () => setDeviceType( 'Mobile' ) } - icon={ deviceType === 'Mobile' && check } - > - { __( 'Mobile' ) } - </MenuItem> - </MenuGroup> - { children?.( renderProps ) } - </> - ) } - </DropdownMenu> - ); +export default function PreviewOptions() { + deprecated( 'wp.blockEditor.PreviewOptions', { + version: '6.5', + } ); + return null; } diff --git a/packages/block-editor/src/components/preview-options/style.scss b/packages/block-editor/src/components/preview-options/style.scss deleted file mode 100644 index fb79926ba1dee9..00000000000000 --- a/packages/block-editor/src/components/preview-options/style.scss +++ /dev/null @@ -1,64 +0,0 @@ -.block-editor-post-preview__dropdown { - padding: 0; -} - -.block-editor-post-preview__button-resize.block-editor-post-preview__button-resize { - padding-left: $button-size-small + $grid-unit-10 + $grid-unit-10; - - &.has-icon { - padding-left: $grid-unit-10; - } -} - -.block-editor-post-preview__dropdown-content { - &.edit-post-post-preview-dropdown { - .components-menu-group { - &:first-child { - padding-bottom: $grid-unit-10; - } - &:last-child { - margin-bottom: 0; - } - } - } - - .components-menu-group + .components-menu-group { - padding: $grid-unit-10; - } -} - -.edit-post-header__settings, -.edit-site-header-edit-mode__actions { - @include break-small () { - .editor-post-preview { - display: none; - } - } -} - -// Reduced UI. -.edit-post-header.has-reduced-ui { - @include break-small() { - // Apply transition to first two buttons. - .edit-post-header__settings .editor-post-save-draft, - .edit-post-header__settings .editor-post-saved-state, - .edit-post-header__settings .block-editor-post-preview__button-toggle { - transition: opacity 0.1s linear; - @include reduce-motion("transition"); - } - - // Zero out opacity unless hovered. - &:not(:hover) { - .edit-post-header__settings .editor-post-save-draft, - .edit-post-header__settings .editor-post-saved-state, - .edit-post-header__settings .block-editor-post-preview__button-toggle { - opacity: 0; - } - - // ... or opened. - .edit-post-header__settings .block-editor-post-preview__button-toggle.is-opened { - opacity: 1; - } - } - } -} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 16de2dfdb71142..80489479724fff 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -59,7 +59,6 @@ @import "./components/block-toolbar/style.scss"; @import "./components/inserter/style.scss"; -@import "./components/preview-options/style.scss"; @import "./components/spacing-sizes-control/style.scss"; @include wordpress-admin-schemes(); diff --git a/packages/e2e-test-utils-playwright/src/editor/preview.ts b/packages/e2e-test-utils-playwright/src/editor/preview.ts index 97d3ef1d1d6603..c697ca714fe969 100644 --- a/packages/e2e-test-utils-playwright/src/editor/preview.ts +++ b/packages/e2e-test-utils-playwright/src/editor/preview.ts @@ -19,9 +19,7 @@ export async function openPreviewPage( this: Editor ): Promise< Page > { const editorTopBar = this.page.locator( 'role=region[name="Editor top bar"i]' ); - const previewButton = editorTopBar.locator( - 'role=button[name="Preview"i]' - ); + const previewButton = editorTopBar.locator( 'role=button[name="View"i]' ); await previewButton.click(); diff --git a/packages/e2e-test-utils/src/preview.js b/packages/e2e-test-utils/src/preview.js index 1d96eda1766751..24c5dc69dd0e2e 100644 --- a/packages/e2e-test-utils/src/preview.js +++ b/packages/e2e-test-utils/src/preview.js @@ -10,13 +10,13 @@ export async function openPreviewPage( editorPage = page ) { let openTabs = await browser.pages(); const expectedTabsCount = openTabs.length + 1; await page.waitForSelector( - '.block-editor-post-preview__button-toggle:not([disabled])' + '.editor-preview-dropdown__toggle:not([disabled])' ); - await editorPage.click( '.block-editor-post-preview__button-toggle' ); + await editorPage.click( '.editor-preview-dropdown__toggle' ); await editorPage.waitForSelector( - '.edit-post-header-preview__button-external' + '.editor-preview-dropdown__button-external' ); - await editorPage.click( '.edit-post-header-preview__button-external' ); + await editorPage.click( '.editor-preview-dropdown__button-external' ); // Wait for the new tab to open. while ( openTabs.length < expectedTabsCount ) { diff --git a/packages/edit-post/src/components/device-preview/index.js b/packages/edit-post/src/components/device-preview/index.js deleted file mode 100644 index 9fc95b943609d5..00000000000000 --- a/packages/edit-post/src/components/device-preview/index.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * WordPress dependencies - */ -import { Icon, MenuGroup } from '@wordpress/components'; -import { PostPreviewButton, store as editorStore } from '@wordpress/editor'; -import { external } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; -import { __experimentalPreviewOptions as PreviewOptions } from '@wordpress/block-editor'; -import { useDispatch, useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../store'; - -export default function DevicePreview() { - const { - hasActiveMetaboxes, - isPostSaveable, - isViewable, - deviceType, - showIconLabels, - } = useSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { getPostType } = select( coreStore ); - const postType = getPostType( getEditedPostAttribute( 'type' ) ); - - return { - hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), - isPostSaveable: select( editorStore ).isEditedPostSaveable(), - isViewable: postType?.viewable ?? false, - deviceType: select( editorStore ).getDeviceType(), - showIconLabels: - select( editPostStore ).isFeatureActive( 'showIconLabels' ), - }; - }, [] ); - const { setDeviceType } = useDispatch( editorStore ); - - return ( - <PreviewOptions - isEnabled={ isPostSaveable } - className="edit-post-post-preview-dropdown" - deviceType={ deviceType } - setDeviceType={ setDeviceType } - label={ __( 'Preview' ) } - showIconLabels={ showIconLabels } - > - { ( { onClose } ) => - isViewable && ( - <MenuGroup> - <div className="edit-post-header-preview__grouping-external"> - <PostPreviewButton - className="edit-post-header-preview__button-external" - role="menuitem" - forceIsAutosaveable={ hasActiveMetaboxes } - textContent={ - <> - { __( 'Preview in new tab' ) } - <Icon icon={ external } /> - </> - } - onPreview={ onClose } - /> - </div> - </MenuGroup> - ) - } - </PreviewOptions> - ); -} diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index b92c6c44fe49f3..c86b24b4b7ccf8 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -15,6 +15,7 @@ import { PostPreviewButton, store as editorStore, DocumentBar, + privateApis as editorPrivateApis, } from '@wordpress/editor'; import { useEffect, useRef, useState } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; @@ -36,10 +37,12 @@ import FullscreenModeClose from './fullscreen-mode-close'; import HeaderToolbar from './header-toolbar'; import MoreMenu from './more-menu'; import PostPublishButtonOrToggle from './post-publish-button-or-toggle'; -import { default as DevicePreview } from '../device-preview'; import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +const { PreviewDropdown } = unlock( editorPrivateApis ); const slideY = { hidden: { y: '-50px' }, @@ -180,8 +183,14 @@ function Header( { showIconLabels={ showIconLabels } /> ) } - <DevicePreview /> - <PostPreviewButton forceIsAutosaveable={ hasActiveMetaboxes } /> + <PreviewDropdown + showIconLabels={ showIconLabels } + forceIsAutosaveable={ hasActiveMetaboxes } + /> + <PostPreviewButton + className="edit-post-header__post-preview-button" + forceIsAutosaveable={ hasActiveMetaboxes } + /> <ViewLink /> <PostPublishButtonOrToggle forceIsDirty={ hasActiveMetaboxes } diff --git a/packages/edit-post/src/components/header/style.scss b/packages/edit-post/src/components/header/style.scss index 55450ba8f1de28..acd6dde411d928 100644 --- a/packages/edit-post/src/components/header/style.scss +++ b/packages/edit-post/src/components/header/style.scss @@ -127,29 +127,6 @@ gap: $grid-unit-10; } -.edit-post-header-preview__grouping-external { - display: flex; - position: relative; - padding-bottom: 0; -} - -.edit-post-header-preview__button-external { - padding-left: $grid-unit-10; - - margin-right: auto; - width: 100%; - display: flex; - justify-content: flex-start; - - svg { - margin-left: auto; - } -} - -.edit-post-post-preview-dropdown .components-popover__content { - padding-bottom: 0; -} - /** * Show icon labels. */ @@ -274,6 +251,12 @@ } } +.edit-post-header__post-preview-button { + @include break-small { + display: none; + } +} + .is-distraction-free { .interface-interface-skeleton__header { border-bottom: none; @@ -288,13 +271,13 @@ // hide some parts - & > .edit-post-header__settings > .editor-post-preview { + & > .edit-post-header__settings > .edit-post-header__post-preview-button { visibility: hidden; } & > .edit-post-header__toolbar .edit-post-header-toolbar__inserter-toggle, & > .edit-post-header__toolbar .edit-post-header-toolbar__document-overview-toggle, - & > .edit-post-header__settings > .block-editor-post-preview__dropdown, + & > .edit-post-header__settings > .editor-preview-dropdown, & > .edit-post-header__settings > .interface-pinned-items { display: none; } diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 2d751be691a745..a18c7e3a3eaad4 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -7,27 +7,26 @@ import classnames from 'classnames'; * WordPress dependencies */ import { useViewportMatch, useReducedMotion } from '@wordpress/compose'; -import { store as coreStore } from '@wordpress/core-data'; import { BlockToolbar, - __experimentalPreviewOptions as PreviewOptions, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect, useDispatch } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { useEffect, useRef, useState } from '@wordpress/element'; import { PinnedItems } from '@wordpress/interface'; import { __ } from '@wordpress/i18n'; -import { external, next, previous } from '@wordpress/icons'; +import { next, previous } from '@wordpress/icons'; import { Button, __unstableMotion as motion, - MenuGroup, - MenuItem, Popover, - VisuallyHidden, } from '@wordpress/components'; import { store as preferencesStore } from '@wordpress/preferences'; -import { DocumentBar, store as editorStore } from '@wordpress/editor'; +import { + DocumentBar, + store as editorStore, + privateApis as editorPrivateApis, +} from '@wordpress/editor'; /** * Internal dependencies @@ -43,14 +42,14 @@ import { import { unlock } from '../../lock-unlock'; import { FOCUSABLE_ENTITIES } from '../../utils/constants'; +const { PreviewDropdown } = unlock( editorPrivateApis ); + export default function HeaderEditMode( { setListViewToggleElement } ) { const { - deviceType, templateType, isDistractionFree, blockEditorMode, blockSelectionStart, - homeUrl, showIconLabels, editorCanvasView, hasFixedToolbar, @@ -59,9 +58,6 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { const { getEditedPostType } = select( editSiteStore ); const { getBlockSelectionStart, __unstableGetEditorMode } = select( blockEditorStore ); - const { - getUnstableBase, // Site index. - } = select( coreStore ); const { get: getPreference } = select( preferencesStore ); const { getDeviceType } = select( editorStore ); @@ -70,7 +66,6 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { templateType: getEditedPostType(), blockEditorMode: __unstableGetEditorMode(), blockSelectionStart: getBlockSelectionStart(), - homeUrl: getUnstableBase()?.home, showIconLabels: getPreference( editSiteStore.name, 'showIconLabels' @@ -93,7 +88,6 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { const isLargeViewport = useViewportMatch( 'medium' ); const isTopToolbar = ! isZoomOutMode && hasFixedToolbar && isLargeViewport; const blockToolbarRef = useRef(); - const { setDeviceType } = useDispatch( editorStore ); const disableMotion = useReducedMotion(); const hasDefaultEditorCanvasView = ! useHasEditorCanvasContainer(); @@ -215,34 +209,12 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { { 'is-zoomed-out': isZoomedOutView } ) } > - <PreviewOptions - deviceType={ deviceType } - setDeviceType={ setDeviceType } - label={ __( 'View' ) } - isEnabled={ - ! isFocusMode && hasDefaultEditorCanvasView - } + <PreviewDropdown showIconLabels={ showIconLabels } - > - { ( { onClose } ) => ( - <MenuGroup> - <MenuItem - href={ homeUrl } - target="_blank" - icon={ external } - onClick={ onClose } - > - { __( 'View site' ) } - <VisuallyHidden as="span"> - { - /* translators: accessibility text */ - __( '(opens in a new tab)' ) - } - </VisuallyHidden> - </MenuItem> - </MenuGroup> - ) } - </PreviewOptions> + disabled={ + isFocusMode || ! hasDefaultEditorCanvasView + } + /> </div> ) } <SaveButton /> diff --git a/packages/editor/src/components/preview-dropdown/index.js b/packages/editor/src/components/preview-dropdown/index.js new file mode 100644 index 00000000000000..b7d64f2eeebc63 --- /dev/null +++ b/packages/editor/src/components/preview-dropdown/index.js @@ -0,0 +1,136 @@ +/** + * WordPress dependencies + */ +import { useViewportMatch } from '@wordpress/compose'; +import { + DropdownMenu, + MenuGroup, + MenuItem, + VisuallyHidden, + Icon, +} from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { check, desktop, mobile, tablet, external } from '@wordpress/icons'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import PostPreviewButton from '../post-preview-button'; + +export default function PreviewDropdown( { + showIconLabels, + forceIsAutosaveable, + disabled, +} ) { + const { deviceType, homeUrl, isTemplate, isViewable } = useSelect( + ( select ) => { + const { getDeviceType, getCurrentPostType } = select( editorStore ); + const { getUnstableBase, getPostType } = select( coreStore ); + const _currentPostType = getCurrentPostType(); + return { + deviceType: getDeviceType(), + homeUrl: getUnstableBase()?.home, + isTemplate: _currentPostType === 'wp_template', + isViewable: getPostType( _currentPostType )?.viewable ?? false, + }; + }, + [] + ); + const { setDeviceType } = useDispatch( editorStore ); + const isMobile = useViewportMatch( 'medium', '<' ); + if ( isMobile ) return null; + + const popoverProps = { + placement: 'bottom-end', + }; + const toggleProps = { + className: 'editor-preview-dropdown__toggle', + size: 'compact', + showTooltip: ! showIconLabels, + disabled, + __experimentalIsFocusable: disabled, + }; + const menuProps = { + 'aria-label': __( 'View options' ), + }; + + const deviceIcons = { + mobile, + tablet, + desktop, + }; + + return ( + <DropdownMenu + className="editor-preview-dropdown" + popoverProps={ popoverProps } + toggleProps={ toggleProps } + menuProps={ menuProps } + icon={ deviceIcons[ deviceType.toLowerCase() ] } + label={ __( 'View' ) } + disableOpenOnArrowDown={ disabled } + > + { ( { onClose } ) => ( + <> + <MenuGroup> + <MenuItem + onClick={ () => setDeviceType( 'Desktop' ) } + icon={ deviceType === 'Desktop' && check } + > + { __( 'Desktop' ) } + </MenuItem> + <MenuItem + onClick={ () => setDeviceType( 'Tablet' ) } + icon={ deviceType === 'Tablet' && check } + > + { __( 'Tablet' ) } + </MenuItem> + <MenuItem + onClick={ () => setDeviceType( 'Mobile' ) } + icon={ deviceType === 'Mobile' && check } + > + { __( 'Mobile' ) } + </MenuItem> + </MenuGroup> + { isTemplate && ( + <MenuGroup> + <MenuItem + href={ homeUrl } + target="_blank" + icon={ external } + onClick={ onClose } + > + { __( 'View site' ) } + <VisuallyHidden as="span"> + { + /* translators: accessibility text */ + __( '(opens in a new tab)' ) + } + </VisuallyHidden> + </MenuItem> + </MenuGroup> + ) } + { isViewable && ( + <MenuGroup> + <PostPreviewButton + className="editor-preview-dropdown__button-external" + role="menuitem" + forceIsAutosaveable={ forceIsAutosaveable } + textContent={ + <> + { __( 'Preview in new tab' ) } + <Icon icon={ external } /> + </> + } + onPreview={ onClose } + /> + </MenuGroup> + ) } + </> + ) } + </DropdownMenu> + ); +} diff --git a/packages/editor/src/components/preview-dropdown/style.scss b/packages/editor/src/components/preview-dropdown/style.scss new file mode 100644 index 00000000000000..43fa7cdd8ecd97 --- /dev/null +++ b/packages/editor/src/components/preview-dropdown/style.scss @@ -0,0 +1,5 @@ +.editor-preview-dropdown__button-external { + width: 100%; + display: flex; + justify-content: space-between; +} diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index 046feee5b9c3f6..ac5bd4324946ee 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -7,6 +7,7 @@ import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; import useBlockEditorSettings from './components/provider/use-block-editor-settings'; import PostPanelRow from './components/post-panel-row'; +import PreviewDropdown from './components/preview-dropdown'; export const privateApis = {}; lock( privateApis, { @@ -14,6 +15,7 @@ lock( privateApis, { ExperimentalEditorProvider, EntitiesSavedStatesExtensible, PostPanelRow, + PreviewDropdown, // This is a temporary private API while we're updating the site editor to use EditorProvider. useBlockEditorSettings, diff --git a/packages/editor/src/style.scss b/packages/editor/src/style.scss index 01e17a1a964ab0..50359984af1628 100644 --- a/packages/editor/src/style.scss +++ b/packages/editor/src/style.scss @@ -23,5 +23,6 @@ @import "./components/post-url/style.scss"; @import "./components/post-visibility/style.scss"; @import "./components/post-trash/style.scss"; +@import "./components/preview-dropdown/style.scss"; @import "./components/table-of-contents/style.scss"; @import "./components/template-validation-notice/style.scss"; diff --git a/test/e2e/specs/editor/plugins/block-context.spec.js b/test/e2e/specs/editor/plugins/block-context.spec.js index 1fc91debd1145f..c819f29bc7383d 100644 --- a/test/e2e/specs/editor/plugins/block-context.spec.js +++ b/test/e2e/specs/editor/plugins/block-context.spec.js @@ -64,7 +64,11 @@ test.describe( 'Block context', () => { .fill( '123' ); await editorPage - .getByRole( 'button', { name: 'Preview', expanded: false } ) + .getByRole( 'button', { + name: 'View', + expanded: false, + exact: true, + } ) .click(); await editorPage .getByRole( 'menuitem', { name: 'Preview in new tab' } ) diff --git a/test/e2e/specs/editor/various/new-post.spec.js b/test/e2e/specs/editor/various/new-post.spec.js index cc0243eb8e6312..b3591db1ec50b9 100644 --- a/test/e2e/specs/editor/various/new-post.spec.js +++ b/test/e2e/specs/editor/various/new-post.spec.js @@ -32,9 +32,9 @@ test.describe( 'new editor state', () => { await expect( title ).toBeEditable(); await expect( title ).toHaveText( '' ); - // Should display the Preview button. + // Should display the View button. await expect( - page.locator( 'role=button[name="Preview"i]' ) + page.locator( 'role=button[name="View"i]' ) ).toBeVisible(); // Should display the Post Formats UI. diff --git a/test/e2e/specs/editor/various/preview.spec.js b/test/e2e/specs/editor/various/preview.spec.js index 0666de1405fae1..0657a45567baf5 100644 --- a/test/e2e/specs/editor/various/preview.spec.js +++ b/test/e2e/specs/editor/various/preview.spec.js @@ -22,11 +22,6 @@ test.describe( 'Preview', () => { } ) => { const editorPage = page; - // Disabled until content present. - await expect( - editorPage.locator( 'role=button[name="Preview"i]' ) - ).toBeDisabled(); - await editor.canvas .locator( 'role=textbox[name="Add title"i]' ) .type( 'Hello World' ); @@ -301,7 +296,7 @@ test.describe( 'Preview with private custom post type', () => { } ); // Open the view menu. - await page.click( 'role=button[name="Preview"i]' ); + await page.click( 'role=button[name="View"i]' ); await expect( page.locator( 'role=menuitem[name="Preview in new tab"i]' ) @@ -316,7 +311,7 @@ class PreviewUtils { async waitForPreviewNavigation( previewPage ) { const previewToggle = this.page.locator( - 'role=button[name="Preview"i][expanded=false]' + 'role=button[name="View"i][expanded=false]' ); const isDropdownClosed = await previewToggle.isVisible(); if ( isDropdownClosed ) { From 22c7c1629c5a2a5f7ba06c5602b76c4d71fba7e5 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Mon, 11 Dec 2023 14:09:59 +0100 Subject: [PATCH 119/325] Create-block-interactive-template: Prevent crash when Gutenberg plugin is not installed (#56941) * Prevent crash when Gutenberg plugin is not installed * Update changelog --- .../create-block-interactive-template/CHANGELOG.md | 2 ++ .../block-templates/render.php.mustache | 4 +++- .../plugin-templates/$slug.php.mustache | 14 ++++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 72ed6677e0b4ed..735790d07b803e 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +- Prevent crash when Gutenberg plugin is not installed. [#56941](https://github.com/WordPress/gutenberg/pull/56941) + ## 1.10.1 (2023-12-07) - Update template to use modules instead of scripts. [#56694](https://github.com/WordPress/gutenberg/pull/56694) diff --git a/packages/create-block-interactive-template/block-templates/render.php.mustache b/packages/create-block-interactive-template/block-templates/render.php.mustache index 01cbe6ed83cfb5..0f6883a9362407 100644 --- a/packages/create-block-interactive-template/block-templates/render.php.mustache +++ b/packages/create-block-interactive-template/block-templates/render.php.mustache @@ -15,7 +15,9 @@ $unique_id = wp_unique_id( 'p-' ); // Enqueue the view file. -gutenberg_enqueue_module( '{{namespace}}-view' ); +if (function_exists('gutenberg_enqueue_module')) { + gutenberg_enqueue_module( '{{namespace}}-view' ); +} ?> <div diff --git a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache index 73726b930e4728..81e6b035b1d737 100644 --- a/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache +++ b/packages/create-block-interactive-template/plugin-templates/$slug.php.mustache @@ -44,11 +44,13 @@ if ( ! defined( 'ABSPATH' ) ) { function {{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init() { register_block_type( __DIR__ . '/build' ); - gutenberg_register_module( - '{{namespace}}-view', - plugin_dir_url( __FILE__ ) . 'src/view.js', - array( '@wordpress/interactivity' ), - '{{version}}' - ); + if (function_exists('gutenberg_register_module')) { + gutenberg_register_module( + '{{namespace}}-view', + plugin_dir_url( __FILE__ ) . 'src/view.js', + array( '@wordpress/interactivity' ), + '{{version}}' + ); + } } add_action( 'init', '{{namespaceSnakeCase}}_{{slugSnakeCase}}_block_init' ); From d1b2abff85e2d97169f3e65c878c5827cae33db8 Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Mon, 11 Dec 2023 14:02:36 +0000 Subject: [PATCH 120/325] Add scroll padding to dataviews container (#56946) --- packages/dataviews/src/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index ab6c052e3a8980..3ff82d32382661 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -3,6 +3,7 @@ height: 100%; overflow: auto; box-sizing: border-box; + scroll-padding-bottom: $grid-unit-80; > div { min-height: 100%; From c0b18b08cb32aa3c766f7ea4a872544cca310506 Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Mon, 11 Dec 2023 16:08:11 +0100 Subject: [PATCH 121/325] [Mobile] Fix crash when appending media (Android) (#56791) * Mobile - Fix subscribeMediaAppend to skip adding unsupported types of files, it currently supports images and video only * Mock requestMediaImport * Mobile - Edit Post tests - Remove old unsupported block test and adds tests for subscribeMediaAppend * Update Changelog --- .../test/__snapshots__/editor.native.js.snap | 21 +++ packages/edit-post/src/test/editor.native.js | 146 +++++++++++------- .../src/components/provider/index.native.js | 38 +++-- packages/react-native-editor/CHANGELOG.md | 1 + test/native/setup.js | 1 + 5 files changed, 139 insertions(+), 68 deletions(-) create mode 100644 packages/edit-post/src/test/__snapshots__/editor.native.js.snap diff --git a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap new file mode 100644 index 00000000000000..8b820cd38f11bd --- /dev/null +++ b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Editor appends media correctly for allowed types 1`] = ` +"<!-- wp:image --> +<figure class="wp-block-image"><img src="https://test-site.files.wordpress.com/local-image-1.jpeg" alt=""/></figure> +<!-- /wp:image --> + +<!-- wp:image --> +<figure class="wp-block-image"><img src="https://test-site.files.wordpress.com/local-image-3.jpeg" alt=""/></figure> +<!-- /wp:image -->" +`; + +exports[`Editor appends media correctly for allowed types and skips unsupported ones 1`] = ` +"<!-- wp:image --> +<figure class="wp-block-image"><img src="https://test-site.files.wordpress.com/local-image-1.jpeg" alt=""/></figure> +<!-- /wp:image --> + +<!-- wp:video --> +<figure class="wp-block-video"><video controls src="file:///local-video-4.mp4"></video></figure> +<!-- /wp:video -->" +`; diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index 4911fb5128655c..7faeb2e51ab4b2 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -6,93 +6,127 @@ import { addBlock, fireEvent, getBlock, + getEditorHtml, initializeEditor, - render, + screen, setupCoreBlocks, } from 'test/helpers'; /** * WordPress dependencies */ -import RNReactNativeGutenbergBridge, { +import { + requestMediaImport, + subscribeMediaAppend, subscribeParentToggleHTMLMode, } from '@wordpress/react-native-bridge'; -// Force register 'core/editor' store. -import { store } from '@wordpress/editor'; // eslint-disable-line no-unused-vars - -/** - * Internal dependencies - */ -import '..'; -import Editor from '../editor'; -const unsupportedBlock = ` -<!-- wp:notablock --> -<p>Not supported</p> -<!-- /wp:notablock --> -`; +setupCoreBlocks(); -beforeAll( () => { - jest.useFakeTimers( { legacyFakeTimers: true } ); +let toggleModeCallback; +subscribeParentToggleHTMLMode.mockImplementation( ( callback ) => { + toggleModeCallback = callback; } ); -afterAll( () => { - jest.runOnlyPendingTimers(); - jest.useRealTimers(); +let mediaAppendCallback; +subscribeMediaAppend.mockImplementation( ( callback ) => { + mediaAppendCallback = callback; } ); -setupCoreBlocks(); +const MEDIA = [ + { + localId: 1, + mediaUrl: 'file:///local-image-1.jpeg', + mediaType: 'image', + serverId: 2000, + serverUrl: 'https://test-site.files.wordpress.com/local-image-1.jpeg', + }, + { + localId: 2, + mediaUrl: 'file:///local-file-1.pdf', + mediaType: 'other', + serverId: 2001, + serverUrl: 'https://test-site.files.wordpress.com/local-file-1.pdf', + }, + { + localId: 3, + mediaUrl: 'file:///local-image-3.jpeg', + mediaType: 'image', + serverId: 2002, + serverUrl: 'https://test-site.files.wordpress.com/local-image-3.jpeg', + }, + { + localId: 4, + mediaUrl: 'file:///local-video-4.mp4', + mediaType: 'video', + serverId: 2003, + serverUrl: 'https://test-site.files.wordpress.com/local-video-4.mp4', + }, +]; describe( 'Editor', () => { - it( 'detects unsupported block and sends hasUnsupportedBlocks true to native', () => { - RNReactNativeGutenbergBridge.editorDidMount = jest.fn(); - - const appContainer = renderEditorWith( unsupportedBlock ); - // For some reason resetEditorBlocks() is asynchronous when dispatching editEntityRecord. - act( () => { - jest.runAllTicks(); - } ); - appContainer.unmount(); - - expect( - RNReactNativeGutenbergBridge.editorDidMount - ).toHaveBeenCalledTimes( 1 ); - expect( - RNReactNativeGutenbergBridge.editorDidMount - ).toHaveBeenCalledWith( [ 'core/notablock' ] ); + afterEach( () => { + jest.clearAllMocks(); } ); it( 'toggles the editor from Visual to HTML mode', async () => { // Arrange - let toggleMode; - subscribeParentToggleHTMLMode.mockImplementation( ( callback ) => { - toggleMode = callback; - } ); - const screen = await initializeEditor(); + await initializeEditor(); await addBlock( screen, 'Paragraph' ); // Act const paragraphBlock = getBlock( screen, 'Paragraph' ); fireEvent.press( paragraphBlock ); act( () => { - toggleMode(); + toggleModeCallback(); } ); // Assert const htmlEditor = screen.getByLabelText( 'html-view-content' ); expect( htmlEditor ).toBeVisible(); + + act( () => { + toggleModeCallback(); + } ); } ); -} ); -// Utilities. -const renderEditorWith = ( content ) => { - return render( - <Editor - initialHtml={ content } - initialHtmlModeEnabled={ false } - initialTitle={ '' } - postType="post" - postId="1" - /> - ); -}; + it( 'appends media correctly for allowed types', async () => { + // Arrange + requestMediaImport + .mockImplementationOnce( ( _, callback ) => + callback( MEDIA[ 0 ].id, MEDIA[ 0 ].serverUrl ) + ) + .mockImplementationOnce( ( _, callback ) => + callback( MEDIA[ 2 ].id, MEDIA[ 2 ].serverUrl ) + ); + await initializeEditor(); + + // Act + await act( () => mediaAppendCallback( MEDIA[ 0 ] ) ); + await act( () => mediaAppendCallback( MEDIA[ 2 ] ) ); + + // Assert + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'appends media correctly for allowed types and skips unsupported ones', async () => { + // Arrange + requestMediaImport + .mockImplementationOnce( ( _, callback ) => + callback( MEDIA[ 0 ].id, MEDIA[ 0 ].serverUrl ) + ) + .mockImplementationOnce( ( _, callback ) => + callback( MEDIA[ 3 ].id, MEDIA[ 3 ].serverUrl ) + ); + await initializeEditor(); + + // Act + await act( () => mediaAppendCallback( MEDIA[ 0 ] ) ); + // Unsupported type (PDF file) + await act( () => mediaAppendCallback( MEDIA[ 1 ] ) ); + await act( () => mediaAppendCallback( MEDIA[ 3 ] ) ); + + // Assert + expect( getEditorHtml() ).toMatchSnapshot(); + } ); +} ); diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 5fd6a4cdbb888b..bbd710f031c849 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -27,6 +27,7 @@ import { parse, serialize, getUnregisteredTypeHandlerName, + getBlockType, createBlock, } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; @@ -35,6 +36,7 @@ import { applyFilters } from '@wordpress/hooks'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { getGlobalStyles, getColorsAndGradients } from '@wordpress/components'; import { NEW_BLOCK_TYPES } from '@wordpress/block-library'; +import { __ } from '@wordpress/i18n'; const postTypeEntities = [ { name: 'post', baseURL: '/wp/v2/posts' }, @@ -94,6 +96,7 @@ class NativeEditorProvider extends Component { componentDidMount() { const { capabilities, + createErrorNotice, locale, hostAppNamespace, updateEditorSettings, @@ -136,17 +139,26 @@ class NativeEditorProvider extends Component { this.subscriptionParentMediaAppend = subscribeMediaAppend( ( payload ) => { const blockName = 'core/' + payload.mediaType; - const newBlock = createBlock( blockName, { - id: payload.mediaId, - [ payload.mediaType === 'image' ? 'url' : 'src' ]: - payload.mediaUrl, - } ); - - const indexAfterSelected = this.props.selectedBlockIndex + 1; - const insertionIndex = - indexAfterSelected || this.props.blockCount; - - this.props.insertBlock( newBlock, insertionIndex ); + const blockType = getBlockType( blockName ); + + if ( blockType && blockType?.name ) { + const newBlock = createBlock( blockType.name, { + id: payload.mediaId, + [ payload.mediaType === 'image' ? 'url' : 'src' ]: + payload.mediaUrl, + } ); + + const indexAfterSelected = + this.props.selectedBlockIndex + 1; + const insertionIndex = + indexAfterSelected || this.props.blockCount; + + this.props.insertBlock( newBlock, insertionIndex ); + } else { + createErrorNotice( + __( 'File type not supported as a media file.' ) + ); + } } ); @@ -389,7 +401,8 @@ const ComposedNativeProvider = compose( [ dispatch( blockEditorStore ); const { switchEditorMode } = dispatch( editPostStore ); const { addEntities, receiveEntityRecords } = dispatch( coreStore ); - const { createSuccessNotice } = dispatch( noticesStore ); + const { createSuccessNotice, createErrorNotice } = + dispatch( noticesStore ); return { updateBlockEditorSettings: updateSettings, @@ -397,6 +410,7 @@ const ComposedNativeProvider = compose( [ addEntities, insertBlock, createSuccessNotice, + createErrorNotice, editTitle( title ) { editPost( { title } ); }, diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 791b39fee2c7f8..83b4800fe2a9de 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -13,6 +13,7 @@ For each user feature we should also add a importance categorization label to i - [*] [internal] Move InserterButton from components package to block-editor package [#56494] - [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] - [*] Guard against an Image block styles crash due to null block values [#56903] +- [**] Fix crash when sharing unsupported media types on Android [#56791] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] diff --git a/test/native/setup.js b/test/native/setup.js index 53ab28f861a1ef..3770a4ce3efc6f 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -117,6 +117,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { requestImageUploadCancelDialog: jest.fn(), requestMediaEditor: jest.fn(), requestMediaPicker: jest.fn(), + requestMediaImport: jest.fn(), requestUnsupportedBlockFallback: jest.fn(), subscribeReplaceBlock: jest.fn(), mediaSources: { From 93349ff2ba3010baf4957f575a9a6a8672ec98c5 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Mon, 11 Dec 2023 18:03:33 +0200 Subject: [PATCH 122/325] DataViews: Hide pagination if we have only one page (#56948) --- packages/dataviews/src/pagination.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index 25672208d993ca..1c41691a13d0a3 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -36,7 +36,7 @@ function Pagination( { ) } </Text> - { !! totalItems && ( + { !! totalItems && totalPages !== 1 && ( <HStack expanded={ false } spacing={ 3 }> <HStack justify="flex-start" From 275e8331e026968e0eb7dba7ae01c1b8decb48f3 Mon Sep 17 00:00:00 2001 From: Birgit Pauli-Haack <birgit.pauli@gmail.com> Date: Mon, 11 Dec 2023 19:17:45 +0100 Subject: [PATCH 123/325] Doc: Search Control - add Storybook link (#56815) * Update README.md with Storybook link * Fixed grammar * removed 'also' --- packages/components/src/search-control/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/search-control/README.md b/packages/components/src/search-control/README.md index bd12580f3c8780..07a18f07130ce0 100644 --- a/packages/components/src/search-control/README.md +++ b/packages/components/src/search-control/README.md @@ -8,6 +8,8 @@ SearchControl components let users display a search control. 1. [Development guidelines](#development-guidelines) 2. [Related components](#related-components) +Check out the [Storybook page](https://wordpress.github.io/gutenberg/?path=/docs/components-searchcontrol--docs) for a visual exploration of this component. + ## Development guidelines ### Usage From 1ed4defe122c7bae94f06e90f6e1850c1c684d84 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Mon, 11 Dec 2023 12:06:41 -0800 Subject: [PATCH 124/325] `CustomSelectControl`: add additional unit tests (#56575) * `CustomSelectControl`: add additional unit tests * Update tests based on PR feedback * Update naming * Add an extra selection to bolster test * Test both uncontrolled and controlled * Add test for custom event handlers * Add alliterative options for testing character matches * Add tests for styles and classes * Make tests more dynamic * Adjust specificity of button queries to check for expanded value * Add label and adjust listbox queries * Add additional assertions for focus * Update test to include all items from options --- .../src/custom-select-control/test/index.js | 402 ++++++++++++++++-- 1 file changed, 367 insertions(+), 35 deletions(-) diff --git a/packages/components/src/custom-select-control/test/index.js b/packages/components/src/custom-select-control/test/index.js index 150afe4aa75f51..52bb841a4f953e 100644 --- a/packages/components/src/custom-select-control/test/index.js +++ b/packages/components/src/custom-select-control/test/index.js @@ -4,54 +4,205 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +/** + * WordPress dependencies + */ +import { useState } from '@wordpress/element'; + /** * Internal dependencies */ import CustomSelectControl from '..'; -describe( 'CustomSelectControl', () => { - it( 'Captures the keypress event and does not let it propagate', async () => { - const user = userEvent.setup(); - const onKeyDown = jest.fn(); - const options = [ - { - key: 'one', - name: 'Option one', - }, - { - key: 'two', - name: 'Option two', - }, - { - key: 'three', - name: 'Option three', +const customClass = 'amber-skies'; + +const props = { + label: 'label!', + options: [ + { + key: 'flower1', + name: 'violets', + }, + { + key: 'flower2', + name: 'crimson clover', + className: customClass, + }, + { + key: 'flower3', + name: 'poppy', + }, + { + key: 'color1', + name: 'amber', + className: customClass, + }, + { + key: 'color2', + name: 'aquamarine', + style: { + backgroundColor: 'rgb(127, 255, 212)', + rotate: '13deg', }, - ]; + }, + ], + __nextUnconstrainedWidth: true, +}; - render( - <div - // This role="none" is required to prevent an eslint warning about accessibility. - role="none" - onKeyDown={ onKeyDown } - > - <CustomSelectControl - options={ options } - __nextUnconstrainedWidth - /> - </div> +const ControlledCustomSelectControl = ( { options } ) => { + const [ value, setValue ] = useState( options[ 0 ] ); + return ( + <CustomSelectControl + { ...props } + onChange={ ( { selectedItem } ) => setValue( selectedItem ) } + value={ options.find( ( option ) => option.key === value.key ) } + /> + ); +}; + +describe.each( [ + [ 'uncontrolled', CustomSelectControl ], + [ 'controlled', ControlledCustomSelectControl ], +] )( 'CustomSelectControl %s', ( ...modeAndComponent ) => { + const [ , Component ] = modeAndComponent; + + it( 'Should replace the initial selection when a new item is selected', async () => { + const user = userEvent.setup(); + + render( <Component { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.click( currentSelectedItem ); + + await user.click( + screen.getByRole( 'option', { + name: 'crimson clover', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + + await user.click( currentSelectedItem ); + + await user.click( + screen.getByRole( 'option', { + name: 'poppy', + } ) + ); + + expect( currentSelectedItem ).toHaveTextContent( 'poppy' ); + } ); + + it( 'Should keep current selection if dropdown is closed without changing selection', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.tab(); + await user.keyboard( '{enter}' ); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toBeVisible(); + + await user.keyboard( '{escape}' ); + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + } ) + ).not.toBeInTheDocument(); + + expect( currentSelectedItem ).toHaveTextContent( + props.options[ 0 ].name + ); + } ); + + it( 'Should apply class only to options that have a className defined', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + await user.click( + screen.getByRole( 'button', { + expanded: false, + } ) + ); + + // return an array of items _with_ a className added + const itemsWithClass = props.options.filter( + ( option ) => option.className !== undefined + ); + + // assert against filtered array + itemsWithClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveClass( + customClass + ) + ); + + // return an array of items _without_ a className added + const itemsWithoutClass = props.options.filter( + ( option ) => option.className === undefined + ); + + // assert against filtered array + itemsWithoutClass.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveClass( + customClass + ) + ); + } ); + + it( 'Should apply styles only to options that have styles defined', async () => { + const user = userEvent.setup(); + const customStyles = + 'background-color: rgb(127, 255, 212); rotate: 13deg;'; + + render( <CustomSelectControl { ...props } /> ); + + await user.click( + screen.getByRole( 'button', { + expanded: false, + } ) + ); + + // return an array of items _with_ styles added + const styledItems = props.options.filter( + ( option ) => option.style !== undefined ); - const toggleButton = screen.getByRole( 'button' ); - await user.click( toggleButton ); - const customSelect = screen.getByRole( 'listbox' ); - await user.type( customSelect, '{enter}' ); + // assert against filtered array + styledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).toHaveStyle( + customStyles + ) + ); + + // return an array of items _without_ styles added + const unstyledItems = props.options.filter( + ( option ) => option.style === undefined + ); - expect( onKeyDown ).toHaveBeenCalledTimes( 0 ); + // assert against filtered array + unstyledItems.map( ( { name } ) => + expect( screen.getByRole( 'option', { name } ) ).not.toHaveStyle( + customStyles + ) + ); } ); it( 'does not show selected hint by default', () => { render( <CustomSelectControl + { ...props } label="Custom select" options={ [ { @@ -60,7 +211,6 @@ describe( 'CustomSelectControl', () => { __experimentalHint: 'Hint', }, ] } - __nextUnconstrainedWidth /> ); expect( @@ -71,6 +221,7 @@ describe( 'CustomSelectControl', () => { it( 'shows selected hint when __experimentalShowSelectedHint is set', () => { render( <CustomSelectControl + { ...props } label="Custom select" options={ [ { @@ -80,11 +231,192 @@ describe( 'CustomSelectControl', () => { }, ] } __experimentalShowSelectedHint - __nextUnconstrainedWidth /> ); expect( screen.getByRole( 'button', { name: 'Custom select' } ) ).toHaveTextContent( 'Hint' ); } ); + + describe( 'Keyboard behavior and accessibility', () => { + it( 'Captures the keypress event and does not let it propagate', async () => { + const user = userEvent.setup(); + const onKeyDown = jest.fn(); + + render( + <div + // This role="none" is required to prevent an eslint warning about accessibility. + role="none" + onKeyDown={ onKeyDown } + > + <CustomSelectControl { ...props } /> + </div> + ); + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + await user.click( currentSelectedItem ); + + const customSelect = screen.getByRole( 'listbox', { + name: 'label!', + } ); + await user.type( customSelect, '{enter}' ); + + expect( onKeyDown ).toHaveBeenCalledTimes( 0 ); + } ); + + it( 'Should be able to change selection using keyboard', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await user.keyboard( '{enter}' ); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await user.keyboard( '{arrowdown}' ); + await user.keyboard( '{enter}' ); + + expect( currentSelectedItem ).toHaveTextContent( 'crimson clover' ); + } ); + + it( 'Should be able to type characters to select matching options', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.tab(); + await user.keyboard( '{enter}' ); + expect( + screen.getByRole( 'listbox', { + name: 'label!', + } ) + ).toHaveFocus(); + + await user.keyboard( '{a}' ); + await user.keyboard( '{enter}' ); + expect( currentSelectedItem ).toHaveTextContent( 'amber' ); + } ); + + it( 'Can change selection with a focused input and closed dropdown if typed characters match an option', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.tab(); + expect( currentSelectedItem ).toHaveFocus(); + + await user.keyboard( '{a}' ); + await user.keyboard( '{q}' ); + + expect( + screen.queryByRole( 'listbox', { + name: 'label!', + hidden: true, + } ) + ).not.toBeInTheDocument(); + + await user.keyboard( '{enter}' ); + expect( currentSelectedItem ).toHaveTextContent( 'aquamarine' ); + } ); + + it( 'Should have correct aria-selected value for selections', async () => { + const user = userEvent.setup(); + + render( <CustomSelectControl { ...props } /> ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.click( currentSelectedItem ); + + // get all items except for first option + const unselectedItems = props.options.filter( + ( { name } ) => name !== props.options[ 0 ].name + ); + + // assert that all other items have aria-selected="false" + unselectedItems.map( ( { name } ) => + expect( + screen.getByRole( 'option', { name, selected: false } ) + ).toBeVisible() + ); + + // assert that first item has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: props.options[ 0 ].name, + selected: true, + } ) + ).toBeVisible(); + + // change the current selection + await user.click( screen.getByRole( 'option', { name: 'poppy' } ) ); + + // click button to mount listbox with options again + await user.click( currentSelectedItem ); + + // check that first item is has aria-selected="false" after new selection + expect( + screen.getByRole( 'option', { + name: props.options[ 0 ].name, + selected: false, + } ) + ).toBeVisible(); + + // check that new selected item now has aria-selected="true" + expect( + screen.getByRole( 'option', { + name: 'poppy', + selected: true, + } ) + ).toBeVisible(); + } ); + + it( 'Should call custom event handlers', async () => { + const user = userEvent.setup(); + const onFocusMock = jest.fn(); + const onBlurMock = jest.fn(); + + render( + <CustomSelectControl + { ...props } + onFocus={ onFocusMock } + onBlur={ onBlurMock } + /> + ); + + const currentSelectedItem = screen.getByRole( 'button', { + expanded: false, + } ); + + await user.tab(); + + expect( currentSelectedItem ).toHaveFocus(); + expect( onFocusMock ).toHaveBeenCalledTimes( 1 ); + + await user.tab(); + expect( currentSelectedItem ).not.toHaveFocus(); + expect( onBlurMock ).toHaveBeenCalledTimes( 1 ); + } ); + } ); } ); From d617e5c55c43fcf60b56c93755bd005406df4923 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Dec 2023 22:19:37 +0100 Subject: [PATCH 125/325] Block editor: hooks: avoid BlockEdit filter for content locking UI (#56957) --- .../block-editor/src/hooks/content-lock-ui.js | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/packages/block-editor/src/hooks/content-lock-ui.js b/packages/block-editor/src/hooks/content-lock-ui.js index 8f95c14d118e56..1bcd0d7ce80163 100644 --- a/packages/block-editor/src/hooks/content-lock-ui.js +++ b/packages/block-editor/src/hooks/content-lock-ui.js @@ -2,9 +2,7 @@ * WordPress dependencies */ import { ToolbarButton, MenuItem } from '@wordpress/components'; -import { createHigherOrderComponent, pure } from '@wordpress/compose'; import { useDispatch, useSelect } from '@wordpress/data'; -import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef, useCallback } from '@wordpress/element'; @@ -147,28 +145,9 @@ function ContentLockControlsPure( { clientId, isSelected } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -const ContentLockControls = pure( ContentLockControlsPure ); - -export const withContentLockControls = createHigherOrderComponent( - ( BlockEdit ) => ( props ) => { - return ( - <> - <ContentLockControls - clientId={ props.clientId } - isSelected={ props.isSelected } - /> - <BlockEdit key="edit" { ...props } /> - </> - ); +export default { + edit: ContentLockControlsPure, + hasSupport() { + return true; }, - 'withContentLockControls' -); - -addFilter( - 'editor.BlockEdit', - 'core/content-lock-ui/with-block-controls', - withContentLockControls -); +}; From 5ea1a8f1348fb45c090da697461e8bb67f259086 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 11 Dec 2023 23:51:48 +0100 Subject: [PATCH 126/325] Media upload component: lazy mount (#56958) --- .../src/components/media-upload/index.js | 67 +++++++++---------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/packages/media-utils/src/components/media-upload/index.js b/packages/media-utils/src/components/media-upload/index.js index bf2da5c470a5ef..c62f755a27fb53 100644 --- a/packages/media-utils/src/components/media-upload/index.js +++ b/packages/media-utils/src/components/media-upload/index.js @@ -224,45 +224,13 @@ const getAttachmentsCollection = ( ids ) => { }; class MediaUpload extends Component { - constructor( { - allowedTypes, - gallery = false, - unstableFeaturedImageFlow = false, - modalClass, - multiple = false, - title = __( 'Select or Upload Media' ), - } ) { + constructor() { super( ...arguments ); this.openModal = this.openModal.bind( this ); this.onOpen = this.onOpen.bind( this ); this.onSelect = this.onSelect.bind( this ); this.onUpdate = this.onUpdate.bind( this ); this.onClose = this.onClose.bind( this ); - - const { wp } = window; - - if ( gallery ) { - this.buildAndSetGalleryFrame(); - } else { - const frameConfig = { - title, - multiple, - }; - if ( !! allowedTypes ) { - frameConfig.library = { type: allowedTypes }; - } - - this.frame = wp.media( frameConfig ); - } - - if ( modalClass ) { - this.frame.$el.addClass( modalClass ); - } - - if ( unstableFeaturedImageFlow ) { - this.buildAndSetFeatureImageFrame(); - } - this.initializeListeners(); } initializeListeners() { @@ -348,7 +316,7 @@ class MediaUpload extends Component { } componentWillUnmount() { - this.frame.remove(); + this.frame?.remove(); } onUpdate( selections ) { @@ -444,9 +412,38 @@ class MediaUpload extends Component { } openModal() { - if ( this.props.gallery ) { + const { + allowedTypes, + gallery = false, + unstableFeaturedImageFlow = false, + modalClass, + multiple = false, + title = __( 'Select or Upload Media' ), + } = this.props; + const { wp } = window; + + if ( gallery ) { this.buildAndSetGalleryFrame(); + } else { + const frameConfig = { + title, + multiple, + }; + if ( !! allowedTypes ) { + frameConfig.library = { type: allowedTypes }; + } + + this.frame = wp.media( frameConfig ); + } + + if ( modalClass ) { + this.frame.$el.addClass( modalClass ); + } + + if ( unstableFeaturedImageFlow ) { + this.buildAndSetFeatureImageFrame(); } + this.initializeListeners(); this.frame.open(); } From 9080cb62538898dabd10e2ab87840a55dec0ae45 Mon Sep 17 00:00:00 2001 From: Andy Peatling <apeatling@users.noreply.github.com> Date: Mon, 11 Dec 2023 14:59:39 -0800 Subject: [PATCH 127/325] Add storybook link. (#56953) --- packages/components/src/spinner/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/components/src/spinner/README.md b/packages/components/src/spinner/README.md index 474e6ebcb0a81a..64f596f22032fa 100644 --- a/packages/components/src/spinner/README.md +++ b/packages/components/src/spinner/README.md @@ -17,3 +17,5 @@ function Example() { The spinner component should: - Signal to users that the processing of their request is underway and will soon complete. + +Check out the [Storybook page](https://wordpress.github.io/gutenberg/?path=/docs/components-spinner--docs) for a visual exploration of this component. From 65486eca93778cf6bb8e7f4c3930acac7b1b6bc0 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Mon, 11 Dec 2023 18:49:40 -0500 Subject: [PATCH 128/325] feat: Add Hook to monitor network connectivity status (#56861) * feat: Frame useNetInfo hook foundation This code is non-functioning currently. * feat: Add iOS connection status bridge utilities This bridge will be required for the planned JavaScript Hook to monitor connection status. * feat: Add `useIsConnected` hook Provides React Hook for monitoring the network connection status via the bridge to the host app. * Revert "feat: Frame useNetInfo hook foundation" This reverts commit a8d3660845457787f6b368fe0276bfcfdbd213a6. * refactor: Align with project Swift syntax Semicolon is unnecessary. Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> * feat: Add Android connection status bridge utilities This bridge enables monitoring the connection status on Android. * feat: Android network connection status request utility Allow the Android platform to request the current network connection status. * fix: Add missing `requestConnectionStatus` bridge method mock The Demo editor fails to build without a mocked bridge method. --------- Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> --- .../GutenbergBridgeJS2Parent.java | 6 +++ .../RNReactNativeGutenbergBridgeModule.java | 17 +++++++ .../WPAndroidGlue/DeferredEventEmitter.java | 9 ++++ .../WPAndroidGlue/WPAndroidGlueCode.java | 17 +++++++ packages/react-native-bridge/index.js | 48 +++++++++++++++++++ .../react-native-bridge/ios/Gutenberg.swift | 5 ++ .../ios/GutenbergBridgeDelegate.swift | 2 + .../ios/RNReactNativeGutenbergBridge.m | 1 + .../ios/RNReactNativeGutenbergBridge.swift | 6 +++ .../java/com/gutenberg/MainApplication.java | 5 ++ .../GutenbergViewController.swift | 4 ++ 11 files changed, 120 insertions(+) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index c6e20b29db072e..c1dc4bab896b3a 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -60,6 +60,10 @@ interface BlockTypeImpressionsCallback { void onRequestBlockTypeImpressions(ReadableMap impressions); } + interface ConnectionStatusCallback { + void onRequestConnectionStatus(boolean isConnected); + } + // Ref: https://github.com/facebook/react-native/blob/HEAD/Libraries/polyfills/console.js#L376 enum LogLevel { TRACE(0), @@ -183,4 +187,6 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void toggleUndoButton(boolean isDisabled); void toggleRedoButton(boolean isDisabled); + + void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback); } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index d922d863cb3011..0073db769d9cd5 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -23,6 +23,7 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.modules.core.DeviceEventManagerModule; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ConnectionStatusCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaType; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.OtherMediaOptionsReceivedCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FocalPointPickerTooltipShownCallback; @@ -85,6 +86,8 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; + public static final String MAP_KEY_IS_CONNECTED = "isConnected"; + private boolean mIsDarkMode; public RNReactNativeGutenbergBridgeModule(ReactApplicationContext reactContext, @@ -533,4 +536,18 @@ public void generateHapticFeedback() { } } } + + @ReactMethod + public void requestConnectionStatus(final Callback jsCallback) { + ConnectionStatusCallback connectionStatusCallback = requestConnectionStatusCallback(jsCallback); + mGutenbergBridgeJS2Parent.requestConnectionStatus(connectionStatusCallback); + } + + private ConnectionStatusCallback requestConnectionStatusCallback(final Callback jsCallback) { + return new GutenbergBridgeJS2Parent.ConnectionStatusCallback() { + @Override public void onRequestConnectionStatus(boolean isConnected) { + jsCallback.invoke(isConnected); + } + }; + } } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index 7dd4dbf3811feb..fe83bc8a14b540 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -15,6 +15,7 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_IS_CONNECTED; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL; @@ -44,6 +45,8 @@ public interface JSEventEmitter { private static final String EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED = "featuredImageIdNativeUpdated"; + private static final String EVENT_CONNECTION_STATUS_CHANGE = "connectionStatusChange"; + private static final String MAP_KEY_MEDIA_FILE_STATE = "state"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS = "progress"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_SERVER_ID = "mediaServerId"; @@ -222,6 +225,12 @@ public void sendToJSFeaturedImageId(int mediaId) { queueActionToJS(EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED, writableMap); } + public void onConnectionStatusChange(boolean isConnected) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putBoolean(MAP_KEY_IS_CONNECTED, isConnected); + queueActionToJS(EVENT_CONNECTION_STATUS_CHANGE, writableMap); + } + @Override public void onReplaceMediaFilesEditedBlock(String mediaFiles, String blockId) { WritableMap writableMap = new WritableNativeMap(); writableMap.putString(MAP_KEY_REPLACE_BLOCK_HTML, mediaFiles); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 69adb653211da2..c0916d1417a34f 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -112,6 +112,7 @@ public class WPAndroidGlueCode { private OnToggleUndoButtonListener mOnToggleUndoButtonListener; private OnToggleRedoButtonListener mOnToggleRedoButtonListener; + private OnConnectionStatusEventListener mOnConnectionStatusEventListener; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -259,6 +260,10 @@ public interface OnToggleRedoButtonListener { void onToggleRedoButton(boolean isDisabled); } + public interface OnConnectionStatusEventListener { + boolean onRequestConnectionStatus(); + } + public void mediaSelectionCancelled() { mAppendsMultipleSelectedToSiblingBlocks = false; } @@ -594,6 +599,12 @@ public void toggleUndoButton(boolean isDisabled) { public void toggleRedoButton(boolean isDisabled) { mOnToggleRedoButtonListener.onToggleRedoButton(isDisabled); } + + @Override + public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { + boolean isConnected = mOnConnectionStatusEventListener.onRequestConnectionStatus(); + connectionStatusCallback.onRequestConnectionStatus(isConnected); + } }, mIsDarkMode); return Arrays.asList( @@ -688,6 +699,7 @@ public void attachToContainer(ViewGroup viewGroup, OnSendEventToHostListener onSendEventToHostListener, OnToggleUndoButtonListener onToggleUndoButtonListener, OnToggleRedoButtonListener onToggleRedoButtonListener, + OnConnectionStatusEventListener onConnectionStatusEventListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); @@ -713,6 +725,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnSendEventToHostListener = onSendEventToHostListener; mOnToggleUndoButtonListener = onToggleUndoButtonListener; mOnToggleRedoButtonListener = onToggleRedoButtonListener; + mOnConnectionStatusEventListener = onConnectionStatusEventListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -1149,6 +1162,10 @@ public void sendToJSFeaturedImageId(int mediaId) { mDeferredEventEmitter.sendToJSFeaturedImageId(mediaId); } + public void connectionStatusChange(boolean isConnected) { + mDeferredEventEmitter.onConnectionStatusChange(isConnected); + } + public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 89f9f029901f9a..8e9065cc568e56 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -3,6 +3,11 @@ */ import { NativeModules, NativeEventEmitter, Platform } from 'react-native'; +/** + * WordPress dependencies + */ +import { useEffect, useState } from '@wordpress/element'; + const { RNReactNativeGutenbergBridge } = NativeModules; const isIOS = Platform.OS === 'ios'; const isAndroid = Platform.OS === 'android'; @@ -185,6 +190,49 @@ export function subscribeOnRedoPressed( callback ) { return gutenbergBridgeEvents.addListener( 'onRedoPressed', callback ); } +export function useIsConnected() { + const [ isConnected, setIsConnected ] = useState( null ); + + useEffect( () => { + let isCurrent = true; + + RNReactNativeGutenbergBridge.requestConnectionStatus( + ( isBridgeConnected ) => { + if ( ! isCurrent ) { + return; + } + + setIsConnected( isBridgeConnected ); + } + ); + + return () => { + isCurrent = false; + }; + }, [] ); + + useEffect( () => { + const subscription = subscribeConnectionStatus( + ( { isConnected: isBridgeConnected } ) => { + setIsConnected( isBridgeConnected ); + } + ); + + return () => { + subscription.remove(); + }; + }, [] ); + + return { isConnected }; +} + +function subscribeConnectionStatus( callback ) { + return gutenbergBridgeEvents.addListener( + 'connectionStatusChange', + callback + ); +} + /** * Request media picker for the given media source. * diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 4175c1e2343c32..de0d1b513f00dc 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -210,6 +210,11 @@ public class Gutenberg: UIResponder { bridgeModule.sendEventIfNeeded(.onRedoPressed, body: nil) } + public func connectionStatusChange(isConnected: Bool) { + var data: [String: Any] = ["isConnected": isConnected] + bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) + } + private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() settingsUpdates["isFSETheme"] = editorSettings?.isFSETheme ?? false diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 83d087bccab9d1..8890cd4de0f7ec 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -283,6 +283,8 @@ public protocol GutenbergBridgeDelegate: AnyObject { func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) + + func gutenbergDidRequestConnectionStatus() -> Bool } // MARK: - Optional GutenbergBridgeDelegate methods diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index d333f8c1722ad9..3d68e51ebcacb5 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -42,5 +42,6 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(generateHapticFeedback) RCT_EXTERN_METHOD(toggleUndoButton:(BOOL)isDisabled) RCT_EXTERN_METHOD(toggleRedoButton:(BOOL)isDisabled) +RCT_EXTERN_METHOD(requestConnectionStatus:(RCTResponseSenderBlock)callback) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 8cf4f685bd22c4..ec763b2b8aaa2c 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -421,6 +421,11 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { func toggleRedoButton(_ isDisabled: Bool) { self.delegate?.gutenbergDidRequestToggleRedoButton(isDisabled) } + + @objc + func requestConnectionStatus(_ callback: @escaping RCTResponseSenderBlock) { + callback([self.delegate?.gutenbergDidRequestConnectionStatus() ?? true]) + } } // MARK: - RCTBridgeModule delegate @@ -450,6 +455,7 @@ extension RNReactNativeGutenbergBridge { case showEditorHelp case onUndoPressed case onRedoPressed + case connectionStatusChange } public override func supportedEvents() -> [String]! { diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index d718b34f25db3b..4477f1cc1d9f35 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -308,6 +308,11 @@ public void toggleRedoButton(boolean isDisabled) { mainActivity.updateRedoItem(isDisabled); } } + + @Override + public void requestConnectionStatus(ConnectionStatusCallback connectionStatusCallback) { + connectionStatusCallback.onRequestConnectionStatus(true); + } }, isDarkMode()); return new DefaultReactNativeHost(this) { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index b269d1feb8ddfe..ef95c7e65862f6 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -345,6 +345,10 @@ extension GutenbergViewController: GutenbergBridgeDelegate { } } } + + func gutenbergDidRequestConnectionStatus() -> Bool { + return true + } } extension GutenbergViewController: GutenbergWebDelegate { From ac304fe7290acf0d4b0ee849c3a53573660ce6cc Mon Sep 17 00:00:00 2001 From: Andy Peatling <apeatling@users.noreply.github.com> Date: Mon, 11 Dec 2023 19:00:44 -0800 Subject: [PATCH 129/325] Components: Update CHANGELOG.md (#56960) * Components: Update CHANGELOG.md Add references to the search and spinner docs changes. * Update packages/components/CHANGELOG.md --------- Co-authored-by: Andrew Serong <14988353+andrewserong@users.noreply.github.com> --- packages/components/CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 33e4e58d29e7c0..701e8661054fab 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -30,6 +30,11 @@ - `Tabs`: improve focus handling in controlled mode ([#56658](https://github.com/WordPress/gutenberg/pull/56658)). +### Documentation + +- `Search`: Added links to storybook for more information on usage. ([#56815](https://github.com/WordPress/gutenberg/pull/56815)). +- `Spinner`: Added links to storybook for more information on usage. ([#56953](https://github.com/WordPress/gutenberg/pull/56953)). + ## 25.13.0 (2023-11-29) ### Enhancements From 839e55370bfd388c9d3ea2acff678bf630a3af32 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Tue, 12 Dec 2023 16:44:54 +1100 Subject: [PATCH 130/325] PaletteEdit: temporary custom gradient not saving (#56896) * Gradient elements contain both color and gradient properties, therefore they'll always return true for this test if the color property is default (#000) * CHANGELOG.md * isTemporaryElement should return false by default so that it will catch changes to the slug Added tests --- packages/components/CHANGELOG.md | 2 +- .../components/src/palette-edit/index.tsx | 30 ++++++-- .../src/palette-edit/test/index.tsx | 76 ++++++++++++++++++- 3 files changed, 98 insertions(+), 10 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 701e8661054fab..5e73e4319a1ba0 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,7 +13,7 @@ - `FontSizePicker`: Add opt-in prop for 40px default size ([#56804](https://github.com/WordPress/gutenberg/pull/56804)). ### Bug Fix - +- `PaletteEdit`: temporary custom gradient not saving ([#56896](https://github.com/WordPress/gutenberg/pull/56896)). - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). - `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index b3b8b626ce3b4a..40621a407f2173 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -60,7 +60,7 @@ import type { PaletteElement, } from './types'; -const DEFAULT_COLOR = '#000'; +export const DEFAULT_COLOR = '#000'; function NameInput( { value, onChange, label }: NameInputProps ) { return ( @@ -261,16 +261,30 @@ function Option< T extends Color | Gradient >( { ); } -function isTemporaryElement( +/** + * Checks if a color or gradient is a temporary element by testing against default values. + */ +export function isTemporaryElement( slugPrefix: string, { slug, color, gradient }: Color | Gradient -) { +): Boolean { const regex = new RegExp( `^${ slugPrefix }color-([\\d]+)$` ); - return ( - regex.test( slug ) && - ( ( !! color && color === DEFAULT_COLOR ) || - ( !! gradient && gradient === DEFAULT_GRADIENT ) ) - ); + + // If the slug matches the temporary name regex, + // check if the color or gradient matches the default value. + if ( regex.test( slug ) ) { + // The order is important as gradient elements + // contain a color property. + if ( !! gradient ) { + return gradient === DEFAULT_GRADIENT; + } + + if ( !! color ) { + return color === DEFAULT_COLOR; + } + } + + return false; } function PaletteEditListView< T extends Color | Gradient >( { diff --git a/packages/components/src/palette-edit/test/index.tsx b/packages/components/src/palette-edit/test/index.tsx index 1bf2802709de7f..1a0b2fdaaab3fb 100644 --- a/packages/components/src/palette-edit/test/index.tsx +++ b/packages/components/src/palette-edit/test/index.tsx @@ -6,8 +6,13 @@ import { render, fireEvent, screen } from '@testing-library/react'; /** * Internal dependencies */ -import PaletteEdit, { getNameForPosition } from '..'; +import PaletteEdit, { + getNameForPosition, + isTemporaryElement, + DEFAULT_COLOR, +} from '..'; import type { PaletteElement } from '../types'; +import { DEFAULT_GRADIENT } from '../../custom-gradient-picker/constants'; describe( 'getNameForPosition', () => { test( 'should return 1 by default', () => { @@ -80,6 +85,75 @@ describe( 'getNameForPosition', () => { } ); } ); +describe( 'isTemporaryElement', () => { + [ + { + message: 'identifies temporary color', + slug: 'test-', + obj: { + name: '', + slug: 'test-color-1', + color: DEFAULT_COLOR, + }, + expected: true, + }, + { + message: 'identifies temporary gradient', + slug: 'test-', + obj: { + name: '', + slug: 'test-color-1', + gradient: DEFAULT_GRADIENT, + }, + expected: true, + }, + { + message: 'identifies custom color slug', + slug: 'test-', + obj: { + name: '', + slug: 'test-color-happy', + color: DEFAULT_COLOR, + }, + expected: false, + }, + { + message: 'identifies custom color value', + slug: 'test-', + obj: { + name: '', + slug: 'test-color-1', + color: '#ccc', + }, + expected: false, + }, + { + message: 'identifies custom gradient slug', + slug: 'test-', + obj: { + name: '', + slug: 'test-gradient-joy', + color: DEFAULT_GRADIENT, + }, + expected: false, + }, + { + message: 'identifies custom gradient value', + slug: 'test-', + obj: { + name: '', + slug: 'test-color-3', + color: 'linear-gradient(90deg, rgba(22, 22, 22, 1) 0%, rgb(155, 81, 224) 100%)', + }, + expected: false, + }, + ].forEach( ( { message, slug, obj, expected } ) => { + it( `should ${ message }`, () => { + expect( isTemporaryElement( slug, obj ) ).toBe( expected ); + } ); + } ); +} ); + describe( 'PaletteEdit', () => { const defaultProps = { colors: [ { color: '#ffffff', name: 'Base', slug: 'base' } ], From a43ffa42d9d6481fb5372eac9ad774f443bbeb21 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 12 Dec 2023 07:39:15 +0100 Subject: [PATCH 131/325] Block editor: hooks: avoid getEditWrapperProps (#56912) --- .../src/components/block-list/block.js | 12 +- packages/block-editor/src/hooks/border.js | 54 ++---- packages/block-editor/src/hooks/color.js | 78 ++++----- .../block-editor/src/hooks/font-family.js | 39 ++--- packages/block-editor/src/hooks/font-size.js | 163 ++++++------------ packages/block-editor/src/hooks/index.js | 4 +- packages/block-editor/src/hooks/style.js | 59 ++----- packages/block-editor/src/hooks/utils.js | 14 +- 8 files changed, 136 insertions(+), 287 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index b1a97237ab630d..b38dcf3ef1f2ec 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -54,10 +54,18 @@ function mergeWrapperProps( propsA, propsB ) { ...propsB, }; - if ( propsA?.className && propsB?.className ) { + // May be set to undefined, so check if the property is set! + if ( + propsA?.hasOwnProperty( 'className' ) && + propsB?.hasOwnProperty( 'className' ) + ) { newProps.className = classnames( propsA.className, propsB.className ); } - if ( propsA?.style && propsB?.style ) { + + if ( + propsA?.hasOwnProperty( 'style' ) && + propsB?.hasOwnProperty( 'style' ) + ) { newProps.style = { ...propsA.style, ...propsB.style }; } diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index 6ac4dd2360fb08..c6947eeaa18e38 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -258,16 +258,16 @@ function addAttributes( settings ) { /** * Override props assigned to save component to inject border color. * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type definition. - * @param {Object} attributes Block's attributes. + * @param {Object} props Additional props applied to save element. + * @param {Object|string} blockNameOrType Block type definition. + * @param {Object} attributes Block's attributes. * * @return {Object} Filtered props to apply to save element. */ -function addSaveProps( props, blockType, attributes ) { +function addSaveProps( props, blockNameOrType, attributes ) { if ( - ! hasBorderSupport( blockType, 'color' ) || - shouldSkipSerialization( blockType, BORDER_SUPPORT_KEY, 'color' ) + ! hasBorderSupport( blockNameOrType, 'color' ) || + shouldSkipSerialization( blockNameOrType, BORDER_SUPPORT_KEY, 'color' ) ) { return props; } @@ -300,36 +300,6 @@ export function getBorderClasses( attributes ) { } ); } -/** - * Filters the registered block settings to apply border color styles and - * classnames to the block edit wrapper. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addEditProps( settings ) { - if ( - ! hasBorderSupport( settings, 'color' ) || - shouldSkipSerialization( settings, BORDER_SUPPORT_KEY, 'color' ) - ) { - return settings; - } - - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - - return addSaveProps( props, settings, attributes ); - }; - - return settings; -} - function useBlockProps( { name, borderColor, style } ) { const { colors } = useMultipleOriginColorsAndGradients(); @@ -369,7 +339,11 @@ function useBlockProps( { name, borderColor, style } ) { borderLeftColor: borderLeftColor || borderColorValue, }; - return { style: cleanEmptyObject( extraStyles ) || {} }; + return addSaveProps( + { style: cleanEmptyObject( extraStyles ) || {} }, + name, + { borderColor, style } + ); } export default { @@ -391,9 +365,3 @@ addFilter( 'core/border/addSaveProps', addSaveProps ); - -addFilter( - 'blocks.registerBlockType', - 'core/border/addEditProps', - addEditProps -); diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index f259ff9c9c0865..db6c3dc8fd86ce 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -36,8 +36,8 @@ import { store as blockEditorStore } from '../store'; export const COLOR_SUPPORT_KEY = 'color'; -const hasColorSupport = ( blockType ) => { - const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY ); +const hasColorSupport = ( blockNameOrType ) => { + const colorSupport = getBlockSupport( blockNameOrType, COLOR_SUPPORT_KEY ); return ( colorSupport && ( colorSupport.link === true || @@ -61,8 +61,8 @@ const hasLinkColorSupport = ( blockType ) => { ); }; -const hasGradientSupport = ( blockType ) => { - const colorSupport = getBlockSupport( blockType, COLOR_SUPPORT_KEY ); +const hasGradientSupport = ( blockNameOrType ) => { + const colorSupport = getBlockSupport( blockNameOrType, COLOR_SUPPORT_KEY ); return ( colorSupport !== null && @@ -126,27 +126,31 @@ function addAttributes( settings ) { /** * Override props assigned to save component to inject colors classnames. * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. + * @param {Object} props Additional props applied to save element. + * @param {Object|string} blockNameOrType Block type. + * @param {Object} attributes Block attributes. * * @return {Object} Filtered props applied to save element. */ -export function addSaveProps( props, blockType, attributes ) { +export function addSaveProps( props, blockNameOrType, attributes ) { if ( - ! hasColorSupport( blockType ) || - shouldSkipSerialization( blockType, COLOR_SUPPORT_KEY ) + ! hasColorSupport( blockNameOrType ) || + shouldSkipSerialization( blockNameOrType, COLOR_SUPPORT_KEY ) ) { return props; } - const hasGradient = hasGradientSupport( blockType ); + const hasGradient = hasGradientSupport( blockNameOrType ); // I'd have preferred to avoid the "style" attribute usage here const { backgroundColor, textColor, gradient, style } = attributes; const shouldSerialize = ( feature ) => - ! shouldSkipSerialization( blockType, COLOR_SUPPORT_KEY, feature ); + ! shouldSkipSerialization( + blockNameOrType, + COLOR_SUPPORT_KEY, + feature + ); // Primary color classes must come before the `has-text-color`, // `has-background` and `has-link-color` classes to maintain backwards @@ -192,33 +196,6 @@ export function addSaveProps( props, blockType, attributes ) { return props; } -/** - * Filters registered block settings to extend the block edit wrapper - * to apply the desired styles and classnames properly. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -export function addEditProps( settings ) { - if ( - ! hasColorSupport( settings ) || - shouldSkipSerialization( settings, COLOR_SUPPORT_KEY ) - ) { - return settings; - } - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - return addSaveProps( props, settings, attributes ); - }; - - return settings; -} - function styleToAttributes( style ) { const textColorValue = style?.color?.text; const textColorSlug = textColorValue?.startsWith( 'var:preset|color|' ) @@ -364,7 +341,13 @@ function ColorEditPure( { clientId, name, setAttributes, settings } ) { // and not the whole attributes object. export const ColorEdit = pure( ColorEditPure ); -function useBlockProps( { name, backgroundColor, textColor } ) { +function useBlockProps( { + name, + backgroundColor, + textColor, + gradient, + style, +} ) { const [ userPalette, themePalette, defaultPalette ] = useSettings( 'color.palette.custom', 'color.palette.theme', @@ -406,12 +389,17 @@ function useBlockProps( { name, backgroundColor, textColor } ) { )?.color; } - return { style: extraStyles }; + return addSaveProps( { style: extraStyles }, name, { + textColor, + backgroundColor, + gradient, + style, + } ); } export default { useBlockProps, - attributeKeys: [ 'backgroundColor', 'textColor' ], + attributeKeys: [ 'backgroundColor', 'textColor', 'gradient', 'style' ], hasSupport: hasColorSupport, }; @@ -455,12 +443,6 @@ addFilter( addSaveProps ); -addFilter( - 'blocks.registerBlockType', - 'core/color/addEditProps', - addEditProps -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/color/addTransforms', diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index 0988b285564d3e..36266d59adcf2c 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -74,31 +74,18 @@ function addSaveProps( props, blockType, attributes ) { return props; } -/** - * Filters registered block settings to expand the block edit wrapper - * by applying the desired styles and classnames. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addEditProps( settings ) { - if ( ! hasBlockSupport( settings, FONT_FAMILY_SUPPORT_KEY ) ) { - return settings; - } - - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - return addSaveProps( props, settings, attributes ); - }; - - return settings; +function useBlockProps( { name, fontFamily } ) { + return addSaveProps( {}, name, { fontFamily } ); } +export default { + useBlockProps, + attributeKeys: [ 'fontFamily' ], + hasSupport( name ) { + return hasBlockSupport( name, FONT_FAMILY_SUPPORT_KEY ); + }, +}; + /** * Resets the font family block support attribute. This can be used when * disabling the font family support controls for a block via a progressive @@ -122,9 +109,3 @@ addFilter( 'core/fontFamily/addSaveProps', addSaveProps ); - -addFilter( - 'blocks.registerBlockType', - 'core/fontFamily/addEditProps', - addEditProps -); diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index a7ef79f0d4f2bf..b30fcc82d99463 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -58,19 +58,23 @@ function addAttributes( settings ) { /** * Override props assigned to save component to inject font size. * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. + * @param {Object} props Additional props applied to save element. + * @param {Object} blockNameOrType Block type. + * @param {Object} attributes Block attributes. * * @return {Object} Filtered props applied to save element. */ -function addSaveProps( props, blockType, attributes ) { - if ( ! hasBlockSupport( blockType, FONT_SIZE_SUPPORT_KEY ) ) { +function addSaveProps( props, blockNameOrType, attributes ) { + if ( ! hasBlockSupport( blockNameOrType, FONT_SIZE_SUPPORT_KEY ) ) { return props; } if ( - shouldSkipSerialization( blockType, TYPOGRAPHY_SUPPORT_KEY, 'fontSize' ) + shouldSkipSerialization( + blockNameOrType, + TYPOGRAPHY_SUPPORT_KEY, + 'fontSize' + ) ) { return props; } @@ -84,31 +88,6 @@ function addSaveProps( props, blockType, attributes ) { return props; } -/** - * Filters registered block settings to expand the block edit wrapper - * by applying the desired styles and classnames. - * - * @param {Object} settings Original block settings. - * - * @return {Object} Filtered block settings. - */ -function addEditProps( settings ) { - if ( ! hasBlockSupport( settings, FONT_SIZE_SUPPORT_KEY ) ) { - return settings; - } - - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - return addSaveProps( props, settings, attributes ); - }; - - return settings; -} - /** * Inspector control panel containing the font size related configuration * @@ -184,19 +163,50 @@ function useBlockProps( { name, fontSize, style } ) { if ( ! hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ) || shouldSkipSerialization( name, TYPOGRAPHY_SUPPORT_KEY, 'fontSize' ) || - ! fontSize || - style?.typography?.fontSize + ! fontSize ) { return; } - const fontSizeValue = getFontSize( - fontSizes, - fontSize, - style?.typography?.fontSize - ).size; + let props = {}; + + if ( ! style?.typography?.fontSize ) { + props = { + style: { + fontSize: getFontSize( + fontSizes, + fontSize, + style?.typography?.fontSize + ).size, + }, + }; + } - return { style: { fontSize: fontSizeValue } }; + // TODO: This sucks! We should be using useSetting( 'typography.fluid' ) + // or even useSelect( blockEditorStore ). We can't do either here + // because getEditWrapperProps is a plain JavaScript function called by + // BlockListBlock and not a React component rendered within + // BlockListContext.Provider. If we set fontSize using editor. + // BlockListBlock instead of using getEditWrapperProps then the value is + // clobbered when the core/style/addEditProps filter runs. + + // TODO: We can do the thing above now. + const fluidTypographySettings = getFluidTypographyOptionsFromSettings( + select( blockEditorStore ).getSettings().__experimentalFeatures + ); + + if ( fontSize ) { + props = { + style: { + fontSize: getTypographyFontSizeValue( + { size: fontSize }, + fluidTypographySettings + ), + }, + }; + } + + return addSaveProps( props, name, { fontSize } ); } export default { @@ -229,70 +239,6 @@ function addTransforms( result, source, index, results ) { ); } -/** - * Allow custom font sizes to appear fluid when fluid typography is enabled at - * the theme level. - * - * Adds a custom getEditWrapperProps() callback to all block types that support - * font sizes. Then, if fluid typography is enabled, this callback will swap any - * custom font size in style.fontSize with a fluid font size (i.e. one that uses - * clamp()). - * - * It's important that this hook runs after 'core/style/addEditProps' sets - * style.fontSize as otherwise fontSize will be overwritten. - * - * @param {Object} blockType Block settings object. - */ -function addEditPropsForFluidCustomFontSizes( blockType ) { - if ( - ! hasBlockSupport( blockType, FONT_SIZE_SUPPORT_KEY ) || - shouldSkipSerialization( blockType, TYPOGRAPHY_SUPPORT_KEY, 'fontSize' ) - ) { - return blockType; - } - - const existingGetEditWrapperProps = blockType.getEditWrapperProps; - - blockType.getEditWrapperProps = ( attributes ) => { - const wrapperProps = existingGetEditWrapperProps - ? existingGetEditWrapperProps( attributes ) - : {}; - - const fontSize = wrapperProps?.style?.fontSize; - - // TODO: This sucks! We should be using useSetting( 'typography.fluid' ) - // or even useSelect( blockEditorStore ). We can't do either here - // because getEditWrapperProps is a plain JavaScript function called by - // BlockListBlock and not a React component rendered within - // BlockListContext.Provider. If we set fontSize using editor. - // BlockListBlock instead of using getEditWrapperProps then the value is - // clobbered when the core/style/addEditProps filter runs. - const fluidTypographySettings = getFluidTypographyOptionsFromSettings( - select( blockEditorStore ).getSettings().__experimentalFeatures - ); - const newFontSize = fontSize - ? getTypographyFontSizeValue( - { size: fontSize }, - fluidTypographySettings - ) - : null; - - if ( newFontSize === null ) { - return wrapperProps; - } - - return { - ...wrapperProps, - style: { - ...wrapperProps?.style, - fontSize: newFontSize, - }, - }; - }; - - return blockType; -} - addFilter( 'blocks.registerBlockType', 'core/font/addAttribute', @@ -305,19 +251,8 @@ addFilter( addSaveProps ); -addFilter( 'blocks.registerBlockType', 'core/font/addEditProps', addEditProps ); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/font-size/addTransforms', addTransforms ); - -addFilter( - 'blocks.registerBlockType', - 'core/font-size/addEditPropsForFluidCustomFontSizes', - addEditPropsForFluidCustomFontSizes, - // Run after 'core/style/addEditProps' so that the style object has already - // been translated into inline CSS. - 11 -); diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 506f2a50a83a73..ec0dba5efb2b69 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -13,7 +13,7 @@ import style from './style'; import './settings'; import color from './color'; import duotone from './duotone'; -import './font-family'; +import fontFamily from './font-family'; import fontSize from './font-size'; import border from './border'; import position from './position'; @@ -41,8 +41,10 @@ createBlockEditFilter( ); createBlockListBlockFilter( [ align, + style, color, duotone, + fontFamily, fontSize, border, position, diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index 935e8260fa89f6..b6098969bebb5e 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -269,20 +269,20 @@ export function omitStyle( style, paths, preserveReference = false ) { /** * Override props assigned to save component to inject the CSS variables definition. * - * @param {Object} props Additional props applied to save element. - * @param {Object} blockType Block type. - * @param {Object} attributes Block attributes. - * @param {?Record<string, string[]>} skipPaths An object of keys and paths to skip serialization. + * @param {Object} props Additional props applied to save element. + * @param {Object|string} blockNameOrType Block type. + * @param {Object} attributes Block attributes. + * @param {?Record<string, string[]>} skipPaths An object of keys and paths to skip serialization. * * @return {Object} Filtered props applied to save element. */ export function addSaveProps( props, - blockType, + blockNameOrType, attributes, skipPaths = skipSerializationPathsSave ) { - if ( ! hasStyleSupport( blockType ) ) { + if ( ! hasStyleSupport( blockNameOrType ) ) { return props; } @@ -290,7 +290,7 @@ export function addSaveProps( Object.entries( skipPaths ).forEach( ( [ indicator, path ] ) => { const skipSerialization = skipSerializationPathsSaveChecks[ indicator ] || - getBlockSupport( blockType, indicator ); + getBlockSupport( blockNameOrType, indicator ); if ( skipSerialization === true ) { style = omitStyle( style, path ); @@ -312,37 +312,6 @@ export function addSaveProps( return props; } -/** - * Filters registered block settings to extend the block edit wrapper - * to apply the desired styles and classnames properly. - * - * @param {Object} settings Original block settings. - * - * @return {Object}.Filtered block settings. - */ -export function addEditProps( settings ) { - if ( ! hasStyleSupport( settings ) ) { - return settings; - } - - const existingGetEditWrapperProps = settings.getEditWrapperProps; - settings.getEditWrapperProps = ( attributes ) => { - let props = {}; - if ( existingGetEditWrapperProps ) { - props = existingGetEditWrapperProps( attributes ); - } - - return addSaveProps( - props, - settings, - attributes, - skipSerializationPathsEdit - ); - }; - - return settings; -} - function BlockStyleControls( { clientId, name, @@ -472,7 +441,13 @@ function useBlockProps( { name, style } ) { }, [ baseElementSelector, blockElementStyles, name ] ); useStyleOverride( { css: styles } ); - return { className: blockElementsContainerIdentifier }; + + return addSaveProps( + { className: blockElementsContainerIdentifier }, + name, + { style }, + skipSerializationPathsEdit + ); } addFilter( @@ -486,9 +461,3 @@ addFilter( 'core/style/addSaveProps', addSaveProps ); - -addFilter( - 'blocks.registerBlockType', - 'core/style/addEditProps', - addEditProps -); diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index c6076b822545a9..49617013dc1153 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -112,14 +112,18 @@ export function transformStyles( * Check whether serialization of specific block support feature or set should * be skipped. * - * @param {string|Object} blockType Block name or block type object. - * @param {string} featureSet Name of block support feature set. - * @param {string} feature Name of the individual feature to check. + * @param {string|Object} blockNameOrType Block name or block type object. + * @param {string} featureSet Name of block support feature set. + * @param {string} feature Name of the individual feature to check. * * @return {boolean} Whether serialization should occur. */ -export function shouldSkipSerialization( blockType, featureSet, feature ) { - const support = getBlockSupport( blockType, featureSet ); +export function shouldSkipSerialization( + blockNameOrType, + featureSet, + feature +) { + const support = getBlockSupport( blockNameOrType, featureSet ); const skipSerialization = support?.__experimentalSkipSerialization; if ( Array.isArray( skipSerialization ) ) { From 45d7babf58f54d8d79efcc135eeec72f95a75eff Mon Sep 17 00:00:00 2001 From: Derek Blank <derekpblank@gmail.com> Date: Tue, 12 Dec 2023 16:07:17 +0800 Subject: [PATCH 132/325] [RNMobile] Ensure that `blockType` is defined when accessing `wrapperProps` (#56846) * Check if native blockType exists before assigning wrapperProps * Update CHANGELOG --- .../src/components/block-list/block.native.js | 11 +++++++---- packages/react-native-editor/CHANGELOG.md | 1 + 2 files changed, 8 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 2daaf846443b5a..70a66c445f58f9 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -253,10 +253,13 @@ function BlockListBlock( { ); // Block level styles. - const wrapperProps = getWrapperProps( - attributes, - blockType.getEditWrapperProps - ); + let wrapperProps = {}; + if ( blockType?.getEditWrapperProps ) { + wrapperProps = getWrapperProps( + attributes, + blockType.getEditWrapperProps + ); + } // Inherited styles merged with block level styles. const mergedStyle = useMemo( () => { diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 83b4800fe2a9de..33fa3d26e6c0ad 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] [internal] Move InserterButton from components package to block-editor package [#56494] - [*] [internal] Move ImageLinkDestinationsScreen from components package to block-editor package [#56775] +- [*] Fix crash when blockType wrapperProps are not defined [#56846] - [*] Guard against an Image block styles crash due to null block values [#56903] - [**] Fix crash when sharing unsupported media types on Android [#56791] From dc95863e510c213d8896dd71843d2c3b62fd260d Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:03:01 +0100 Subject: [PATCH 133/325] Paragraph: store subscription for selected block only (#56967) --- packages/block-library/src/paragraph/edit.js | 85 +++++++++++--------- 1 file changed, 49 insertions(+), 36 deletions(-) diff --git a/packages/block-library/src/paragraph/edit.js b/packages/block-library/src/paragraph/edit.js index 3a0f4f0688ac9b..ac766f69dd846a 100644 --- a/packages/block-library/src/paragraph/edit.js +++ b/packages/block-library/src/paragraph/edit.js @@ -49,6 +49,48 @@ function hasDropCapDisabled( align ) { return align === ( isRTL() ? 'left' : 'right' ) || align === 'center'; } +function DropCapControl( { clientId, attributes, setAttributes } ) { + // Please do no add a useSelect call to the paragraph block unconditionaly. + // Every useSelect added to a (frequestly used) block will degrade the load + // and type bit. By moving it within InspectorControls, the subscription is + // now only added for the selected block(s). + const [ isDropCapFeatureEnabled ] = useSettings( 'typography.dropCap' ); + + if ( ! isDropCapFeatureEnabled ) { + return null; + } + + const { align, dropCap } = attributes; + + let helpText; + if ( hasDropCapDisabled( align ) ) { + helpText = __( 'Not available for aligned text.' ); + } else if ( dropCap ) { + helpText = __( 'Showing large initial letter.' ); + } else { + helpText = __( 'Toggle to show a large initial letter.' ); + } + + return ( + <ToolsPanelItem + hasValue={ () => !! dropCap } + label={ __( 'Drop cap' ) } + onDeselect={ () => setAttributes( { dropCap: undefined } ) } + resetAllFilter={ () => ( { dropCap: undefined } ) } + panelId={ clientId } + > + <ToggleControl + __nextHasNoMarginBottom + label={ __( 'Drop cap' ) } + checked={ !! dropCap } + onChange={ () => setAttributes( { dropCap: ! dropCap } ) } + help={ helpText } + disabled={ hasDropCapDisabled( align ) ? true : false } + /> + </ToolsPanelItem> + ); +} + function ParagraphBlock( { attributes, mergeBlocks, @@ -58,7 +100,6 @@ function ParagraphBlock( { clientId, } ) { const { align, content, direction, dropCap, placeholder } = attributes; - const [ isDropCapFeatureEnabled ] = useSettings( 'typography.dropCap' ); const blockProps = useBlockProps( { ref: useOnEnter( { clientId, content } ), className: classnames( { @@ -68,15 +109,6 @@ function ParagraphBlock( { style: { direction }, } ); - let helpText; - if ( hasDropCapDisabled( align ) ) { - helpText = __( 'Not available for aligned text.' ); - } else if ( dropCap ) { - helpText = __( 'Showing large initial letter.' ); - } else { - helpText = __( 'Toggle to show a large initial letter.' ); - } - return ( <> <BlockControls group="block"> @@ -98,32 +130,13 @@ function ParagraphBlock( { } /> </BlockControls> - { isDropCapFeatureEnabled && ( - <InspectorControls group="typography"> - <ToolsPanelItem - hasValue={ () => !! dropCap } - label={ __( 'Drop cap' ) } - onDeselect={ () => - setAttributes( { dropCap: undefined } ) - } - resetAllFilter={ () => ( { dropCap: undefined } ) } - panelId={ clientId } - > - <ToggleControl - __nextHasNoMarginBottom - label={ __( 'Drop cap' ) } - checked={ !! dropCap } - onChange={ () => - setAttributes( { dropCap: ! dropCap } ) - } - help={ helpText } - disabled={ - hasDropCapDisabled( align ) ? true : false - } - /> - </ToolsPanelItem> - </InspectorControls> - ) } + <InspectorControls group="typography"> + <DropCapControl + clientId={ clientId } + attributes={ attributes } + setAttributes={ setAttributes } + /> + </InspectorControls> <RichText identifier="content" tagName="p" From 0456925145f6ca180639082ae83f4190b9e303ff Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 12 Dec 2023 18:12:18 +0900 Subject: [PATCH 134/325] Input Field Block: Use `useblockProps` hook in save function (#56507) * Input Field Block: Use blockProps hook in save function * Update fixture files * Update packages/block-library/src/form-input/deprecated.js Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --------- Co-authored-by: Aaron Robertshaw <60436221+aaronrobertshaw@users.noreply.github.com> --- .../src/form-input/deprecated.js | 142 ++++++++++++++++++ packages/block-library/src/form-input/edit.js | 2 +- .../block-library/src/form-input/index.js | 2 + packages/block-library/src/form-input/save.js | 45 +++--- .../fixtures/blocks/core__form-input.html | 2 +- .../blocks/core__form-input.parsed.json | 4 +- .../blocks/core__form-input.serialized.html | 2 +- .../core__form-input__deprecated-v1.html | 6 + .../core__form-input__deprecated-v1.json | 15 ++ ...ore__form-input__deprecated-v1.parsed.json | 11 ++ ..._form-input__deprecated-v1.serialized.html | 3 + .../fixtures/blocks/core__form.html | 16 +- .../fixtures/blocks/core__form.parsed.json | 32 ++-- .../blocks/core__form.serialized.html | 8 +- 14 files changed, 233 insertions(+), 57 deletions(-) create mode 100644 packages/block-library/src/form-input/deprecated.js create mode 100644 test/integration/fixtures/blocks/core__form-input__deprecated-v1.html create mode 100644 test/integration/fixtures/blocks/core__form-input__deprecated-v1.json create mode 100644 test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json create mode 100644 test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html diff --git a/packages/block-library/src/form-input/deprecated.js b/packages/block-library/src/form-input/deprecated.js new file mode 100644 index 00000000000000..bb1fdf6e40204d --- /dev/null +++ b/packages/block-library/src/form-input/deprecated.js @@ -0,0 +1,142 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; +import removeAccents from 'remove-accents'; + +/** + * WordPress dependencies + */ +import { + RichText, + __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, + __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, +} from '@wordpress/block-editor'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; + +const getNameFromLabelV1 = ( content ) => { + return ( + removeAccents( stripHTML( content ) ) + // Convert anything that's not a letter or number to a hyphen. + .replace( /[^\p{L}\p{N}]+/gu, '-' ) + // Convert to lowercase + .toLowerCase() + // Remove any remaining leading or trailing hyphens. + .replace( /(^-+)|(-+$)/g, '' ) + ); +}; + +// Version without wrapper div in saved markup +// See: https://github.com/WordPress/gutenberg/pull/56507 +const v1 = { + attributes: { + type: { + type: 'string', + default: 'text', + }, + name: { + type: 'string', + }, + label: { + type: 'string', + default: 'Label', + selector: '.wp-block-form-input__label-content', + source: 'html', + __experimentalRole: 'content', + }, + inlineLabel: { + type: 'boolean', + default: false, + }, + required: { + type: 'boolean', + default: false, + selector: '.wp-block-form-input__input', + source: 'attribute', + attribute: 'required', + }, + placeholder: { + type: 'string', + selector: '.wp-block-form-input__input', + source: 'attribute', + attribute: 'placeholder', + __experimentalRole: 'content', + }, + value: { + type: 'string', + default: '', + selector: 'input', + source: 'attribute', + attribute: 'value', + }, + visibilityPermissions: { + type: 'string', + default: 'all', + }, + }, + supports: { + className: false, + anchor: true, + reusable: false, + spacing: { + margin: [ 'top', 'bottom' ], + }, + __experimentalBorder: { + radius: true, + __experimentalSkipSerialization: true, + __experimentalDefaultControls: { + radius: true, + }, + }, + }, + save( { attributes } ) { + const { type, name, label, inlineLabel, required, placeholder, value } = + attributes; + + const borderProps = getBorderClassesAndStyles( attributes ); + const colorProps = getColorClassesAndStyles( attributes ); + + const inputStyle = { + ...borderProps.style, + ...colorProps.style, + }; + + const inputClasses = classNames( + 'wp-block-form-input__input', + colorProps.className, + borderProps.className + ); + const TagName = type === 'textarea' ? 'textarea' : 'input'; + + if ( 'hidden' === type ) { + return <input type={ type } name={ name } value={ value } />; + } + + /* eslint-disable jsx-a11y/label-has-associated-control */ + return ( + <label + className={ classNames( 'wp-block-form-input__label', { + 'is-label-inline': inlineLabel, + } ) } + > + <span className="wp-block-form-input__label-content"> + <RichText.Content value={ label } /> + </span> + <TagName + className={ inputClasses } + type={ 'textarea' === type ? undefined : type } + name={ name || getNameFromLabelV1( label ) } + required={ required } + aria-required={ required } + placeholder={ placeholder || undefined } + style={ inputStyle } + /> + </label> + ); + /* eslint-enable jsx-a11y/label-has-associated-control */ + }, +}; + +const deprecated = [ v1 ]; + +export default deprecated; diff --git a/packages/block-library/src/form-input/edit.js b/packages/block-library/src/form-input/edit.js index 0742c22c22f429..0b34c70fbad2d7 100644 --- a/packages/block-library/src/form-input/edit.js +++ b/packages/block-library/src/form-input/edit.js @@ -59,7 +59,7 @@ function InputFieldBlock( { attributes, setAttributes, className } ) { </PanelBody> </InspectorControls> ) } - <InspectorControls __experimentalGroup="advanced"> + <InspectorControls group="advanced"> <TextControl autoComplete="off" label={ __( 'Name' ) } diff --git a/packages/block-library/src/form-input/index.js b/packages/block-library/src/form-input/index.js index b700e0ade6ca7f..8e0548a6b24dbf 100644 --- a/packages/block-library/src/form-input/index.js +++ b/packages/block-library/src/form-input/index.js @@ -2,6 +2,7 @@ * Internal dependencies */ import initBlock from '../utils/init-block'; +import deprecated from './deprecated'; import edit from './edit'; import metadata from './block.json'; import save from './save'; @@ -12,6 +13,7 @@ const { name } = metadata; export { metadata, name }; export const settings = { + deprecated, edit, save, variations, diff --git a/packages/block-library/src/form-input/save.js b/packages/block-library/src/form-input/save.js index 1404e40634e82e..d8f5852c2ab90b 100644 --- a/packages/block-library/src/form-input/save.js +++ b/packages/block-library/src/form-input/save.js @@ -9,6 +9,7 @@ import removeAccents from 'remove-accents'; */ import { RichText, + useBlockProps, __experimentalGetBorderClassesAndStyles as getBorderClassesAndStyles, __experimentalGetColorClassesAndStyles as getColorClassesAndStyles, } from '@wordpress/block-editor'; @@ -52,30 +53,34 @@ export default function save( { attributes } ) { ); const TagName = type === 'textarea' ? 'textarea' : 'input'; + const blockProps = useBlockProps.save(); + if ( 'hidden' === type ) { return <input type={ type } name={ name } value={ value } />; } - /* eslint-disable jsx-a11y/label-has-associated-control */ return ( - <label - className={ classNames( 'wp-block-form-input__label', { - 'is-label-inline': inlineLabel, - } ) } - > - <span className="wp-block-form-input__label-content"> - <RichText.Content value={ label } /> - </span> - <TagName - className={ inputClasses } - type={ 'textarea' === type ? undefined : type } - name={ name || getNameFromLabel( label ) } - required={ required } - aria-required={ required } - placeholder={ placeholder || undefined } - style={ inputStyle } - /> - </label> + <div { ...blockProps }> + { /* eslint-disable jsx-a11y/label-has-associated-control */ } + <label + className={ classNames( 'wp-block-form-input__label', { + 'is-label-inline': inlineLabel, + } ) } + > + <span className="wp-block-form-input__label-content"> + <RichText.Content value={ label } /> + </span> + <TagName + className={ inputClasses } + type={ 'textarea' === type ? undefined : type } + name={ name || getNameFromLabel( label ) } + required={ required } + aria-required={ required } + placeholder={ placeholder || undefined } + style={ inputStyle } + /> + </label> + { /* eslint-enable jsx-a11y/label-has-associated-control */ } + </div> ); - /* eslint-enable jsx-a11y/label-has-associated-control */ } diff --git a/test/integration/fixtures/blocks/core__form-input.html b/test/integration/fixtures/blocks/core__form-input.html index f30d44a2503d20..33f1fe88c2c6a1 100644 --- a/test/integration/fixtures/blocks/core__form-input.html +++ b/test/integration/fixtures/blocks/core__form-input.html @@ -1,3 +1,3 @@ <!-- wp:form-input --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Label</span><input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Label</span><input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/></label></div> <!-- /wp:form-input --> diff --git a/test/integration/fixtures/blocks/core__form-input.parsed.json b/test/integration/fixtures/blocks/core__form-input.parsed.json index f92379b595276f..5470c653c403b5 100644 --- a/test/integration/fixtures/blocks/core__form-input.parsed.json +++ b/test/integration/fixtures/blocks/core__form-input.parsed.json @@ -3,9 +3,9 @@ "blockName": "core/form-input", "attrs": {}, "innerBlocks": [], - "innerHTML": "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Label</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/></label>\n", + "innerHTML": "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Label</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/></label></div>\n", "innerContent": [ - "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Label</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/></label>\n" + "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Label</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/></label></div>\n" ] } ] diff --git a/test/integration/fixtures/blocks/core__form-input.serialized.html b/test/integration/fixtures/blocks/core__form-input.serialized.html index f30d44a2503d20..33f1fe88c2c6a1 100644 --- a/test/integration/fixtures/blocks/core__form-input.serialized.html +++ b/test/integration/fixtures/blocks/core__form-input.serialized.html @@ -1,3 +1,3 @@ <!-- wp:form-input --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Label</span><input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Label</span><input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/></label></div> <!-- /wp:form-input --> diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html new file mode 100644 index 00000000000000..08ea6618386200 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.html @@ -0,0 +1,6 @@ +<!-- wp:form-input --> +<label class="wp-block-form-input__label"> + <span class="wp-block-form-input__label-content">Label</span> + <input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/> +</label> +<!-- /wp:form-input --> diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json new file mode 100644 index 00000000000000..fee4df284f1156 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.json @@ -0,0 +1,15 @@ +[ + { + "name": "core/form-input", + "isValid": true, + "attributes": { + "type": "text", + "label": "Label", + "inlineLabel": false, + "required": false, + "value": "", + "visibilityPermissions": "all" + }, + "innerBlocks": [] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json new file mode 100644 index 00000000000000..645337cbfdb4a1 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.parsed.json @@ -0,0 +1,11 @@ +[ + { + "blockName": "core/form-input", + "attrs": {}, + "innerBlocks": [], + "innerHTML": "\n<label class=\"wp-block-form-input__label\">\n\t<span class=\"wp-block-form-input__label-content\">Label</span>\n\t<input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/>\n</label>\n", + "innerContent": [ + "\n<label class=\"wp-block-form-input__label\">\n\t<span class=\"wp-block-form-input__label-content\">Label</span>\n\t<input class=\"wp-block-form-input__input\" type=\"text\" name=\"label\" aria-required=\"false\"/>\n</label>\n" + ] + } +] diff --git a/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html new file mode 100644 index 00000000000000..33f1fe88c2c6a1 --- /dev/null +++ b/test/integration/fixtures/blocks/core__form-input__deprecated-v1.serialized.html @@ -0,0 +1,3 @@ +<!-- wp:form-input --> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Label</span><input class="wp-block-form-input__input" type="text" name="label" aria-required="false"/></label></div> +<!-- /wp:form-input --> diff --git a/test/integration/fixtures/blocks/core__form.html b/test/integration/fixtures/blocks/core__form.html index f443172601a3b5..825389eb75ecd3 100644 --- a/test/integration/fixtures/blocks/core__form.html +++ b/test/integration/fixtures/blocks/core__form.html @@ -1,19 +1,19 @@ <!-- wp:form --> <form class="wp-block-form" enctype="text/plain"> -<!-- wp:form-input {"label":"Name","required":true} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Name</span><input class="wp-block-form-input__input" type="text" name="name" required aria-required="true"/></label> +<!-- wp:form-input --> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Name</span><input class="wp-block-form-input__input" type="text" name="name" required aria-required="true"/></label></div> <!-- /wp:form-input --> -<!-- wp:form-input {"type":"email","label":"Email","required":true} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Email</span><input class="wp-block-form-input__input" type="email" name="email" required aria-required="true"/></label> +<!-- wp:form-input {"type":"email"} --> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Email</span><input class="wp-block-form-input__input" type="email" name="email" required aria-required="true"/></label></div> <!-- /wp:form-input --> -<!-- wp:form-input {"type":"url","label":"Website"} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Website</span><input class="wp-block-form-input__input" type="url" name="website" aria-required="false"/></label> +<!-- wp:form-input {"type":"url"} --> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Website</span><input class="wp-block-form-input__input" type="url" name="website" aria-required="false"/></label></div> <!-- /wp:form-input --> -<!-- wp:form-input {"type":"textarea","label":"Comment","required":true} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Comment</span><textarea class="wp-block-form-input__input" name="comment" required aria-required="true"></textarea></label> +<!-- wp:form-input {"type":"textarea"} --> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Comment</span><textarea class="wp-block-form-input__input" name="comment" required aria-required="true"></textarea></label></div> <!-- /wp:form-input --> <!-- wp:form-submit-button --> diff --git a/test/integration/fixtures/blocks/core__form.parsed.json b/test/integration/fixtures/blocks/core__form.parsed.json index fb2daff4a47411..e33849b5be5041 100644 --- a/test/integration/fixtures/blocks/core__form.parsed.json +++ b/test/integration/fixtures/blocks/core__form.parsed.json @@ -5,52 +5,44 @@ "innerBlocks": [ { "blockName": "core/form-input", - "attrs": { - "label": "Name", - "required": true - }, + "attrs": {}, "innerBlocks": [], - "innerHTML": "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Name</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"name\" required aria-required=\"true\"/></label>\n", + "innerHTML": "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Name</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"name\" required aria-required=\"true\"/></label></div>\n", "innerContent": [ - "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Name</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"name\" required aria-required=\"true\"/></label>\n" + "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Name</span><input class=\"wp-block-form-input__input\" type=\"text\" name=\"name\" required aria-required=\"true\"/></label></div>\n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "email", - "label": "Email", - "required": true + "type": "email" }, "innerBlocks": [], - "innerHTML": "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Email</span><input class=\"wp-block-form-input__input\" type=\"email\" name=\"email\" required aria-required=\"true\"/></label>\n", + "innerHTML": "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Email</span><input class=\"wp-block-form-input__input\" type=\"email\" name=\"email\" required aria-required=\"true\"/></label></div>\n", "innerContent": [ - "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Email</span><input class=\"wp-block-form-input__input\" type=\"email\" name=\"email\" required aria-required=\"true\"/></label>\n" + "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Email</span><input class=\"wp-block-form-input__input\" type=\"email\" name=\"email\" required aria-required=\"true\"/></label></div>\n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "url", - "label": "Website" + "type": "url" }, "innerBlocks": [], - "innerHTML": "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Website</span><input class=\"wp-block-form-input__input\" type=\"url\" name=\"website\" aria-required=\"false\"/></label>\n", + "innerHTML": "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Website</span><input class=\"wp-block-form-input__input\" type=\"url\" name=\"website\" aria-required=\"false\"/></label></div>\n", "innerContent": [ - "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Website</span><input class=\"wp-block-form-input__input\" type=\"url\" name=\"website\" aria-required=\"false\"/></label>\n" + "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Website</span><input class=\"wp-block-form-input__input\" type=\"url\" name=\"website\" aria-required=\"false\"/></label></div>\n" ] }, { "blockName": "core/form-input", "attrs": { - "type": "textarea", - "label": "Comment", - "required": true + "type": "textarea" }, "innerBlocks": [], - "innerHTML": "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Comment</span><textarea class=\"wp-block-form-input__input\" name=\"comment\" required aria-required=\"true\"></textarea></label>\n", + "innerHTML": "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Comment</span><textarea class=\"wp-block-form-input__input\" name=\"comment\" required aria-required=\"true\"></textarea></label></div>\n", "innerContent": [ - "\n<label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Comment</span><textarea class=\"wp-block-form-input__input\" name=\"comment\" required aria-required=\"true\"></textarea></label>\n" + "\n<div class=\"wp-block-form-input\"><label class=\"wp-block-form-input__label\"><span class=\"wp-block-form-input__label-content\">Comment</span><textarea class=\"wp-block-form-input__input\" name=\"comment\" required aria-required=\"true\"></textarea></label></div>\n" ] }, { diff --git a/test/integration/fixtures/blocks/core__form.serialized.html b/test/integration/fixtures/blocks/core__form.serialized.html index 9bfff780f50dbf..e3209102ce20be 100644 --- a/test/integration/fixtures/blocks/core__form.serialized.html +++ b/test/integration/fixtures/blocks/core__form.serialized.html @@ -1,18 +1,18 @@ <!-- wp:form --> <form class="wp-block-form" enctype="text/plain"><!-- wp:form-input --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Name</span><input class="wp-block-form-input__input" type="text" name="name" required aria-required="true"/></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Name</span><input class="wp-block-form-input__input" type="text" name="name" required aria-required="true"/></label></div> <!-- /wp:form-input --> <!-- wp:form-input {"type":"email"} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Email</span><input class="wp-block-form-input__input" type="email" name="email" required aria-required="true"/></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Email</span><input class="wp-block-form-input__input" type="email" name="email" required aria-required="true"/></label></div> <!-- /wp:form-input --> <!-- wp:form-input {"type":"url"} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Website</span><input class="wp-block-form-input__input" type="url" name="website" aria-required="false"/></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Website</span><input class="wp-block-form-input__input" type="url" name="website" aria-required="false"/></label></div> <!-- /wp:form-input --> <!-- wp:form-input {"type":"textarea"} --> -<label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Comment</span><textarea class="wp-block-form-input__input" name="comment" required aria-required="true"></textarea></label> +<div class="wp-block-form-input"><label class="wp-block-form-input__label"><span class="wp-block-form-input__label-content">Comment</span><textarea class="wp-block-form-input__input" name="comment" required aria-required="true"></textarea></label></div> <!-- /wp:form-input --> <!-- wp:form-submit-button --> From 55ff770cc7e65d26c6480cd2d0c66ec74404cc78 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Tue, 12 Dec 2023 10:04:27 +0000 Subject: [PATCH 135/325] Fix: Use span on template list titles. (#56955) --- .../src/components/page-templates/dataviews-templates.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index e51f9ba970c4cd..07de0cb73ff445 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -83,7 +83,7 @@ function TemplateTitle( { item } ) { const { isCustomized } = useAddedBy( item.type, item.id ); return ( <VStack spacing={ 1 }> - <View as="h3"> + <View as="span" className="edit-site-list-title__customized-info"> <Link params={ { postId: item.id, From 16ec554a2605d45414ee121e2f36c1d7177974ad Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Tue, 12 Dec 2023 11:25:59 +0100 Subject: [PATCH 136/325] Site editor: do not use navigator's internal classname (#56911) * Apply the `edit-site-sidebar__screen-wrapper` classname to all navigator screens in the site editor sidebar * Use the newly added classname instead of the private component's classname --- .../edit-site/src/components/sidebar/index.js | 65 ++++++++++++------- .../src/components/sidebar/style.scss | 22 +++---- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/packages/edit-site/src/components/sidebar/index.js b/packages/edit-site/src/components/sidebar/index.js index b66bf4390a6bcf..3fa1280d59f427 100644 --- a/packages/edit-site/src/components/sidebar/index.js +++ b/packages/edit-site/src/components/sidebar/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classNames from 'classnames'; + /** * WordPress dependencies */ @@ -33,53 +38,65 @@ import DataViewsSidebarContent from '../sidebar-dataviews'; const { useLocation } = unlock( routerPrivateApis ); +function SidebarScreenWrapper( { className, ...props } ) { + return ( + <NavigatorScreen + className={ classNames( + 'edit-site-sidebar__screen-wrapper', + className + ) } + { ...props } + /> + ); +} + function SidebarScreens() { useSyncPathWithURL(); return ( <> - <NavigatorScreen path="/"> + <SidebarScreenWrapper path="/"> <SidebarNavigationScreenMain /> - </NavigatorScreen> - <NavigatorScreen path="/navigation"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/navigation"> <SidebarNavigationScreenNavigationMenus /> - </NavigatorScreen> - <NavigatorScreen path="/navigation/:postType/:postId"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/navigation/:postType/:postId"> <SidebarNavigationScreenNavigationMenu /> - </NavigatorScreen> - <NavigatorScreen path="/wp_global_styles"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/wp_global_styles"> <SidebarNavigationScreenGlobalStyles /> - </NavigatorScreen> - <NavigatorScreen path="/page"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/page"> <SidebarNavigationScreenPages /> - </NavigatorScreen> - <NavigatorScreen path="/page/:postId"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/page/:postId"> <SidebarNavigationScreenPage /> - </NavigatorScreen> + </SidebarScreenWrapper> { window?.__experimentalAdminViews && ( - <NavigatorScreen path="/pages"> + <SidebarScreenWrapper path="/pages"> <SidebarNavigationScreen title={ __( 'Pages' ) } backPath="/page" content={ <DataViewsSidebarContent /> } /> - </NavigatorScreen> + </SidebarScreenWrapper> ) } - <NavigatorScreen path="/:postType(wp_template)"> + <SidebarScreenWrapper path="/:postType(wp_template)"> <SidebarNavigationScreenTemplates /> - </NavigatorScreen> - <NavigatorScreen path="/patterns"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/patterns"> <SidebarNavigationScreenPatterns /> - </NavigatorScreen> - <NavigatorScreen path="/:postType(wp_template|wp_template_part)/all"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/:postType(wp_template|wp_template_part)/all"> <SidebarNavigationScreenTemplatesBrowse /> - </NavigatorScreen> - <NavigatorScreen path="/:postType(wp_template_part|wp_block)/:postId"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/:postType(wp_template_part|wp_block)/:postId"> <SidebarNavigationScreenPattern /> - </NavigatorScreen> - <NavigatorScreen path="/:postType(wp_template)/:postId"> + </SidebarScreenWrapper> + <SidebarScreenWrapper path="/:postType(wp_template)/:postId"> <SidebarNavigationScreenTemplate /> - </NavigatorScreen> + </SidebarScreenWrapper> </> ); } diff --git a/packages/edit-site/src/components/sidebar/style.scss b/packages/edit-site/src/components/sidebar/style.scss index 9a3644cc830d56..ef24b0d4b8cf6f 100644 --- a/packages/edit-site/src/components/sidebar/style.scss +++ b/packages/edit-site/src/components/sidebar/style.scss @@ -1,14 +1,17 @@ .edit-site-sidebar__content { flex-grow: 1; overflow-y: auto; +} + +.edit-site-sidebar__screen-wrapper { + @include custom-scrollbars-on-hover(transparent, $gray-700); + scrollbar-gutter: stable; + display: flex; + flex-direction: column; + height: 100%; - .components-navigator-screen { - @include custom-scrollbars-on-hover(transparent, $gray-700); - scrollbar-gutter: stable; - display: flex; - flex-direction: column; - height: 100%; - } + // This matches the logo padding + padding: 0 $grid-unit-15; } .edit-site-sidebar__footer { @@ -17,8 +20,3 @@ margin: 0 $canvas-padding; padding: $canvas-padding 0; } - -.edit-site-sidebar__content > div { - // This matches the logo padding - padding: 0 $grid-unit-15; -} From 4aa1d984375cb9467e9cc5248c9804a4bd97b2f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Dec 2023 19:23:43 +0100 Subject: [PATCH 137/325] DataViews: update sorting semantics (#56717) --- packages/dataviews/src/view-actions.js | 23 +++++++---------------- packages/dataviews/src/view-table.js | 25 +++++++------------------ 2 files changed, 14 insertions(+), 34 deletions(-) diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 4d012d4e5a38ff..a5330c08f299ce 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -232,22 +232,13 @@ function SortMenu( { fields, view, onChangeView } ) { } onSelect={ ( event ) => { event.preventDefault(); - if ( - sortedDirection === direction - ) { - onChangeView( { - ...view, - sort: undefined, - } ); - } else { - onChangeView( { - ...view, - sort: { - field: field.id, - direction, - }, - } ); - } + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); } } > { info.label } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index e34d99008657bc..3f5891f076791e 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -118,24 +118,13 @@ function HeaderMenu( { field, view, onChangeView } ) { } onSelect={ ( event ) => { event.preventDefault(); - if ( - isSorted && - view.sort.direction === - direction - ) { - onChangeView( { - ...view, - sort: undefined, - } ); - } else { - onChangeView( { - ...view, - sort: { - field: field.id, - direction, - }, - } ); - } + onChangeView( { + ...view, + sort: { + field: field.id, + direction, + }, + } ); } } > { info.label } From 5b688d4656dfb9534aa478b1076eabeac2fd7969 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Tue, 12 Dec 2023 19:56:35 +0100 Subject: [PATCH 138/325] Create-block-interactive-template: Add all files to the generated plugin zip (#56943) * Add a files field to the package.json to add all files to the plugin zip * Update changelog --- packages/create-block-interactive-template/CHANGELOG.md | 1 + packages/create-block-interactive-template/index.js | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 735790d07b803e..388c9de959e437 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add all files to the generated plugin zip. [#56943](https://github.com/WordPress/gutenberg/pull/56943) - Prevent crash when Gutenberg plugin is not installed. [#56941](https://github.com/WordPress/gutenberg/pull/56941) ## 1.10.1 (2023-12-07) diff --git a/packages/create-block-interactive-template/index.js b/packages/create-block-interactive-template/index.js index b2682600f7af6d..6e5ffcb9cc9ae6 100644 --- a/packages/create-block-interactive-template/index.js +++ b/packages/create-block-interactive-template/index.js @@ -10,6 +10,7 @@ module.exports = { description: 'An interactive block with the Interactivity API', dashicon: 'media-interactive', npmDependencies: [ '@wordpress/interactivity' ], + customPackageJSON: { files: [ '[^.]*' ] }, supports: { interactivity: true, }, From 482ac0c420ce25b4794fcb87c46667cb62169f18 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Tue, 12 Dec 2023 19:13:10 +0000 Subject: [PATCH 139/325] Fix: Fatal php error if a template was created by an author that was deleted. (#56990) --- lib/compat/wordpress-6.5/rest-api.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/rest-api.php b/lib/compat/wordpress-6.5/rest-api.php index 3b82815c41e420..12d789fb58b869 100644 --- a/lib/compat/wordpress-6.5/rest-api.php +++ b/lib/compat/wordpress-6.5/rest-api.php @@ -91,7 +91,11 @@ function _gutenberg_get_wp_templates_author_text_field( $template_object ) { case 'site': return get_bloginfo( 'name' ); case 'user': - return get_user_by( 'id', $template_object['author'] )->get( 'display_name' ); + $author = get_user_by( 'id', $template_object['author'] ); + if ( ! $author ) { + return __( 'Unknown author', 'gutenberg' ); + } + return $author->get( 'display_name' ); } } From c90bb031ee5ee72ec382c10a123cd713e9c78f65 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 12 Dec 2023 14:23:15 -0500 Subject: [PATCH 140/325] Implement `Tabs` in editor settings (#55360) * implement Tabs in editor settings sidebar * remove duplicated styles from editor sidebar * incorporate initial feedback * pass props to Tabs directly * add `closeGeneralSidebar` to `onSelect` callback * set TabPanels to `focuasable={false}` * detect when sidebar is closed. pass `selectedTabId` of `null` * improve `Tabs` `onSelect` callback * remove `aria-label` and `data-label` props * add note explaining null selected tab when sidebar is closed * update e2e test * style updates * update internal component structure to avoid rerenders * remove fragment * prevent infinite loop when opening third party sidebar * update e2e tests for `Tabs` compatibility * fix double top margin on tabpanels * update to use new tabId prop * remove import that is no longer needed after rebase * fix keyboard navigable blocks test * fix visibility and tab order tests * fix footnotes tests * fix change detection test --- .../editor/various/change-detection.test.js | 6 +- .../specs/editor/various/editor-modes.test.js | 15 +- .../specs/editor/various/preferences.test.js | 10 +- .../specs/editor/various/sidebar.test.js | 29 +-- .../sidebar/settings-header/index.js | 84 ++------- .../sidebar/settings-header/style.scss | 74 -------- .../sidebar/settings-sidebar/index.js | 172 ++++++++++++------ .../src/components/sidebar/style.scss | 16 +- packages/edit-post/src/style.scss | 1 - .../editor/plugins/custom-post-types.spec.js | 2 +- .../block-hierarchy-navigation.spec.js | 2 +- .../specs/editor/various/footnotes.spec.js | 4 +- .../various/keyboard-navigable-blocks.spec.js | 8 +- 13 files changed, 180 insertions(+), 243 deletions(-) delete mode 100644 packages/edit-post/src/components/sidebar/settings-header/style.scss diff --git a/packages/e2e-tests/specs/editor/various/change-detection.test.js b/packages/e2e-tests/specs/editor/various/change-detection.test.js index 62057c4cbb2bc0..0eb673671222f2 100644 --- a/packages/e2e-tests/specs/editor/various/change-detection.test.js +++ b/packages/e2e-tests/specs/editor/various/change-detection.test.js @@ -370,7 +370,11 @@ describe( 'Change detection', () => { it( 'consecutive edits to the same attribute should mark the post as dirty after a save', async () => { // Open the sidebar block settings. await openDocumentSettingsSidebar(); - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + + const blockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + await blockInspectorTab.click(); // Insert a paragraph. await clickBlockAppender(); diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js index 81878ebf7208e3..aea6536f605bb6 100644 --- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js @@ -102,21 +102,24 @@ describe( 'Editing modes (visual/HTML)', () => { expect( title ).toBe( 'Paragraph' ); // The Block inspector should be active. - let blockInspectorTab = await page.$( - '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' + let [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); expect( blockInspectorTab ).not.toBeNull(); await switchEditorModeTo( 'Code' ); // The Block inspector should not be active anymore. - blockInspectorTab = await page.$( - '.edit-post-sidebar__panel-tab.is-active[data-label="Block"]' + [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( blockInspectorTab ).toBeNull(); + expect( blockInspectorTab ).toBeUndefined(); // No block is selected. - await page.click( '.edit-post-sidebar__panel-tab[data-label="Block"]' ); + const inactiveBlockInspectorTab = await page.waitForXPath( + '//button[@role="tab"][contains(text(), "Block")]' + ); + inactiveBlockInspectorTab.click(); const noBlocksElement = await page.$( '.block-editor-block-inspector__no-blocks' ); diff --git a/packages/e2e-tests/specs/editor/various/preferences.test.js b/packages/e2e-tests/specs/editor/various/preferences.test.js index 98249637c7e968..54990a4004422e 100644 --- a/packages/e2e-tests/specs/editor/various/preferences.test.js +++ b/packages/e2e-tests/specs/editor/various/preferences.test.js @@ -17,7 +17,7 @@ describe( 'preferences', () => { async function getActiveSidebarTabText() { try { return await page.$eval( - '.edit-post-sidebar__panel-tab.is-active', + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]', ( node ) => node.textContent ); } catch ( error ) { @@ -29,11 +29,15 @@ describe( 'preferences', () => { } it( 'remembers sidebar dismissal between sessions', async () => { + const blockTab = await page.waitForXPath( + `//button[@role="tab"][contains(text(), 'Block')]` + ); + // Open by default. expect( await getActiveSidebarTabText() ).toBe( 'Post' ); // Change to "Block" tab. - await page.click( '.edit-post-sidebar__panel-tab[aria-label="Block"]' ); + await blockTab.click(); expect( await getActiveSidebarTabText() ).toBe( 'Block' ); // Regression test: Reload resets to document tab. @@ -46,7 +50,7 @@ describe( 'preferences', () => { // Dismiss. await page.click( - '.edit-post-sidebar__panel-tabs [aria-label="Close Settings"]' + 'div[aria-label="Editor settings"] div[role="tablist"] + button[aria-label="Close Settings"]' ); expect( await getActiveSidebarTabText() ).toBe( null ); diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js index 2e5d46eec2f7a2..0cd39093aabb8c 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js @@ -13,7 +13,8 @@ import { } from '@wordpress/e2e-test-utils'; const SIDEBAR_SELECTOR = '.edit-post-sidebar'; -const ACTIVE_SIDEBAR_TAB_SELECTOR = '.edit-post-sidebar__panel-tab.is-active'; +const ACTIVE_SIDEBAR_TAB_SELECTOR = + 'div[aria-label="Editor settings"] [role="tab"][aria-selected="true"]'; const ACTIVE_SIDEBAR_BUTTON_TEXT = 'Post'; describe( 'Sidebar', () => { @@ -99,22 +100,24 @@ describe( 'Sidebar', () => { // Tab lands at first (presumed selected) option "Post". await page.keyboard.press( 'Tab' ); - const isActiveDocumentTab = await page.evaluate( - () => - document.activeElement.textContent === 'Post' && - document.activeElement.classList.contains( 'is-active' ) + + // The Post tab should be focused and selected. + const [ documentInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Post")]' ); - expect( isActiveDocumentTab ).toBe( true ); + expect( documentInspectorTab ).toBeDefined(); + expect( documentInspectorTab ).toHaveFocus(); - // Tab into and activate "Block". - await page.keyboard.press( 'Tab' ); + // Arrow key into and activate "Block". + await page.keyboard.press( 'ArrowRight' ); await page.keyboard.press( 'Space' ); - const isActiveBlockTab = await page.evaluate( - () => - document.activeElement.textContent === 'Block' && - document.activeElement.classList.contains( 'is-active' ) + + // The Block tab should be focused and selected. + const [ blockInspectorTab ] = await page.$x( + '//button[@role="tab"][@aria-selected="true"][contains(text(), "Block")]' ); - expect( isActiveBlockTab ).toBe( true ); + expect( blockInspectorTab ).toBeDefined(); + expect( blockInspectorTab ).toHaveFocus(); } ); it( 'should be possible to programmatically remove Document Settings panels', async () => { diff --git a/packages/edit-post/src/components/sidebar/settings-header/index.js b/packages/edit-post/src/components/sidebar/settings-header/index.js index ef32450e7209fd..368bd3e9e50dbd 100644 --- a/packages/edit-post/src/components/sidebar/settings-header/index.js +++ b/packages/edit-post/src/components/sidebar/settings-header/index.js @@ -1,22 +1,20 @@ /** * WordPress dependencies */ -import { Button } from '@wordpress/components'; -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { __, _x } from '@wordpress/i18n'; +import { useSelect } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editPostStore } from '../../../store'; +import { unlock } from '../../../lock-unlock'; +import { sidebars } from '../settings-sidebar'; -const SettingsHeader = ( { sidebarName } ) => { - const { openGeneralSidebar } = useDispatch( editPostStore ); - const openDocumentSettings = () => - openGeneralSidebar( 'edit-post/document' ); - const openBlockSettings = () => openGeneralSidebar( 'edit-post/block' ); +const { Tabs } = unlock( componentsPrivateApis ); +const SettingsHeader = () => { const { documentLabel, isTemplateMode } = useSelect( ( select ) => { const { getPostTypeLabel, getRenderingMode } = select( editorStore ); @@ -27,66 +25,16 @@ const SettingsHeader = ( { sidebarName } ) => { }; }, [] ); - const [ documentAriaLabel, documentActiveClass ] = - sidebarName === 'edit-post/document' - ? // translators: ARIA label for the Document sidebar tab, selected. %s: Document label. - [ sprintf( __( '%s (selected)' ), documentLabel ), 'is-active' ] - : [ documentLabel, '' ]; - - const [ blockAriaLabel, blockActiveClass ] = - sidebarName === 'edit-post/block' - ? // translators: ARIA label for the Block Settings Sidebar tab, selected. - [ __( 'Block (selected)' ), 'is-active' ] - : // translators: ARIA label for the Block Settings Sidebar tab, not selected. - [ __( 'Block' ), '' ]; - - const [ templateAriaLabel, templateActiveClass ] = - sidebarName === 'edit-post/document' - ? [ __( 'Template (selected)' ), 'is-active' ] - : [ __( 'Template' ), '' ]; - - /* Use a list so screen readers will announce how many tabs there are. */ return ( - <ul> - { ! isTemplateMode && ( - <li> - <Button - onClick={ openDocumentSettings } - className={ `edit-post-sidebar__panel-tab ${ documentActiveClass }` } - aria-label={ documentAriaLabel } - data-label={ documentLabel } - > - { documentLabel } - </Button> - </li> - ) } - { isTemplateMode && ( - <li> - <Button - onClick={ openDocumentSettings } - className={ `edit-post-sidebar__panel-tab ${ templateActiveClass }` } - aria-label={ templateAriaLabel } - data-label={ __( 'Template' ) } - > - { __( 'Template' ) } - </Button> - </li> - ) } - <li> - <Button - onClick={ openBlockSettings } - className={ `edit-post-sidebar__panel-tab ${ blockActiveClass }` } - aria-label={ blockAriaLabel } - // translators: Data label for the Block Settings Sidebar tab. - data-label={ __( 'Block' ) } - > - { - // translators: Text label for the Block Settings Sidebar tab. - __( 'Block' ) - } - </Button> - </li> - </ul> + <Tabs.TabList> + <Tabs.Tab tabId={ sidebars.document }> + { isTemplateMode ? __( 'Template' ) : documentLabel } + </Tabs.Tab> + <Tabs.Tab tabId={ sidebars.block }> + { /* translators: Text label for the Block Settings Sidebar tab. */ } + { __( 'Block' ) } + </Tabs.Tab> + </Tabs.TabList> ); }; diff --git a/packages/edit-post/src/components/sidebar/settings-header/style.scss b/packages/edit-post/src/components/sidebar/settings-header/style.scss deleted file mode 100644 index aaf7698cb6ddb6..00000000000000 --- a/packages/edit-post/src/components/sidebar/settings-header/style.scss +++ /dev/null @@ -1,74 +0,0 @@ -// This tab style CSS is duplicated verbatim in -// /packages/components/src/tab-panel/style.scss -.components-button.edit-post-sidebar__panel-tab { - position: relative; - border-radius: 0; - height: $grid-unit-60; - background: transparent; - border: none; - box-shadow: none; - cursor: pointer; - padding: 3px $grid-unit-20; // Use padding to offset the is-active border, this benefits Windows High Contrast mode - margin-left: 0; - font-weight: 500; - - &:focus:not(:disabled) { - position: relative; - box-shadow: none; - outline: none; - } - - // Tab indicator - &::after { - content: ""; - position: absolute; - right: 0; - bottom: 0; - left: 0; - pointer-events: none; - - // Draw the indicator. - background: var(--wp-admin-theme-color); - height: calc(0 * var(--wp-admin-border-width-focus)); - border-radius: 0; - - // Animation - transition: all 0.1s linear; - @include reduce-motion("transition"); - } - - // Active. - &.is-active::after { - height: calc(1 * var(--wp-admin-border-width-focus)); - - // Windows high contrast mode. - outline: 2px solid transparent; - outline-offset: -1px; - } - - // Focus. - &::before { - content: ""; - position: absolute; - top: $grid-unit-15; - right: $grid-unit-15; - bottom: $grid-unit-15; - left: $grid-unit-15; - pointer-events: none; - - // Draw the indicator. - box-shadow: 0 0 0 0 transparent; - border-radius: $radius-block-ui; - - // Animation - transition: all 0.1s linear; - @include reduce-motion("transition"); - } - - &:focus-visible::before { - box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - - // Windows high contrast mode. - outline: 2px solid transparent; - } -} diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index e566ea400c12b1..9fa27c6ac2adeb 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -5,8 +5,8 @@ import { BlockInspector, store as blockEditorStore, } from '@wordpress/block-editor'; -import { useSelect } from '@wordpress/data'; -import { Platform } from '@wordpress/element'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { Platform, useCallback, useContext } from '@wordpress/element'; import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; @@ -29,54 +29,43 @@ import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; import PluginSidebarEditPost from '../plugin-sidebar'; import TemplateSummary from '../template-summary'; import { store as editPostStore } from '../../../store'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; +import { unlock } from '../../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); const SIDEBAR_ACTIVE_BY_DEFAULT = Platform.select( { web: true, native: false, } ); +export const sidebars = { + document: 'edit-post/document', + block: 'edit-post/block', +}; -const SettingsSidebar = () => { - const { sidebarName, keyboardShortcut, isTemplateMode } = useSelect( - ( select ) => { - // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. - // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. - // If one of the two sidebars is active the component will contain the content of that sidebar. - // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost - // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName - // should contain the sidebar that will be active when the toggle button is pressed. If a block - // is selected, that should be edit-post/block otherwise it's edit-post/document. - let sidebar = select( interfaceStore ).getActiveComplementaryArea( - editPostStore.name - ); - if ( - ! [ 'edit-post/document', 'edit-post/block' ].includes( - sidebar - ) - ) { - if ( select( blockEditorStore ).getBlockSelectionStart() ) { - sidebar = 'edit-post/block'; - } - sidebar = 'edit-post/document'; - } - const shortcut = select( - keyboardShortcutsStore - ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); - return { - sidebarName: sidebar, - keyboardShortcut: shortcut, - isTemplateMode: - select( editorStore ).getRenderingMode() === - 'template-only', - }; - }, - [] - ); +const SidebarContent = ( { + sidebarName, + keyboardShortcut, + isTemplateMode, +} ) => { + // Because `PluginSidebarEditPost` renders a `ComplementaryArea`, we + // need to forward the `Tabs` context so it can be passed through the + // underlying slot/fill. + const tabsContextValue = useContext( Tabs.Context ); return ( <PluginSidebarEditPost identifier={ sidebarName } - header={ <SettingsHeader sidebarName={ sidebarName } /> } + header={ + <Tabs.Context.Provider value={ tabsContextValue }> + <SettingsHeader /> + </Tabs.Context.Provider> + } closeLabel={ __( 'Close Settings' ) } + // This classname is added so we can apply a corrective negative + // margin to the panel. + // see https://github.com/WordPress/gutenberg/pull/55360#pullrequestreview-1737671049 + className="edit-post-sidebar__panel" headerClassName="edit-post-sidebar__panel-tabs" /* translators: button label text should, if possible, be under 16 characters. */ title={ __( 'Settings' ) } @@ -84,25 +73,96 @@ const SettingsSidebar = () => { icon={ isRTL() ? drawerLeft : drawerRight } isActiveByDefault={ SIDEBAR_ACTIVE_BY_DEFAULT } > - { ! isTemplateMode && sidebarName === 'edit-post/document' && ( - <> - <PostStatus /> - <PluginDocumentSettingPanel.Slot /> - <LastRevision /> - <PostTaxonomies /> - <FeaturedImage /> - <PostExcerpt /> - <DiscussionPanel /> - <PageAttributes /> - <MetaBoxes location="side" /> - </> - ) } - { isTemplateMode && sidebarName === 'edit-post/document' && ( - <TemplateSummary /> - ) } - { sidebarName === 'edit-post/block' && <BlockInspector /> } + <Tabs.Context.Provider value={ tabsContextValue }> + <Tabs.TabPanel tabId={ sidebars.document } focusable={ false }> + { ! isTemplateMode && ( + <> + <PostStatus /> + <PluginDocumentSettingPanel.Slot /> + <LastRevision /> + <PostTaxonomies /> + <FeaturedImage /> + <PostExcerpt /> + <DiscussionPanel /> + <PageAttributes /> + <MetaBoxes location="side" /> + </> + ) } + { isTemplateMode && <TemplateSummary /> } + </Tabs.TabPanel> + <Tabs.TabPanel tabId={ sidebars.block } focusable={ false }> + <BlockInspector /> + </Tabs.TabPanel> + </Tabs.Context.Provider> </PluginSidebarEditPost> ); }; +const SettingsSidebar = () => { + const { + sidebarName, + isSettingsSidebarActive, + keyboardShortcut, + isTemplateMode, + } = useSelect( ( select ) => { + // The settings sidebar is used by the edit-post/document and edit-post/block sidebars. + // sidebarName represents the sidebar that is active or that should be active when the SettingsSidebar toggle button is pressed. + // If one of the two sidebars is active the component will contain the content of that sidebar. + // When neither of the two sidebars is active we can not simply return null, because the PluginSidebarEditPost + // component, besides being used to render the sidebar, also renders the toggle button. In that case sidebarName + // should contain the sidebar that will be active when the toggle button is pressed. If a block + // is selected, that should be edit-post/block otherwise it's edit-post/document. + let sidebar = select( interfaceStore ).getActiveComplementaryArea( + editPostStore.name + ); + let isSettingsSidebar = true; + if ( ! [ sidebars.document, sidebars.block ].includes( sidebar ) ) { + isSettingsSidebar = false; + if ( select( blockEditorStore ).getBlockSelectionStart() ) { + sidebar = sidebars.block; + } + sidebar = sidebars.document; + } + const shortcut = select( + keyboardShortcutsStore + ).getShortcutRepresentation( 'core/edit-post/toggle-sidebar' ); + return { + sidebarName: sidebar, + isSettingsSidebarActive: isSettingsSidebar, + keyboardShortcut: shortcut, + isTemplateMode: + select( editorStore ).getRenderingMode() === 'template-only', + }; + }, [] ); + + const { openGeneralSidebar } = useDispatch( editPostStore ); + + const onTabSelect = useCallback( + ( newSelectedTabId ) => { + if ( !! newSelectedTabId ) { + openGeneralSidebar( newSelectedTabId ); + } + }, + [ openGeneralSidebar ] + ); + + return ( + <Tabs + // Due to how this component is controlled (via a value from the + // `interfaceStore`), when the sidebar closes the currently selected + // tab can't be found. This causes the component to continuously reset + // the selection to `null` in an infinite loop.Proactively setting + // the selected tab to `null` avoids that. + selectedTabId={ isSettingsSidebarActive ? sidebarName : null } + onSelect={ onTabSelect } + > + <SidebarContent + sidebarName={ sidebarName } + keyboardShortcut={ keyboardShortcut } + isTemplateMode={ isTemplateMode } + /> + </Tabs> + ); +}; + export default SettingsSidebar; diff --git a/packages/edit-post/src/components/sidebar/style.scss b/packages/edit-post/src/components/sidebar/style.scss index 7b10eaec0d2248..1921c5cfd7b312 100644 --- a/packages/edit-post/src/components/sidebar/style.scss +++ b/packages/edit-post/src/components/sidebar/style.scss @@ -1,20 +1,8 @@ .components-panel__header.edit-post-sidebar__panel-tabs { - justify-content: flex-start; padding-left: 0; padding-right: $grid-unit-20; - border-top: 0; - margin-top: 0; - - ul { - display: flex; - } - li { - margin: 0; - } .components-button.has-icon { - display: none; - margin: 0 0 0 auto; padding: 0; min-width: $icon-size; height: $icon-size; @@ -24,3 +12,7 @@ } } } + +.edit-post-sidebar__panel { + margin-top: -1px; +} diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 53219bc6a37368..88916bf70f76d3 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -12,7 +12,6 @@ @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; -@import "./components/sidebar/settings-header/style.scss"; @import "./components/sidebar/template-summary/style.scss"; @import "./components/text-editor/style.scss"; @import "./components/visual-editor/style.scss"; diff --git a/test/e2e/specs/editor/plugins/custom-post-types.spec.js b/test/e2e/specs/editor/plugins/custom-post-types.spec.js index 17a497f26cee02..01dde03650ef73 100644 --- a/test/e2e/specs/editor/plugins/custom-post-types.spec.js +++ b/test/e2e/specs/editor/plugins/custom-post-types.spec.js @@ -31,7 +31,7 @@ test.describe( 'Test Custom Post Types', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { + .getByRole( 'tab', { name: 'Hierarchical No Title', } ) .click(); diff --git a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js index f0bfe5bff203fb..a695b0a9ead67e 100644 --- a/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js +++ b/test/e2e/specs/editor/various/block-hierarchy-navigation.spec.js @@ -127,7 +127,7 @@ test.describe( 'Navigating the block hierarchy', () => { await pageUtils.pressKeys( 'ctrl+`' ); // Navigate to the block settings sidebar and tweak the column count. - await pageUtils.pressKeys( 'Tab', { times: 5 } ); + await pageUtils.pressKeys( 'Tab', { times: 4 } ); await expect( page.getByRole( 'slider', { name: 'Columns' } ) ).toBeFocused(); diff --git a/test/e2e/specs/editor/various/footnotes.spec.js b/test/e2e/specs/editor/various/footnotes.spec.js index 14a2fc653e3873..6102f48749543f 100644 --- a/test/e2e/specs/editor/various/footnotes.spec.js +++ b/test/e2e/specs/editor/various/footnotes.spec.js @@ -362,7 +362,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); await page.locator( 'a:text("2 Revisions")' ).click(); await page.locator( '.revisions-controls .ui-slider-handle' ).focus(); @@ -440,7 +440,7 @@ test.describe( 'Footnotes', () => { await editor.openDocumentSettingsSidebar(); await page .getByRole( 'region', { name: 'Editor settings' } ) - .getByRole( 'button', { name: 'Post' } ) + .getByRole( 'tab', { name: 'Post' } ) .click(); // Visit the published post. diff --git a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js index 080abe011206a7..84536c88227ce9 100644 --- a/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js +++ b/test/e2e/specs/editor/various/keyboard-navigable-blocks.spec.js @@ -75,9 +75,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( - 'Post (selected)' - ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); } ); test( 'allows tabbing in navigation mode if no block is selected (reverse)', async ( { @@ -151,7 +149,7 @@ test.describe( 'Order of block keyboard navigation', () => { ); await page.keyboard.press( 'Tab' ); - await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Post' ); + await KeyboardNavigableBlocks.expectLabelToHaveFocus( 'Block' ); await pageUtils.pressKeys( 'shift+Tab' ); await KeyboardNavigableBlocks.expectLabelToHaveFocus( @@ -233,7 +231,7 @@ class KeyboardNavigableBlocks { await expect( activeElement ).toHaveText( paragraphText ); await this.page.keyboard.press( 'Tab' ); - await this.expectLabelToHaveFocus( 'Post' ); + await this.expectLabelToHaveFocus( 'Block' ); // Need to shift+tab here to end back in the block. If not, we'll be in the next region and it will only require 4 region jumps instead of 5. await this.pageUtils.pressKeys( 'shift+Tab' ); From b149b9647f29fe57ab4feb04545c7e99cf61faa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 12 Dec 2023 20:57:14 +0100 Subject: [PATCH 141/325] Fix e2e test (#56992) --- test/e2e/specs/site-editor/new-templates-list.spec.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js index 8bb98b6ad5355a..34daeb6d40f09b 100644 --- a/test/e2e/specs/site-editor/new-templates-list.spec.js +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -33,10 +33,7 @@ test.describe( 'Templates', () => { name: 'Template', includeHidden: true, } ) - .getByRole( 'heading', { - level: 3, - includeHidden: true, - } ) + .getByRole( 'link', { includeHidden: true } ) .first(); await expect( firstTitle ).toHaveText( 'Tag Archives' ); // Ascending by title. @@ -57,7 +54,7 @@ test.describe( 'Templates', () => { await page.keyboard.type( 'tag' ); const titles = page .getByRole( 'region', { name: 'Template' } ) - .getByRole( 'heading', { level: 3 } ); + .getByRole( 'link' ); await expect( titles ).toHaveCount( 1 ); await expect( titles.first() ).toHaveText( 'Tag Archives' ); await page.getByRole( 'button', { name: 'Reset filters' } ).click(); From 4d61a94e0622eed5e45529a7fcb9a95cd09a61a7 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 13 Dec 2023 08:54:27 +1100 Subject: [PATCH 142/325] Allow dragging between adjacent container blocks based on a threshold (#56466) * Try: Allow drag between blocks based on a threshold * Update insertion point to reflect threshold * Fix threshold for Group block by passing in dropZoneElement * Simplify by reusing operation option * Re-use logic that factors in the dragged blocks if we are ultimately dropping at the same level * Simplify a little further * Factor in orientation * Add minimum height for threshold so that short blocks are still usable * Allow threshold when parent block is horizontal, i.e. a Row block --- .../components/use-block-drop-zone/index.js | 128 ++++++++++++++++-- .../src/components/use-on-block-drop/index.js | 3 +- packages/block-library/src/group/edit.js | 5 +- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 25dc6ee408982f..cb3c3ae6a28a3d 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -20,6 +20,10 @@ import { } from '../../utils/math'; import { store as blockEditorStore } from '../../store'; +const THRESHOLD_DISTANCE = 30; +const MINIMUM_HEIGHT_FOR_THRESHOLD = 120; +const MINIMUM_WIDTH_FOR_THRESHOLD = 120; + /** @typedef {import('../../utils/math').WPPoint} WPPoint */ /** @typedef {import('../use-on-block-drop/types').WPDropOperation} WPDropOperation */ @@ -48,24 +52,86 @@ import { store as blockEditorStore } from '../../store'; * @param {WPBlockData[]} blocksData The block data list. * @param {WPPoint} position The position of the item being dragged. * @param {WPBlockListOrientation} orientation The orientation of the block list. + * @param {Object} options Additional options. * @return {[number, WPDropOperation]} The drop target position. */ export function getDropTargetPosition( blocksData, position, - orientation = 'vertical' + orientation = 'vertical', + options = {} ) { const allowedEdges = orientation === 'horizontal' ? [ 'left', 'right' ] : [ 'top', 'bottom' ]; - const isRightToLeft = isRTL(); - let nearestIndex = 0; let insertPosition = 'before'; let minDistance = Infinity; + const { + dropZoneElement, + parentBlockOrientation, + rootBlockIndex = 0, + } = options; + + // Allow before/after when dragging over the top/bottom edges of the drop zone. + if ( dropZoneElement && parentBlockOrientation !== 'horizontal' ) { + const rect = dropZoneElement.getBoundingClientRect(); + const [ distance, edge ] = getDistanceToNearestEdge( position, rect, [ + 'top', + 'bottom', + ] ); + + // If dragging over the top or bottom of the drop zone, insert the block + // before or after the parent block. This only applies to blocks that use + // a drop zone element, typically container blocks such as Group or Cover. + if ( + rect.height > MINIMUM_HEIGHT_FOR_THRESHOLD && + distance < THRESHOLD_DISTANCE + ) { + if ( edge === 'top' ) { + return [ rootBlockIndex, 'before' ]; + } + if ( edge === 'bottom' ) { + return [ rootBlockIndex + 1, 'after' ]; + } + } + } + + const isRightToLeft = isRTL(); + + // Allow before/after when dragging over the left/right edges of the drop zone. + if ( dropZoneElement && parentBlockOrientation === 'horizontal' ) { + const rect = dropZoneElement.getBoundingClientRect(); + const [ distance, edge ] = getDistanceToNearestEdge( position, rect, [ + 'left', + 'right', + ] ); + + // If dragging over the left or right of the drop zone, insert the block + // before or after the parent block. This only applies to blocks that use + // a drop zone element, typically container blocks such as Group. + if ( + rect.width > MINIMUM_WIDTH_FOR_THRESHOLD && + distance < THRESHOLD_DISTANCE + ) { + if ( + ( isRightToLeft && edge === 'right' ) || + ( ! isRightToLeft && edge === 'left' ) + ) { + return [ rootBlockIndex, 'before' ]; + } + if ( + ( isRightToLeft && edge === 'left' ) || + ( ! isRightToLeft && edge === 'right' ) + ) { + return [ rootBlockIndex + 1, 'after' ]; + } + } + } + blocksData.forEach( ( { isUnmodifiedDefaultBlock, getBoundingClientRect, blockIndex } ) => { const rect = getBoundingClientRect(); @@ -150,19 +216,27 @@ export default function useBlockDropZone( { operation: 'insert', } ); - const isDisabled = useSelect( + const { isDisabled, parentBlockClientId, rootBlockIndex } = useSelect( ( select ) => { const { __unstableIsWithinBlockOverlay, __unstableHasActiveBlockOverlayActive, + getBlockIndex, + getBlockParents, getBlockEditingMode, } = select( blockEditorStore ); const blockEditingMode = getBlockEditingMode( targetRootClientId ); - return ( - blockEditingMode !== 'default' || - __unstableHasActiveBlockOverlayActive( targetRootClientId ) || - __unstableIsWithinBlockOverlay( targetRootClientId ) - ); + return { + parentBlockClientId: + getBlockParents( targetRootClientId, true )[ 0 ] || '', + rootBlockIndex: getBlockIndex( targetRootClientId ), + isDisabled: + blockEditingMode !== 'default' || + __unstableHasActiveBlockOverlayActive( + targetRootClientId + ) || + __unstableIsWithinBlockOverlay( targetRootClientId ), + }; }, [ targetRootClientId ] ); @@ -172,9 +246,15 @@ export default function useBlockDropZone( { const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); - const onBlockDrop = useOnBlockDrop( targetRootClientId, dropTarget.index, { - operation: dropTarget.operation, - } ); + const onBlockDrop = useOnBlockDrop( + dropTarget.operation === 'before' || dropTarget.operation === 'after' + ? parentBlockClientId + : targetRootClientId, + dropTarget.index, + { + operation: dropTarget.operation, + } + ); const throttled = useThrottle( useCallback( ( event, ownerDocument ) => { @@ -211,7 +291,16 @@ export default function useBlockDropZone( { const [ targetIndex, operation ] = getDropTargetPosition( blocksData, { x: event.clientX, y: event.clientY }, - getBlockListSettings( targetRootClientId )?.orientation + getBlockListSettings( targetRootClientId )?.orientation, + { + dropZoneElement, + parentBlockClientId, + parentBlockOrientation: parentBlockClientId + ? getBlockListSettings( parentBlockClientId ) + ?.orientation + : undefined, + rootBlockIndex, + } ); registry.batch( () => { @@ -219,18 +308,29 @@ export default function useBlockDropZone( { index: targetIndex, operation, } ); - showInsertionPoint( targetRootClientId, targetIndex, { + + const insertionPointClientId = [ + 'before', + 'after', + ].includes( operation ) + ? parentBlockClientId + : targetRootClientId; + + showInsertionPoint( insertionPointClientId, targetIndex, { operation, } ); } ); }, [ + dropZoneElement, getBlocks, targetRootClientId, getBlockListSettings, registry, showInsertionPoint, getBlockIndex, + parentBlockClientId, + rootBlockIndex, ] ), 200 diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index 72ea6a698c3439..ab0da8ad99e2ab 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -292,9 +292,10 @@ export default function useOnBlockDrop( operation, getBlockOrder, getBlocksByClientId, - insertBlocks, moveBlocksToPosition, + registry, removeBlocks, + replaceBlocks, targetBlockIndex, targetRootClientId, ] diff --git a/packages/block-library/src/group/edit.js b/packages/block-library/src/group/edit.js index 9c8690c4e0e8e2..a763bc95e60d7c 100644 --- a/packages/block-library/src/group/edit.js +++ b/packages/block-library/src/group/edit.js @@ -10,6 +10,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { SelectControl } from '@wordpress/components'; +import { useRef } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { View } from '@wordpress/primitives'; @@ -97,7 +98,8 @@ function GroupEdit( { attributes, name, setAttributes, clientId } ) { themeSupportsLayout || type === 'flex' || type === 'grid'; // Hooks. - const blockProps = useBlockProps(); + const ref = useRef(); + const blockProps = useBlockProps( { ref } ); const [ showPlaceholder, setShowPlaceholder ] = useShouldShowPlaceHolder( { attributes, @@ -124,6 +126,7 @@ function GroupEdit( { attributes, name, setAttributes, clientId } ) { ? blockProps : { className: 'wp-block-group__inner-container' }, { + dropZoneElement: ref.current, templateLock, allowedBlocks, renderAppender, From 9a46ad1773b736eeffb54755e7cddb7e25c8462c Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Wed, 13 Dec 2023 09:15:36 +1100 Subject: [PATCH 143/325] Global style revisions: show change summary on selected item (#56577) * Moving date format setting call into the comoponent rejigging getLabel function adding css var * Testing a message to indicate that the revisions state is the same as the editor state. * Working on change list, adding translations. WIP * Adding more translations. * Revert button-in-item for another day Reduce depth of changeset and remove unused translations Display aria-label on button instead of tooltip * Removing shuffle function and fixing up block spacing translation * Using the revision has the Map key. This allows us to cache the revision sets themselves and account for changes to Unsaved revisions. * Used WeakMap in favour of Map for garbage collection, if it helps at all * Remove hasMore var - unneeded because it's only used once * Tidying up - remove .map loop * getGlobalStylesChanges was doing nothing! Removed. * Using revision + previousRevision combo for cache key to ensure that the results are cached for the same two objects Returning from cache where maxResults value is smaller than cached results Added first tests * Moving maxResults decisions to consuming component. getRevisionChanges returns an unadulterated array. * Move get blockNames to main component * Have to use map because WeakMap wants the same reference as the object key. * Remove the trailing comma on truncated results * Test commit: listing changes, showing `and n more` * Test commit: grouping changes using tuples * Reverting back to comma-separate list of changes Added e2e assertion * Swapping order of author name and changes block Moving everything into the button so it's clickable. * Don't live in the past, man --- .../screen-revisions/get-revision-changes.js | 171 ++++++++++++++++ .../global-styles/screen-revisions/index.js | 15 +- .../screen-revisions/revisions-buttons.js | 103 ++++++++-- .../global-styles/screen-revisions/style.scss | 12 +- .../test/get-revision-changes.js | 191 ++++++++++++++++++ .../user-global-styles-revisions.spec.js | 5 + 6 files changed, 467 insertions(+), 30 deletions(-) create mode 100644 packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js create mode 100644 packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js b/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js new file mode 100644 index 00000000000000..fed075eb923ff4 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-revisions/get-revision-changes.js @@ -0,0 +1,171 @@ +/** + * WordPress dependencies + */ +import { __, sprintf } from '@wordpress/i18n'; + +const globalStylesChangesCache = new Map(); +const EMPTY_ARRAY = []; + +const translationMap = { + caption: __( 'Caption' ), + link: __( 'Link' ), + button: __( 'Button' ), + heading: __( 'Heading' ), + 'settings.color': __( 'Color settings' ), + 'settings.typography': __( 'Typography settings' ), + 'styles.color': __( 'Colors' ), + 'styles.spacing': __( 'Spacing' ), + 'styles.typography': __( 'Typography' ), +}; + +const isObject = ( obj ) => obj !== null && typeof obj === 'object'; + +/** + * Get the translation for a given global styles key. + * @param {string} key A key representing a path to a global style property or setting. + * @param {Record<string,string>} blockNames A key/value pair object of block names and their rendered titles. + * @return {string|undefined} A translated key or undefined if no translation exists. + */ +function getTranslation( key, blockNames ) { + if ( translationMap[ key ] ) { + return translationMap[ key ]; + } + + const keyArray = key.split( '.' ); + + if ( keyArray?.[ 0 ] === 'blocks' ) { + const blockName = blockNames[ keyArray[ 1 ] ]; + return blockName + ? sprintf( + // translators: %s: block name. + __( '%s block' ), + blockName + ) + : keyArray[ 1 ]; + } + + if ( keyArray?.[ 0 ] === 'elements' ) { + return sprintf( + // translators: %s: element name, e.g., heading button, link, caption. + __( '%s element' ), + translationMap[ keyArray[ 1 ] ] + ); + } + + return undefined; +} + +/** + * A deep comparison of two objects, optimized for comparing global styles. + * @param {Object} changedObject The changed object to compare. + * @param {Object} originalObject The original object to compare against. + * @param {string} parentPath A key/value pair object of block names and their rendered titles. + * @return {string[]} An array of paths whose values have changed. + */ +function deepCompare( changedObject, originalObject, parentPath = '' ) { + // We have two non-object values to compare. + if ( ! isObject( changedObject ) && ! isObject( originalObject ) ) { + /* + * Only return a path if the value has changed. + * And then only the path name up to 2 levels deep. + */ + return changedObject !== originalObject + ? parentPath.split( '.' ).slice( 0, 2 ).join( '.' ) + : undefined; + } + + // Enable comparison when an object doesn't have a corresponding property to compare. + changedObject = isObject( changedObject ) ? changedObject : {}; + originalObject = isObject( originalObject ) ? originalObject : {}; + + const allKeys = new Set( [ + ...Object.keys( changedObject ), + ...Object.keys( originalObject ), + ] ); + + let diffs = []; + for ( const key of allKeys ) { + const path = parentPath ? parentPath + '.' + key : key; + const changedPath = deepCompare( + changedObject[ key ], + originalObject[ key ], + path + ); + if ( changedPath ) { + diffs = diffs.concat( changedPath ); + } + } + return diffs; +} + +/** + * Get an array of translated summarized global styles changes. + * Results are cached using a Map() key of `JSON.stringify( { revision, previousRevision } )`. + * + * @param {Object} revision The changed object to compare. + * @param {Object} previousRevision The original object to compare against. + * @param {Record<string,string>} blockNames A key/value pair object of block names and their rendered titles. + * @return {string[]} An array of translated changes. + */ +export default function getRevisionChanges( + revision, + previousRevision, + blockNames +) { + const cacheKey = JSON.stringify( { revision, previousRevision } ); + + if ( globalStylesChangesCache.has( cacheKey ) ) { + return globalStylesChangesCache.get( cacheKey ); + } + + /* + * Compare the two revisions with normalized keys. + * The order of these keys determines the order in which + * they'll appear in the results. + */ + const changedValueTree = deepCompare( + { + styles: { + color: revision?.styles?.color, + typography: revision?.styles?.typography, + spacing: revision?.styles?.spacing, + }, + blocks: revision?.styles?.blocks, + elements: revision?.styles?.elements, + settings: revision?.settings, + }, + { + styles: { + color: previousRevision?.styles?.color, + typography: previousRevision?.styles?.typography, + spacing: previousRevision?.styles?.spacing, + }, + blocks: previousRevision?.styles?.blocks, + elements: previousRevision?.styles?.elements, + settings: previousRevision?.settings, + } + ); + + if ( ! changedValueTree.length ) { + globalStylesChangesCache.set( cacheKey, EMPTY_ARRAY ); + return EMPTY_ARRAY; + } + + // Remove duplicate results. + const result = [ ...new Set( changedValueTree ) ] + /* + * Translate the keys. + * Remove duplicate or empty translations. + */ + .reduce( ( acc, curr ) => { + const translation = getTranslation( curr, blockNames ); + if ( translation && ! acc.includes( translation ) ) { + acc.push( translation ); + } + return acc; + }, [] ); + + globalStylesChangesCache.set( cacheKey, result ); + + return result; +} diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 90bf68e579cb7c..aa380c5a9fbd0b 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -7,7 +7,6 @@ import { __experimentalUseNavigator as useNavigator, __experimentalConfirmDialog as ConfirmDialog, Spinner, - __experimentalSpacer as Spacer, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; @@ -135,7 +134,8 @@ function ScreenRevisions() { } }, [ shouldSelectFirstItem, firstRevision ] ); - // Only display load button if there is a revision to load and it is different from the current editor styles. + // Only display load button if there is a revision to load, + // and it is different from the current editor styles. const isLoadButtonEnabled = !! currentlySelectedRevisionId && ! selectedRevisionMatchesEditorStyles; const shouldShowRevisions = ! isLoading && revisions.length; @@ -156,7 +156,7 @@ function ScreenRevisions() { { isLoading && ( <Spinner className="edit-site-global-styles-screen-revisions__loading" /> ) } - { shouldShowRevisions ? ( + { shouldShowRevisions && ( <> <Revisions blocks={ blocks } @@ -168,6 +168,7 @@ function ScreenRevisions() { onChange={ selectRevision } selectedRevisionId={ currentlySelectedRevisionId } userRevisions={ revisions } + canApplyRevision={ isLoadButtonEnabled } /> { isLoadButtonEnabled && ( <SidebarFixedBottom> @@ -215,14 +216,6 @@ function ScreenRevisions() { </ConfirmDialog> ) } </> - ) : ( - <Spacer marginX={ 4 } data-testid="global-styles-no-revisions"> - { - // Adding an existing translation here in case these changes are shipped to WordPress 6.3. - // Later we could update to something better, e.g., "There are currently no style revisions.". - __( 'No results found.' ) - } - </Spacer> ) } </> ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index 2786bf6d791212..08930069425729 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -6,28 +6,69 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { dateI18n, getDate, humanTimeDiff, getSettings } from '@wordpress/date'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { getBlockTypes } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import getRevisionChanges from './get-revision-changes'; const DAY_IN_MILLISECONDS = 60 * 60 * 1000 * 24; +const MAX_CHANGES = 7; + +function ChangesSummary( { revision, previousRevision, blockNames } ) { + const changes = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + const changesLength = changes.length; + + if ( ! changesLength ) { + return null; + } + + // Truncate to `n` results if necessary. + if ( changesLength > MAX_CHANGES ) { + const deleteCount = changesLength - MAX_CHANGES; + const andMoreText = sprintf( + // translators: %d: number of global styles changes that are not displayed in the UI. + _n( '…and %d more change.', '…and %d more changes.', deleteCount ), + deleteCount + ); + changes.splice( MAX_CHANGES, deleteCount, andMoreText ); + } + + return ( + <span + data-testid="global-styles-revision-changes" + className="edit-site-global-styles-screen-revisions__changes" + > + { changes.join( ', ' ) } + </span> + ); +} /** * Returns a button label for the revision. * * @param {string|number} id A revision object. - * @param {boolean} isLatest Whether the revision is the most current. * @param {string} authorDisplayName Author name. * @param {string} formattedModifiedDate Revision modified date formatted. + * @param {boolean} areStylesEqual Whether the revision matches the current editor styles. * @return {string} Translated label. */ function getRevisionLabel( id, - isLatest, authorDisplayName, - formattedModifiedDate + formattedModifiedDate, + areStylesEqual ) { if ( 'parent' === id ) { return __( 'Reset the styles to the theme defaults' ); @@ -35,21 +76,23 @@ function getRevisionLabel( if ( 'unsaved' === id ) { return sprintf( - /* translators: %s author display name */ + /* translators: %s: author display name */ __( 'Unsaved changes by %s' ), authorDisplayName ); } - return isLatest + return areStylesEqual ? sprintf( - /* translators: %1$s author display name, %2$s: revision creation date */ - __( 'Changes saved by %1$s on %2$s (current)' ), + // translators: %1$s: author display name, %2$s: revision creation date. + __( + 'Changes saved by %1$s on %2$s. This revision matches current editor styles.' + ), authorDisplayName, formattedModifiedDate ) : sprintf( - /* translators: %1$s author display name, %2$s: revision creation date */ + // translators: %1$s: author display name, %2$s: revision creation date. __( 'Changes saved by %1$s on %2$s' ), authorDisplayName, formattedModifiedDate @@ -67,7 +110,12 @@ function getRevisionLabel( * @param {props} Component props. * @return {JSX.Element} The modal component. */ -function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { +function RevisionsButtons( { + userRevisions, + selectedRevisionId, + onChange, + canApplyRevision, +} ) { const { currentThemeName, currentUser } = useSelect( ( select ) => { const { getCurrentTheme, getCurrentUser } = select( coreStore ); const currentTheme = getCurrentTheme(); @@ -77,8 +125,15 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { currentUser: getCurrentUser(), }; }, [] ); + const blockNames = useMemo( () => { + const blockTypes = getBlockTypes(); + return blockTypes.reduce( ( accumulator, { name, title } ) => { + accumulator[ name ] = title; + return accumulator; + }, {} ); + }, [] ); const dateNowInMs = getDate().getTime(); - const { date: dateFormat, datetimeAbbreviated } = getSettings().formats; + const { datetimeAbbreviated } = getSettings().formats; return ( <ol @@ -87,27 +142,29 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { role="group" > { userRevisions.map( ( revision, index ) => { - const { id, isLatest, author, modified } = revision; + const { id, author, modified } = revision; const isUnsaved = 'unsaved' === id; // Unsaved changes are created by the current user. const revisionAuthor = isUnsaved ? currentUser : author; const authorDisplayName = revisionAuthor?.name || __( 'User' ); const authorAvatar = revisionAuthor?.avatar_urls?.[ '48' ]; + const isFirstItem = index === 0; const isSelected = selectedRevisionId ? selectedRevisionId === id - : index === 0; + : isFirstItem; + const areStylesEqual = ! canApplyRevision && isSelected; const isReset = 'parent' === id; const modifiedDate = getDate( modified ); const displayDate = modified && dateNowInMs - modifiedDate.getTime() > DAY_IN_MILLISECONDS - ? dateI18n( dateFormat, modifiedDate ) + ? dateI18n( datetimeAbbreviated, modifiedDate ) : humanTimeDiff( modified ); const revisionLabel = getRevisionLabel( id, - isLatest, authorDisplayName, - dateI18n( datetimeAbbreviated, modifiedDate ) + dateI18n( datetimeAbbreviated, modifiedDate ), + areStylesEqual ); return ( @@ -116,6 +173,7 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { 'edit-site-global-styles-screen-revisions__revision-item', { 'is-selected': isSelected, + 'is-active': areStylesEqual, 'is-reset': isReset, } ) } @@ -127,7 +185,7 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { onClick={ () => { onChange( revision ); } } - label={ revisionLabel } + aria-label={ revisionLabel } > { isReset ? ( <span className="edit-site-global-styles-screen-revisions__description"> @@ -150,6 +208,17 @@ function RevisionsButtons( { userRevisions, selectedRevisionId, onChange } ) { { displayDate } </time> ) } + { isSelected && ( + <ChangesSummary + blockNames={ blockNames } + revision={ revision } + previousRevision={ + index < userRevisions.length + ? userRevisions[ index + 1 ] + : {} + } + /> + ) } <span className="edit-site-global-styles-screen-revisions__meta"> <img alt={ authorDisplayName } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index 6598fcb5ce1c74..d1325f84772a62 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -66,7 +66,7 @@ width: 100%; height: auto; display: block; - padding: $grid-unit-15 $grid-unit-15 $grid-unit-15 $grid-unit-30; + padding: $grid-unit-15 $grid-unit-15 $grid-unit-10 $grid-unit-30; &:focus, &:active { outline: 0; @@ -103,6 +103,7 @@ } } +.edit-site-global-styles-screen-revisions__changes, .edit-site-global-styles-screen-revisions__meta { color: $gray-600; display: flex; @@ -110,7 +111,6 @@ width: 100%; align-items: center; font-size: 12px; - img { width: $grid-unit-20; height: $grid-unit-20; @@ -122,3 +122,11 @@ .edit-site-global-styles-screen-revisions__loading { margin: $grid-unit-30 auto !important; } + +.edit-site-global-styles-screen-revisions__changes { + margin-bottom: $grid-unit-05; + text-align: left; + color: $gray-900; + line-height: $default-line-height; +} + diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js b/packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js new file mode 100644 index 00000000000000..be9a26b97f6885 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-revisions/test/get-revision-changes.js @@ -0,0 +1,191 @@ +/** + * Internal dependencies + */ +import getRevisionChanges from '../get-revision-changes'; + +describe( 'getRevisionChanges', () => { + const revision = { + id: 10, + styles: { + typography: { + fontSize: 'var(--wp--preset--font-size--potato)', + fontStyle: 'normal', + fontWeight: '600', + lineHeight: '1.85', + fontFamily: 'var(--wp--preset--font-family--asparagus)', + }, + spacing: { + padding: { + top: '36px', + right: '89px', + bottom: '133px', + left: 'var(--wp--preset--spacing--20)', + }, + blockGap: '114px', + }, + elements: { + heading: { + typography: { + letterSpacing: '37px', + }, + }, + caption: { + color: { + text: 'var(--wp--preset--color--pineapple)', + }, + }, + }, + color: { + text: 'var(--wp--preset--color--tomato)', + }, + blocks: { + 'core/paragraph': { + color: { + text: '#000000', + }, + }, + }, + }, + settings: { + color: { + palette: { + theme: [ + { + slug: 'one', + color: 'pink', + }, + ], + }, + }, + }, + }; + const previousRevision = { + id: 9, + styles: { + typography: { + fontSize: 'var(--wp--preset--font-size--fungus)', + fontStyle: 'normal', + fontWeight: '600', + lineHeight: '1.85', + fontFamily: 'var(--wp--preset--font-family--grapes)', + }, + spacing: { + padding: { + top: '36px', + right: '89px', + bottom: '133px', + left: 'var(--wp--preset--spacing--20)', + }, + blockGap: '114px', + }, + elements: { + heading: { + typography: { + letterSpacing: '37px', + }, + }, + caption: { + typography: { + fontSize: '1.11rem', + fontStyle: 'normal', + fontWeight: '600', + }, + }, + link: { + typography: { + lineHeight: 2, + textDecoration: 'line-through', + }, + color: { + text: 'var(--wp--preset--color--egg)', + }, + }, + }, + color: { + text: 'var(--wp--preset--color--tomato)', + background: 'var(--wp--preset--color--pumpkin)', + }, + blocks: { + 'core/paragraph': { + color: { + text: '#fff', + }, + }, + }, + }, + settings: { + color: { + palette: { + theme: [ + { + slug: 'one', + color: 'blue', + }, + ], + }, + }, + }, + }; + const blockNames = { + 'core/paragraph': 'Paragraph', + }; + it( 'returns a list of changes and caches them', () => { + const resultA = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + expect( resultA ).toEqual( [ + 'Colors', + 'Typography', + 'Paragraph block', + 'Caption element', + 'Link element', + 'Color settings', + ] ); + + const resultB = getRevisionChanges( + revision, + previousRevision, + blockNames + ); + + expect( resultA ).toBe( resultB ); + } ); + + it( 'skips unknown and unchanged keys', () => { + const result = getRevisionChanges( + { + styles: { + frogs: { + legs: 'green', + }, + typography: { + fontSize: '1rem', + }, + settings: { + '': { + '': 'foo', + }, + }, + }, + }, + { + styles: { + frogs: { + legs: 'yellow', + }, + typography: { + fontSize: '1rem', + }, + settings: { + '': { + '': 'bar', + }, + }, + }, + } + ); + expect( result ).toEqual( [] ); + } ); +} ); diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 2d51b5ac5014b8..a27bb28adbb911 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -55,6 +55,11 @@ test.describe( 'Global styles revisions', () => { name: /^Changes saved by /, } ); + // Shows changes made in the revision. + await expect( + page.getByTestId( 'global-styles-revision-changes' ) + ).toHaveText( 'Colors' ); + // There should be 2 revisions not including the reset to theme defaults button. await expect( revisionButtons ).toHaveCount( currentRevisions.length + 1 From 1dd952f3267200ee5fad288df26033d5fd33854c Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Tue, 12 Dec 2023 22:24:17 +0000 Subject: [PATCH 144/325] Bump plugin version to 17.2.1 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 7970dd5461fc4f..f39a0cb519c8ae 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.2.0 + * Version: 17.2.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index ded852521693f6..6d9382ec9fb2f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 122a1368eaf1ca..580c100354a966 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.2.0", + "version": "17.2.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 2936660d526a8112479d9c87a92cd6ae7a455a94 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Tue, 12 Dec 2023 22:38:23 +0000 Subject: [PATCH 145/325] Update Changelog for 17.2.1 --- changelog.txt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/changelog.txt b/changelog.txt index 2f7d2f02ff6291..65745b416e4bdb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,20 @@ == Changelog == += 17.2.1 = + +## Changelog + +### Bug Fixes + +- Fix: Fatal php error if a template was created by an author that was deleted ([56990](https://github.com/WordPress/gutenberg/pull/56990)) + +## Contributors + +The following contributors merged PRs in this release: + +@jorgefilipecosta + + = 17.2.0 = From e07802b3d2395c4563c6e8a8013bb2224e0927d7 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 13 Dec 2023 11:49:41 +0900 Subject: [PATCH 146/325] Editor Canvas: Fix animation when device type changes (#56970) * Editor Canvas: Fix animation when device type changes * Include margin in deviceStyles --- .../block-editor/src/components/use-resize-canvas/README.md | 2 +- .../block-editor/src/components/use-resize-canvas/index.js | 5 ++++- packages/editor/src/components/editor-canvas/index.js | 5 ++++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/use-resize-canvas/README.md b/packages/block-editor/src/components/use-resize-canvas/README.md index 51e583f8def474..ce8f06adea5d82 100644 --- a/packages/block-editor/src/components/use-resize-canvas/README.md +++ b/packages/block-editor/src/components/use-resize-canvas/README.md @@ -1,6 +1,6 @@ # useResizeCanvas -This React hook generates inline CSS suitable for resizing a container to fit a device's dimensions. It adjusts the CSS according to the current device dimensions. It has no effect on desktop. +This React hook generates inline CSS suitable for resizing a container to fit a device's dimensions. It adjusts the CSS according to the current device dimensions. On-page CSS media queries are also updated to match the width of the device. diff --git a/packages/block-editor/src/components/use-resize-canvas/index.js b/packages/block-editor/src/components/use-resize-canvas/index.js index fab0b7a15e2afd..a843f160056367 100644 --- a/packages/block-editor/src/components/use-resize-canvas/index.js +++ b/packages/block-editor/src/components/use-resize-canvas/index.js @@ -67,7 +67,10 @@ export default function useResizeCanvas( deviceType ) { overflowY: 'auto', }; default: - return null; + return { + marginLeft: marginHorizontal, + marginRight: marginHorizontal, + }; } }; diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index 921f3ce23c0ee4..1f74dfd262ff54 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -304,7 +304,10 @@ function EditorCanvas( height="100%" iframeProps={ { ...iframeProps, - style: { ...iframeProps?.style, ...deviceStyles }, + style: { + ...iframeProps?.style, + ...deviceStyles, + }, } } > { themeSupportsLayout && From 9855f1629143b107abb3a07a8ca689cc11b28fd2 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Wed, 13 Dec 2023 14:11:36 +1100 Subject: [PATCH 147/325] Background image support: Remove double output of styling rules (#56997) --- lib/block-supports/background.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/block-supports/background.php b/lib/block-supports/background.php index b4779b1a150e46..ab2fa84361fc20 100644 --- a/lib/block-supports/background.php +++ b/lib/block-supports/background.php @@ -103,4 +103,7 @@ function gutenberg_render_background_support( $block_content, $block ) { ) ); +if ( function_exists( 'wp_render_background_support' ) ) { + remove_filter( 'render_block', 'wp_render_background_support' ); +} add_filter( 'render_block', 'gutenberg_render_background_support', 10, 2 ); From 4fb8952c4ed1e8a918241bf4b1dabf623d9eb166 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Wed, 13 Dec 2023 08:54:29 +0200 Subject: [PATCH 148/325] Editor: Fix display of edit template blocks notification (#56978) --- .../editor-canvas/edit-template-blocks-notification.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js index 047ca6688ff021..566311e20cadc2 100644 --- a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js +++ b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js @@ -42,7 +42,7 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { useEffect( () => { const handleClick = async ( event ) => { - if ( renderingMode === 'template-only' ) { + if ( renderingMode !== 'template-locked' ) { return; } if ( ! event.target.classList.contains( 'is-root-container' ) ) { @@ -71,7 +71,7 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { }; const handleDblClick = ( event ) => { - if ( renderingMode === 'template-only' ) { + if ( renderingMode !== 'template-locked' ) { return; } if ( ! event.target.classList.contains( 'is-root-container' ) ) { From 77a8b55446c7a92ebdd430f2f09eaa804c32f66b Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 13 Dec 2023 10:15:14 +0100 Subject: [PATCH 149/325] Framework: Bundle the BlockTools component within BlockCanvas (#56996) --- packages/block-editor/README.md | 5 +- .../src/components/block-canvas/index.js | 48 ++++++++++++------- .../components/sidebar-block-editor/index.js | 17 +++---- .../src/components/visual-editor/index.js | 11 ++--- .../components/block-editor/editor-canvas.js | 9 +--- .../block-editor/site-editor-canvas.js | 19 ++------ .../src/components/editor-canvas/index.js | 28 +++++------ platform-docs/docs/basic-concepts/ui.md | 2 +- storybook/stories/playground/box/index.js | 2 - .../stories/playground/fullpage/index.js | 5 +- .../playground/with-undo-redo/index.js | 2 - .../helpers/integration-test-editor.js | 5 +- 12 files changed, 62 insertions(+), 91 deletions(-) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 56ab5f1bd94d93..6c39b5dcc44b46 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -19,7 +19,6 @@ import { useState } from 'react'; import { BlockEditorProvider, BlockList, - BlockTools, WritingFlow, } from '@wordpress/block-editor'; @@ -32,9 +31,7 @@ function MyEditorComponent() { onInput={ ( blocks ) => updateBlocks( blocks ) } onChange={ ( blocks ) => updateBlocks( blocks ) } > - <BlockTools> - <BlockCanvas height="400px" /> - </BlockTools> + <BlockCanvas height="400px" /> </BlockEditorProvider> ); } diff --git a/packages/block-editor/src/components/block-canvas/index.js b/packages/block-editor/src/components/block-canvas/index.js index 97aec461df7d86..7d64897690721c 100644 --- a/packages/block-editor/src/components/block-canvas/index.js +++ b/packages/block-editor/src/components/block-canvas/index.js @@ -2,11 +2,13 @@ * WordPress dependencies */ import { useMergeRefs } from '@wordpress/compose'; +import { useRef } from '@wordpress/element'; /** * Internal dependencies */ import BlockList from '../block-list'; +import BlockTools from '../block-tools'; import EditorStyles from '../editor-styles'; import Iframe from '../iframe'; import WritingFlow from '../writing-flow'; @@ -23,11 +25,15 @@ export function ExperimentalBlockCanvas( { } ) { const resetTypingRef = useMouseMoveTypingReset(); const clearerRef = useBlockSelectionClearer(); - const contentRef = useMergeRefs( [ contentRefProp, clearerRef ] ); + const localRef = useRef(); + const contentRef = useMergeRefs( [ contentRefProp, clearerRef, localRef ] ); if ( ! shouldIframe ) { return ( - <> + <BlockTools + __unstableContentRef={ localRef } + style={ { height, display: 'flex' } } + > <EditorStyles styles={ styles } scope=".editor-styles-wrapper" @@ -36,29 +42,37 @@ export function ExperimentalBlockCanvas( { ref={ contentRef } className="editor-styles-wrapper" tabIndex={ -1 } - style={ { height } } + style={ { + height: '100%', + width: '100%', + } } > { children } </WritingFlow> - </> + </BlockTools> ); } return ( - <Iframe - { ...iframeProps } - ref={ resetTypingRef } - contentRef={ contentRef } - style={ { - width: '100%', - height, - ...iframeProps?.style, - } } - name="editor-canvas" + <BlockTools + __unstableContentRef={ localRef } + style={ { height, display: 'flex' } } > - <EditorStyles styles={ styles } /> - { children } - </Iframe> + <Iframe + { ...iframeProps } + ref={ resetTypingRef } + contentRef={ contentRef } + style={ { + width: '100%', + height: '100%', + ...iframeProps?.style, + } } + name="editor-canvas" + > + <EditorStyles styles={ styles } /> + { children } + </Iframe> + </BlockTools> ); } diff --git a/packages/customize-widgets/src/components/sidebar-block-editor/index.js b/packages/customize-widgets/src/components/sidebar-block-editor/index.js index c2e10bca16ec0b..80deb12dfcf74d 100644 --- a/packages/customize-widgets/src/components/sidebar-block-editor/index.js +++ b/packages/customize-widgets/src/components/sidebar-block-editor/index.js @@ -8,7 +8,6 @@ import { useMemo, createPortal } from '@wordpress/element'; import { BlockList, BlockToolbar, - BlockTools, BlockInspector, privateApis as blockEditorPrivateApis, __unstableBlockSettingsMenuFirstItem, @@ -120,15 +119,13 @@ export default function SidebarBlockEditor( { { ( isFixedToolbarActive || ! isMediumViewport ) && ( <BlockToolbar hideDragHandle /> ) } - <BlockTools> - <BlockCanvas - shouldIframe={ false } - styles={ settings.defaultEditorStyles } - height="100%" - > - <BlockList renderAppender={ BlockAppender } /> - </BlockCanvas> - </BlockTools> + <BlockCanvas + shouldIframe={ false } + styles={ settings.defaultEditorStyles } + height="100%" + > + <BlockList renderAppender={ BlockAppender } /> + </BlockCanvas> { createPortal( // This is a temporary hack to prevent button component inside <BlockInspector> diff --git a/packages/edit-post/src/components/visual-editor/index.js b/packages/edit-post/src/components/visual-editor/index.js index b929e03bc453a4..fd9b4a6ff8bb6c 100644 --- a/packages/edit-post/src/components/visual-editor/index.js +++ b/packages/edit-post/src/components/visual-editor/index.js @@ -10,8 +10,7 @@ import { store as editorStore, privateApis as editorPrivateApis, } from '@wordpress/editor'; -import { BlockTools } from '@wordpress/block-editor'; -import { useRef, useMemo } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { store as blocksStore } from '@wordpress/blocks'; @@ -59,8 +58,6 @@ export default function VisualEditor( { styles } ) { paddingBottom = '40vh'; } - const ref = useRef(); - styles = useMemo( () => [ ...styles, @@ -80,21 +77,19 @@ export default function VisualEditor( { styles } ) { renderingMode === 'template-only'; return ( - <BlockTools - __unstableContentRef={ ref } + <div className={ classnames( 'edit-post-visual-editor', { 'is-template-mode': renderingMode === 'template-only', 'has-inline-canvas': ! isToBeIframed, } ) } > <EditorCanvas - ref={ ref } disableIframe={ ! isToBeIframed } styles={ styles } // We should auto-focus the canvas (title) on load. // eslint-disable-next-line jsx-a11y/no-autofocus autoFocus={ ! isWelcomeGuideVisible } /> - </BlockTools> + </div> ); } diff --git a/packages/edit-site/src/components/block-editor/editor-canvas.js b/packages/edit-site/src/components/block-editor/editor-canvas.js index d7dbf6fb07a7ab..01bc4cdfa2ddfc 100644 --- a/packages/edit-site/src/components/block-editor/editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/editor-canvas.js @@ -25,13 +25,7 @@ import { const { EditorCanvas: EditorCanvasRoot } = unlock( editorPrivateApis ); -function EditorCanvas( { - enableResizing, - settings, - children, - contentRef, - ...props -} ) { +function EditorCanvas( { enableResizing, settings, children, ...props } ) { const { hasBlocks, isFocusMode, templateType, canvasMode, isZoomOutMode } = useSelect( ( select ) => { const { getBlockCount, __unstableGetEditorMode } = @@ -107,7 +101,6 @@ function EditorCanvas( { return ( <EditorCanvasRoot - ref={ contentRef } className={ classnames( 'edit-site-editor-canvas__block-list', { 'is-navigation-block': isTemplateTypeNavigation, } ) } diff --git a/packages/edit-site/src/components/block-editor/site-editor-canvas.js b/packages/edit-site/src/components/block-editor/site-editor-canvas.js index 944dbab3f96f08..3bba8cc26d01f3 100644 --- a/packages/edit-site/src/components/block-editor/site-editor-canvas.js +++ b/packages/edit-site/src/components/block-editor/site-editor-canvas.js @@ -5,9 +5,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { useRef } from '@wordpress/element'; -import { BlockTools, store as blockEditorStore } from '@wordpress/block-editor'; +import { useSelect } from '@wordpress/data'; import { useViewportMatch, useResizeObserver } from '@wordpress/compose'; /** @@ -26,8 +24,6 @@ import { import { unlock } from '../../lock-unlock'; export default function SiteEditorCanvas() { - const { clearSelectedBlock } = useDispatch( blockEditorStore ); - const { templateType, isFocusMode, isViewMode } = useSelect( ( select ) => { const { getEditedPostType, getCanvasMode } = unlock( select( editSiteStore ) @@ -53,7 +49,6 @@ export default function SiteEditorCanvas() { // Disable resizing in mobile viewport. ! isMobileViewport; - const contentRef = useRef(); const isTemplateTypeNavigation = templateType === NAVIGATION_POST_TYPE; const isNavigationFocusMode = isTemplateTypeNavigation && isFocusMode; const forceFullHeight = isNavigationFocusMode; @@ -66,18 +61,11 @@ export default function SiteEditorCanvas() { { editorCanvasView } </div> ) : ( - <BlockTools + <div className={ classnames( 'edit-site-visual-editor', { 'is-focus-mode': isFocusMode || !! editorCanvasView, 'is-view-mode': isViewMode, } ) } - __unstableContentRef={ contentRef } - onClick={ ( event ) => { - // Clear selected block when clicking on the gray background. - if ( event.target === event.currentTarget ) { - clearSelectedBlock(); - } - } } > <BackButton /> <ResizableEditor @@ -91,12 +79,11 @@ export default function SiteEditorCanvas() { <EditorCanvas enableResizing={ enableResizing } settings={ settings } - contentRef={ contentRef } > { resizeObserver } </EditorCanvas> </ResizableEditor> - </BlockTools> + </div> ) } </EditorCanvasContainer.Slot> diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index 1f74dfd262ff54..cd87db0d4bf5e3 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -16,7 +16,7 @@ import { privateApis as blockEditorPrivateApis, __experimentalUseResizeCanvas as useResizeCanvas, } from '@wordpress/block-editor'; -import { useEffect, useRef, useMemo, forwardRef } from '@wordpress/element'; +import { useEffect, useRef, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { parse } from '@wordpress/blocks'; import { store as coreStore } from '@wordpress/core-data'; @@ -72,19 +72,16 @@ function checkForPostContentAtRootLevel( blocks ) { return false; } -function EditorCanvas( - { - // Ideally as we unify post and site editors, we won't need these props. - autoFocus, - className, - renderAppender, - styles, - disableIframe = false, - iframeProps, - children, - }, - ref -) { +function EditorCanvas( { + // Ideally as we unify post and site editors, we won't need these props. + autoFocus, + className, + renderAppender, + styles, + disableIframe = false, + iframeProps, + children, +} ) { const { renderingMode, postContentAttributes, @@ -288,7 +285,6 @@ function EditorCanvas( const typewriterRef = useTypewriter(); const contentRef = useMergeRefs( [ - ref, localRef, renderingMode === 'post-only' ? typewriterRef : undefined, ].filter( ( r ) => !! r ) @@ -382,4 +378,4 @@ function EditorCanvas( ); } -export default forwardRef( EditorCanvas ); +export default EditorCanvas; diff --git a/platform-docs/docs/basic-concepts/ui.md b/platform-docs/docs/basic-concepts/ui.md index 8b6e706683d085..0dccef3c239b03 100644 --- a/platform-docs/docs/basic-concepts/ui.md +++ b/platform-docs/docs/basic-concepts/ui.md @@ -17,7 +17,7 @@ The Gutenberg platform allows you to render these pieces separately and lay them ## The Block Toolbar -Wrapping your `BlockCanvas` component within the `BlockTools` wrapper allows the editor to render a block toolbar adjacent to the selected block. +The block toolbar is rendered automatically next to the selected block by default. But if you set the flag `hasFixedToolbar` to true in your `BlockEditorProvider` settings, you will be able to use the `BlockToolbar` component to render the block toolbar in your place of choice. ## The Block Inspector diff --git a/storybook/stories/playground/box/index.js b/storybook/stories/playground/box/index.js index 4cb7047b73ec20..3fb3c3b5862c47 100644 --- a/storybook/stories/playground/box/index.js +++ b/storybook/stories/playground/box/index.js @@ -7,7 +7,6 @@ import { BlockEditorProvider, BlockCanvas, BlockToolbar, - BlockTools, } from '@wordpress/block-editor'; /** @@ -38,7 +37,6 @@ export default function EditorBox() { } } > <BlockToolbar hideDragHandle /> - <BlockTools /> <BlockCanvas height="100%" styles={ editorStyles } /> </BlockEditorProvider> </div> diff --git a/storybook/stories/playground/fullpage/index.js b/storybook/stories/playground/fullpage/index.js index 961c15f71f31d0..8b8c037ceb72a3 100644 --- a/storybook/stories/playground/fullpage/index.js +++ b/storybook/stories/playground/fullpage/index.js @@ -5,7 +5,6 @@ import { useEffect, useState } from '@wordpress/element'; import { BlockCanvas, BlockEditorProvider, - BlockTools, BlockInspector, } from '@wordpress/block-editor'; import { registerCoreBlocks } from '@wordpress/block-library'; @@ -46,9 +45,9 @@ export default function EditorFullPage() { <div className="playground__sidebar"> <BlockInspector /> </div> - <BlockTools className="playground__content"> + <div className="playground__content"> <BlockCanvas height="100%" styles={ editorStyles } /> - </BlockTools> + </div> </BlockEditorProvider> </div> ); diff --git a/storybook/stories/playground/with-undo-redo/index.js b/storybook/stories/playground/with-undo-redo/index.js index 537ea16aade99b..8bef2d184f8c59 100644 --- a/storybook/stories/playground/with-undo-redo/index.js +++ b/storybook/stories/playground/with-undo-redo/index.js @@ -8,7 +8,6 @@ import { BlockEditorProvider, BlockCanvas, BlockToolbar, - BlockTools, } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; import { undo as undoIcon, redo as redoIcon } from '@wordpress/icons'; @@ -60,7 +59,6 @@ export default function EditorWithUndoRedo() { label="Redo" /> <BlockToolbar hideDragHandle /> - <BlockTools /> </div> <BlockCanvas height="100%" styles={ editorStyles } /> </BlockEditorProvider> diff --git a/test/integration/helpers/integration-test-editor.js b/test/integration/helpers/integration-test-editor.js index dc83c1bfbe6bd2..1317dec7b9226d 100644 --- a/test/integration/helpers/integration-test-editor.js +++ b/test/integration/helpers/integration-test-editor.js @@ -10,7 +10,6 @@ import userEvent from '@testing-library/user-event'; import { useState, useEffect } from '@wordpress/element'; import { BlockEditorProvider, - BlockTools, BlockInspector, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; @@ -76,9 +75,7 @@ export function Editor( { testBlocks, settings = {} } ) { settings={ settings } > <BlockInspector /> - <BlockTools> - <BlockCanvas height="100%" shouldIframe={ false } /> - </BlockTools> + <BlockCanvas height="100%" shouldIframe={ false } /> </BlockEditorProvider> ); } From ef65c8b9fceadec403a4cd1b473cc2b45d356c52 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Wed, 13 Dec 2023 11:34:10 +0200 Subject: [PATCH 150/325] Performance: Improve opening inserter in post editor (#57006) * Performance: Improve opening inserter in post editor * make selector private --- .../src/components/inserter/menu.js | 10 +-- .../src/store/private-selectors.js | 45 +++++++++++ packages/block-editor/src/store/selectors.js | 74 ++----------------- packages/block-editor/src/store/utils.js | 74 +++++++++++++++++++ 4 files changed, 129 insertions(+), 74 deletions(-) create mode 100644 packages/block-editor/src/store/utils.js diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index a6d752848538e7..4f028eb69c6662 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -22,6 +22,7 @@ import { useDebouncedInput } from '@wordpress/compose'; /** * Internal dependencies */ +import { unlock } from '../../lock-unlock'; import Tips from './tips'; import InserterPreviewPanel from './preview-panel'; import BlockTypesTab from './block-types-tab'; @@ -68,12 +69,11 @@ function InserterMenu( } ); const { showPatterns, inserterItems } = useSelect( ( select ) => { - const { __experimentalGetAllowedPatterns, getInserterItems } = - select( blockEditorStore ); + const { hasAllowedPatterns, getInserterItems } = unlock( + select( blockEditorStore ) + ); return { - showPatterns: !! __experimentalGetAllowedPatterns( - destinationRootClientId - ).length, + showPatterns: hasAllowedPatterns( destinationRootClientId ), inserterItems: getInserterItems( destinationRootClientId ), }; }, diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index c4220e6e7e516c..98a75122f47245 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -10,7 +10,12 @@ import { getBlockOrder, getBlockParents, getBlockEditingMode, + getSettings, + __experimentalGetParsedPattern, + canInsertBlockType, + __experimentalGetAllowedPatterns, } from './selectors'; +import { getUserPatterns, checkAllowListRecursive } from './utils'; /** * Returns true if the block interface is hidden, or false otherwise. @@ -236,3 +241,43 @@ export const getInserterMediaCategories = createSelector( state.registeredInserterMediaCategories, ] ); + +/** + * Returns whether there is at least one allowed pattern for inner blocks children. + * This is useful for deferring the parsing of all patterns until needed. + * + * @param {Object} state Editor state. + * @param {string} [rootClientId=null] Target root client ID. + * + * @return {boolean} If there is at least one allowed pattern. + */ +export const hasAllowedPatterns = createSelector( + ( state, rootClientId = null ) => { + const patterns = state.settings.__experimentalBlockPatterns; + const userPatterns = getUserPatterns( state ); + const { allowedBlockTypes } = getSettings( state ); + return [ ...userPatterns, ...patterns ].some( + ( { name, inserter = true } ) => { + if ( ! inserter ) { + return false; + } + const { blocks } = __experimentalGetParsedPattern( + state, + name + ); + return ( + checkAllowListRecursive( blocks, allowedBlockTypes ) && + blocks.every( ( { name: blockName } ) => + canInsertBlockType( state, blockName, rootClientId ) + ) + ); + } + ); + }, + ( state, rootClientId ) => [ + ...__experimentalGetAllowedPatterns.getDependants( + state, + rootClientId + ), + ] +); diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index c0441cd3b3755e..b6d455333c7a52 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -26,8 +26,12 @@ import { createRegistrySelector } from '@wordpress/data'; /** * Internal dependencies */ +import { + getUserPatterns, + checkAllowListRecursive, + checkAllowList, +} from './utils'; import { orderBy } from '../utils/sorting'; -import { PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; /** * A block selection object. @@ -1481,22 +1485,6 @@ export function getTemplateLock( state, rootClientId ) { return getBlockListSettings( state, rootClientId )?.templateLock ?? false; } -const checkAllowList = ( list, item, defaultResult = null ) => { - if ( typeof list === 'boolean' ) { - return list; - } - if ( Array.isArray( list ) ) { - // TODO: when there is a canonical way to detect that we are editing a post - // the following check should be changed to something like: - // if ( list.includes( 'core/post-content' ) && getEditorMode() === 'post-content' && item === null ) - if ( list.includes( 'core/post-content' ) && item === null ) { - return true; - } - return list.includes( item ); - } - return defaultResult; -}; - /** * Determines if the given block type is allowed to be inserted into the block list. * This function is not exported and not memoized because using a memoized selector @@ -2249,58 +2237,6 @@ export const __experimentalGetDirectInsertBlock = createSelector( ] ); -const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { - if ( typeof allowedBlockTypes === 'boolean' ) { - return allowedBlockTypes; - } - - const blocksQueue = [ ...blocks ]; - while ( blocksQueue.length > 0 ) { - const block = blocksQueue.shift(); - - const isAllowed = checkAllowList( - allowedBlockTypes, - block.name || block.blockName, - true - ); - if ( ! isAllowed ) { - return false; - } - - block.innerBlocks?.forEach( ( innerBlock ) => { - blocksQueue.push( innerBlock ); - } ); - } - - return true; -}; - -function getUserPatterns( state ) { - const userPatterns = - state?.settings?.__experimentalReusableBlocks ?? EMPTY_ARRAY; - const userPatternCategories = - state?.settings?.__experimentalUserPatternCategories ?? []; - const categories = new Map(); - userPatternCategories.forEach( ( userCategory ) => - categories.set( userCategory.id, userCategory ) - ); - return userPatterns.map( ( userPattern ) => { - return { - name: `core/block/${ userPattern.id }`, - id: userPattern.id, - type: PATTERN_TYPES.user, - title: userPattern.title.raw, - categories: userPattern.wp_pattern_category.map( ( catId ) => - categories && categories.get( catId ) - ? categories.get( catId ).slug - : catId - ), - content: userPattern.content.raw, - syncStatus: userPattern.wp_pattern_sync_status, - }; - } ); -} - export const __experimentalUserPatternCategories = createSelector( ( state ) => { return state?.settings?.__experimentalUserPatternCategories; diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js new file mode 100644 index 00000000000000..0103b5192154c4 --- /dev/null +++ b/packages/block-editor/src/store/utils.js @@ -0,0 +1,74 @@ +/** + * Internal dependencies + */ +import { PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; + +const EMPTY_ARRAY = []; + +export function getUserPatterns( state ) { + const userPatterns = + state?.settings?.__experimentalReusableBlocks ?? EMPTY_ARRAY; + const userPatternCategories = + state?.settings?.__experimentalUserPatternCategories ?? []; + const categories = new Map(); + userPatternCategories.forEach( ( userCategory ) => + categories.set( userCategory.id, userCategory ) + ); + return userPatterns.map( ( userPattern ) => { + return { + name: `core/block/${ userPattern.id }`, + id: userPattern.id, + type: PATTERN_TYPES.user, + title: userPattern.title.raw, + categories: userPattern.wp_pattern_category.map( ( catId ) => + categories && categories.get( catId ) + ? categories.get( catId ).slug + : catId + ), + content: userPattern.content.raw, + syncStatus: userPattern.wp_pattern_sync_status, + }; + } ); +} + +export const checkAllowList = ( list, item, defaultResult = null ) => { + if ( typeof list === 'boolean' ) { + return list; + } + if ( Array.isArray( list ) ) { + // TODO: when there is a canonical way to detect that we are editing a post + // the following check should be changed to something like: + // if ( list.includes( 'core/post-content' ) && getEditorMode() === 'post-content' && item === null ) + if ( list.includes( 'core/post-content' ) && item === null ) { + return true; + } + return list.includes( item ); + } + return defaultResult; +}; + +export const checkAllowListRecursive = ( blocks, allowedBlockTypes ) => { + if ( typeof allowedBlockTypes === 'boolean' ) { + return allowedBlockTypes; + } + + const blocksQueue = [ ...blocks ]; + while ( blocksQueue.length > 0 ) { + const block = blocksQueue.shift(); + + const isAllowed = checkAllowList( + allowedBlockTypes, + block.name || block.blockName, + true + ); + if ( ! isAllowed ) { + return false; + } + + block.innerBlocks?.forEach( ( innerBlock ) => { + blocksQueue.push( innerBlock ); + } ); + } + + return true; +}; From d8ae7b3c6da5e79041be12e3cd07744673dd9283 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 13 Dec 2023 09:50:43 +0000 Subject: [PATCH 151/325] Bump plugin version to 17.3.0-rc.1 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index f39a0cb519c8ae..20c51fdb6ead2a 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.2.1 + * Version: 17.3.0-rc.1 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 6d9382ec9fb2f2..e0c9500416a962 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.2.1", + "version": "17.3.0-rc.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.2.1", + "version": "17.3.0-rc.1", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index 580c100354a966..cab3288450cd75 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.2.1", + "version": "17.3.0-rc.1", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From ec9ecf9dbc788b345b89e6cdbde6926dbd5f2bb4 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 13 Dec 2023 10:09:00 +0000 Subject: [PATCH 152/325] Update Changelog for 17.3.0-rc.1 --- changelog.txt | 314 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) diff --git a/changelog.txt b/changelog.txt index 65745b416e4bdb..d0d8e111937e08 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,319 @@ == Changelog == += 17.3.0-rc.1 = + + + +## Changelog + +### Enhancements + +- Components: Replace `TabPanel` with `Tabs` in the editor's `ColorPanel`. ([56878](https://github.com/WordPress/gutenberg/pull/56878)) +- Editor: Move the edit template blocks notification to editor package. ([56901](https://github.com/WordPress/gutenberg/pull/56901)) +- Editor: Unify the preview dropdown between post and site editors. ([56921](https://github.com/WordPress/gutenberg/pull/56921)) +- Editor: Use the same PostTemplatePanel between post and site editors. ([56817](https://github.com/WordPress/gutenberg/pull/56817)) +- Tabs: Replace `id` with new `tabId` prop. ([56883](https://github.com/WordPress/gutenberg/pull/56883)) +- Update main toolbar buttons to all be compact. ([56635](https://github.com/WordPress/gutenberg/pull/56635), [56729](https://github.com/WordPress/gutenberg/pull/56729)) +- Update preferences organization. ([56481](https://github.com/WordPress/gutenberg/pull/56481)) + +#### Components +- FocalPointPicker with __next40pxDefaultSize. ([56021](https://github.com/WordPress/gutenberg/pull/56021)) +- Font Library: Improve usability of font variant selection. ([56158](https://github.com/WordPress/gutenberg/pull/56158)) +- Tabs: Sync browser focus to selected tab in controlled mode. ([56658](https://github.com/WordPress/gutenberg/pull/56658)) +- Use consistent styling for duotone panels. ([56801](https://github.com/WordPress/gutenberg/pull/56801)) +- `BorderControl`: Fix button styles. ([56730](https://github.com/WordPress/gutenberg/pull/56730)) +- `DimensionControl`: Add __next40pxDefaultSize prop. ([56805](https://github.com/WordPress/gutenberg/pull/56805)) +- `FontSizePicker`: Add opt-in prop for 40px default size. ([56804](https://github.com/WordPress/gutenberg/pull/56804)) +- `QueryControls`: Add opt-in prop for 40px default size. ([56576](https://github.com/WordPress/gutenberg/pull/56576)) + +#### Block Library +- Control dimensions (margin and padding) of the list-item block. ([55874](https://github.com/WordPress/gutenberg/pull/55874)) +- Consistent default typography controls across blocks. ([55208](https://github.com/WordPress/gutenberg/pull/55208)) +- Social Icons: Add Gravatar service. ([56544](https://github.com/WordPress/gutenberg/pull/56544)) +- Tweak table block placeholder with __next40pxDefaultSize props. ([56935](https://github.com/WordPress/gutenberg/pull/56935)) + +#### Site Editor +- Merge the post only mode and the post editor. ([56671](https://github.com/WordPress/gutenberg/pull/56671)) +- Site Editor Sidebar: Add "Areas" details panel to all templates and update icon. ([55677](https://github.com/WordPress/gutenberg/pull/55677)) + +#### Block Editor +- Allow dragging between adjacent container blocks based on a threshold. ([56466](https://github.com/WordPress/gutenberg/pull/56466)) +- Components: Replace `TabPanel` with `Tabs` in the editor's `ColorGradientControl`. ([56351](https://github.com/WordPress/gutenberg/pull/56351)) + +#### Data Views +- Update data view layout. ([56786](https://github.com/WordPress/gutenberg/pull/56786)) + +#### Layout +- Match the front end layout classname in the editor. ([56774](https://github.com/WordPress/gutenberg/pull/56774)) + +#### Global Styles +- Global style revisions: Show change summary on selected item. ([56577](https://github.com/WordPress/gutenberg/pull/56577)) + +#### Icons +- Another round of HiDPI icon tweaks. ([56532](https://github.com/WordPress/gutenberg/pull/56532)) + +#### Media +- Update external images panel in post publish sidebar. ([55524](https://github.com/WordPress/gutenberg/pull/55524)) + +#### Post Editor +- Implement `Tabs` in editor settings. ([55360](https://github.com/WordPress/gutenberg/pull/55360)) + + +### Bug Fixes + +- Create-block-interactive-template: Add all files to the generated plugin zip. ([56943](https://github.com/WordPress/gutenberg/pull/56943)) +- Create-block-interactive-template: Prevent crash when Gutenberg plugin is not installed. ([56941](https://github.com/WordPress/gutenberg/pull/56941)) +- Fix end-to-end test: Update how we find the template title to match markup changes. ([56992](https://github.com/WordPress/gutenberg/pull/56992)) +- Fix: Fatal php error if a template was created by an author that was deleted. ([56990](https://github.com/WordPress/gutenberg/pull/56990)) +- Fix: PHP 8.1 deprecated warning strpos(). ([56171](https://github.com/WordPress/gutenberg/pull/56171)) +- Fix: Use span on template list titles. ([56955](https://github.com/WordPress/gutenberg/pull/56955)) +- Font Library: Add font family and font face preview keys to schema. ([56793](https://github.com/WordPress/gutenberg/pull/56793)) +- Remove unnecessary CSS for shrinking central header area. ([56220](https://github.com/WordPress/gutenberg/pull/56220)) +- Revert format types hook refactor. ([56859](https://github.com/WordPress/gutenberg/pull/56859)) +- Show template center UI when no block is selected. ([56217](https://github.com/WordPress/gutenberg/pull/56217)) +- setImmutably: Don't clone all objects. ([56612](https://github.com/WordPress/gutenberg/pull/56612)) + +#### Block Library +- Fix error when using a navigation block that returns an empty fallback result. ([56629](https://github.com/WordPress/gutenberg/pull/56629)) +- Fixture Tests: Correctly generate fixture files for form-related blocks. ([56719](https://github.com/WordPress/gutenberg/pull/56719)) +- Image: Fix resetting behaviour for alt image text. ([56809](https://github.com/WordPress/gutenberg/pull/56809)) +- Social Links Block: Prevent Theme Styles Distorting Size. ([56301](https://github.com/WordPress/gutenberg/pull/56301)) +- Update image block save to only save align none class. ([56449](https://github.com/WordPress/gutenberg/pull/56449)) + +#### Components +- DropdownMenuV2Ariakit: Prevent prefix collapsing if all radios or checkboxes are unselected. ([56720](https://github.com/WordPress/gutenberg/pull/56720)) +- FormToggle: Do not use "/" math operator. ([56672](https://github.com/WordPress/gutenberg/pull/56672)) +- PaletteEdit: Temporary custom gradient not saving. ([56896](https://github.com/WordPress/gutenberg/pull/56896)) +- `ToggleGroupControl`: React correctly to external controlled updates. ([56678](https://github.com/WordPress/gutenberg/pull/56678)) + +#### Block Editor +- Apply __next40pxDefaultSize to TextControl and Button component in renaming UIs. ([56933](https://github.com/WordPress/gutenberg/pull/56933)) +- Pattern inserter: Fix Broken preview layout. ([56814](https://github.com/WordPress/gutenberg/pull/56814)) +- Patterns: Keep synced pattern when added via drag and drop. ([56924](https://github.com/WordPress/gutenberg/pull/56924)) + +#### Design Tools +- Background image support: Fix duplicate output of styling rules. ([56997](https://github.com/WordPress/gutenberg/pull/56997)) +- Fix sticky position in classic themes with appearance tools support. ([56743](https://github.com/WordPress/gutenberg/pull/56743)) + +#### Post Editor +- Editor Canvas: Fix animation when device type changes. ([56970](https://github.com/WordPress/gutenberg/pull/56970)) +- Editor: Fix display of edit template blocks notification. ([56978](https://github.com/WordPress/gutenberg/pull/56978)) + +#### Site Editor +- Fix active edited post. ([56863](https://github.com/WordPress/gutenberg/pull/56863)) +- Show back button when editing navigation and template area in-place with no URL params. ([56741](https://github.com/WordPress/gutenberg/pull/56741)) + +#### Typography +- Fix order of typography sizes and families. ([56659](https://github.com/WordPress/gutenberg/pull/56659)) +- Font Library: Fix font uninstallation. ([56762](https://github.com/WordPress/gutenberg/pull/56762)) + +#### Navigation in Site View +- Navigation editor: Fix content mode. ([56856](https://github.com/WordPress/gutenberg/pull/56856)) + +#### Patterns +- Fix top position and height of Pattern Modal Sidebar. ([56787](https://github.com/WordPress/gutenberg/pull/56787)) + +#### Interactivity API +- Start using modules in the interactive create-block template. ([56694](https://github.com/WordPress/gutenberg/pull/56694)) + +#### Layout +- Fix input not showing when switching to "Fixed" width. ([56660](https://github.com/WordPress/gutenberg/pull/56660)) + +#### Data Views +- Align data view icon usage. ([56602](https://github.com/WordPress/gutenberg/pull/56602)) + +#### Block Styles +- Consolidate and resolve display issues between InserterPreviewPanel and BlockStylesPreviewPanel. ([56011](https://github.com/WordPress/gutenberg/pull/56011)) + +#### Inspector Controls +- Decode some characters if used in taxonomy name so it's displayed correctly in Query Loop filters. ([50376](https://github.com/WordPress/gutenberg/pull/50376)) + + +### Accessibility + +#### Data Views +- Add scroll padding to dataviews container. ([56946](https://github.com/WordPress/gutenberg/pull/56946)) +- Adding `aria-sort` to table view headers. ([56860](https://github.com/WordPress/gutenberg/pull/56860)) +- Fix: Use span instead of heading for the template titles. ([56785](https://github.com/WordPress/gutenberg/pull/56785)) + +#### Post Editor +- Avoid to show unnecessary Tooltip for the Post Schedule button. ([56759](https://github.com/WordPress/gutenberg/pull/56759)) + +#### Block Editor +- Increase right padding of URL field to take the Submit button into account. ([56685](https://github.com/WordPress/gutenberg/pull/56685)) + +#### Site Editor +- Shorter screen reader announcement after changing pages. ([56339](https://github.com/WordPress/gutenberg/pull/56339)) + +#### Components +- Use tooltip for the Timezone only when necessary. ([56214](https://github.com/WordPress/gutenberg/pull/56214)) + + +### Performance + +- Block editor: Make all BlockEdit hooks pure. ([56813](https://github.com/WordPress/gutenberg/pull/56813)) +- Block editor: Remove 4 useSelect in favour of context. ([56915](https://github.com/WordPress/gutenberg/pull/56915)) +- Block editor: hooks: Avoid BlockEdit filter for content locking UI. ([56957](https://github.com/WordPress/gutenberg/pull/56957)) +- Block editor: hooks: Share block settings. ([56852](https://github.com/WordPress/gutenberg/pull/56852)) +- Keycodes: Avoid regex for capital case. ([56822](https://github.com/WordPress/gutenberg/pull/56822)) +- Measure typing without inspector. ([56753](https://github.com/WordPress/gutenberg/pull/56753)) +- Media upload component: Lazy mount. ([56958](https://github.com/WordPress/gutenberg/pull/56958)) +- Paragraph: Store subscription for selected block only. ([56967](https://github.com/WordPress/gutenberg/pull/56967)) +- Perf: Reopen inspector for remaining tests. ([56780](https://github.com/WordPress/gutenberg/pull/56780)) +- useBlockProps: Combine store subscriptions. ([56847](https://github.com/WordPress/gutenberg/pull/56847)) + +#### Block Editor +- Improve opening inserter in post editor. ([57006](https://github.com/WordPress/gutenberg/pull/57006)) +- hooks: Subscribe only to relevant attributes. ([56783](https://github.com/WordPress/gutenberg/pull/56783)) + +#### Site Editor +- Fix typing performance by not rendering sidebar. ([56927](https://github.com/WordPress/gutenberg/pull/56927)) + +#### Components +- ToolsPanel: Fix deregister/register on type. ([56770](https://github.com/WordPress/gutenberg/pull/56770)) + +#### Modules API +- Load the import map polyfill only when there is an import map. ([56699](https://github.com/WordPress/gutenberg/pull/56699)) + +#### Post Editor +- Editor: Avoid double parsing content in 'getSuggestedPostFormat' selelector. ([56679](https://github.com/WordPress/gutenberg/pull/56679)) + + +### Experiments + +#### Data Views +- DataViews: Add story. ([56761](https://github.com/WordPress/gutenberg/pull/56761)) +- DataViews: Add support for `NOT IN` operator in filter. ([56479](https://github.com/WordPress/gutenberg/pull/56479)) +- DataViews: Centralize the view definition and rename `list` to `table`. ([56693](https://github.com/WordPress/gutenberg/pull/56693)) +- DataViews: Do not export strings constants. ([56754](https://github.com/WordPress/gutenberg/pull/56754)) +- DataViews: Export the view components as defaults. ([56677](https://github.com/WordPress/gutenberg/pull/56677)) +- DataViews: Fix dropdown menu actions with modal. ([56760](https://github.com/WordPress/gutenberg/pull/56760)) +- DataViews: Hide pagination if we have only one page. ([56948](https://github.com/WordPress/gutenberg/pull/56948)) +- DataViews: Implement `NOT IN` operator for author filter in templates. ([56777](https://github.com/WordPress/gutenberg/pull/56777)) +- DataViews: Iterate on list view. ([56746](https://github.com/WordPress/gutenberg/pull/56746)) +- DataViews: Make `Actions` styles the same as any other column header. ([56654](https://github.com/WordPress/gutenberg/pull/56654)) +- DataViews: Make `mediaField` not hidable. ([56643](https://github.com/WordPress/gutenberg/pull/56643)) +- DataViews: Rename view components. ([56709](https://github.com/WordPress/gutenberg/pull/56709)) +- DataViews: Render data async conditionally. ([56851](https://github.com/WordPress/gutenberg/pull/56851)) +- DataViews: Set proper role for AddFilter's items. ([56714](https://github.com/WordPress/gutenberg/pull/56714)) +- DataViews: Set proper semantics for dropdown items. ([56676](https://github.com/WordPress/gutenberg/pull/56676)) +- DataViews: Update sorting semantics. ([56717](https://github.com/WordPress/gutenberg/pull/56717)) +- Dataviews: Extract to dedicated bundled package. ([56721](https://github.com/WordPress/gutenberg/pull/56721)) + +#### Block Validation/Deprecation +- Input Field Block: Use `useblockProps` hook in save function. ([56507](https://github.com/WordPress/gutenberg/pull/56507)) + +#### Patterns +- Implement partially synced patterns behind an experimental flag. ([56235](https://github.com/WordPress/gutenberg/pull/56235)) + + +### Documentation + +- Add the nested blocks chapter to the platform documentation. ([56689](https://github.com/WordPress/gutenberg/pull/56689)) +- Components: Update CHANGELOG.md. ([56960](https://github.com/WordPress/gutenberg/pull/56960)) +- Doc: Search Control - add Storybook link. ([56815](https://github.com/WordPress/gutenberg/pull/56815)) +- Doc: Spinner - add Storybook link. ([56818](https://github.com/WordPress/gutenberg/pull/56818)) +- Docs: Add storybook link for spinner component. ([56953](https://github.com/WordPress/gutenberg/pull/56953)) +- Docs: Fix {% end %} tab position to show the text. ([56735](https://github.com/WordPress/gutenberg/pull/56735)) +- Docs: Fundamentals of Block Development - Minor fixes - registration-of-a-block. ([56731](https://github.com/WordPress/gutenberg/pull/56731)) +- Docs: Fundamentals of Block Development - add links. ([56700](https://github.com/WordPress/gutenberg/pull/56700)) +- Docs: Fundamentals of Block Development ---- Small fixes for "Block wrapper". ([56651](https://github.com/WordPress/gutenberg/pull/56651)) +- Link to Dashicons. ([56872](https://github.com/WordPress/gutenberg/pull/56872)) +- Platform Docs: Add trusted by section. ([56749](https://github.com/WordPress/gutenberg/pull/56749)) +- Revert "Doc: Spinner - add Storybook link". ([56913](https://github.com/WordPress/gutenberg/pull/56913)) +- Update Getting Started Guide for Gutenberg 17.2. ([56674](https://github.com/WordPress/gutenberg/pull/56674)) +- Update InnerBlocks defaultblock doc usage. ([56728](https://github.com/WordPress/gutenberg/pull/56728)) +- Update formatting and fix grammar in the Block Editor Handbook readme. ([56798](https://github.com/WordPress/gutenberg/pull/56798)) + + +### Code Quality + +- Block editor: hooks: Avoid getEditWrapperProps. ([56912](https://github.com/WordPress/gutenberg/pull/56912)) +- Block lib: Use RichText.isEmpty where forgotten. ([56726](https://github.com/WordPress/gutenberg/pull/56726)) +- Block library: Reusable caption component util. ([56606](https://github.com/WordPress/gutenberg/pull/56606)) +- Core data revisions: Remove hardcoded supports constant. ([56701](https://github.com/WordPress/gutenberg/pull/56701)) +- Editor: Cleanup default editor mode handling. ([56819](https://github.com/WordPress/gutenberg/pull/56819)) +- Editor: Move the BlockCanvas component within the EditorCanvas component. ([56850](https://github.com/WordPress/gutenberg/pull/56850)) +- Editor: Move the device type state to the editor package. ([56866](https://github.com/WordPress/gutenberg/pull/56866)) +- Editor: Unify device preview styles. ([56904](https://github.com/WordPress/gutenberg/pull/56904)) +- Fix PHP linter failing. ([56905](https://github.com/WordPress/gutenberg/pull/56905)) +- Framework: Bundle the BlockTools component within BlockCanvas. ([56996](https://github.com/WordPress/gutenberg/pull/56996)) +- Move `useDebouncedInput` hook to @wordpress/compose package. ([56744](https://github.com/WordPress/gutenberg/pull/56744)) +- Post Editor: Rely on the editor store for the template mode state. ([56716](https://github.com/WordPress/gutenberg/pull/56716)) +- Refactor <BlockToolbar />. ([56335](https://github.com/WordPress/gutenberg/pull/56335)) +- Remove Block Tools BackCompat. ([56874](https://github.com/WordPress/gutenberg/pull/56874)) +- Site and Post Editor: Unify the DocumentBar component. ([56778](https://github.com/WordPress/gutenberg/pull/56778)) +- getValueFromObjectPath: Remove memize. ([56711](https://github.com/WordPress/gutenberg/pull/56711)) + +#### Block Editor +- Don't render undefined classname in useBlockProps hook. ([56923](https://github.com/WordPress/gutenberg/pull/56923)) +- One hook to rule them all: Preparation for a block supports API. ([56862](https://github.com/WordPress/gutenberg/pull/56862)) +- RichText: Pass value to store. ([43204](https://github.com/WordPress/gutenberg/pull/43204)) +- hooks: Manage BlockListBlock filters in one place. ([56875](https://github.com/WordPress/gutenberg/pull/56875)) + +#### Global Styles +- Command Palette: Use getRevisions instead of deprecated selector. ([56738](https://github.com/WordPress/gutenberg/pull/56738)) +- Global styles revisions: Remove PHP unit tests that are running in Core. ([56492](https://github.com/WordPress/gutenberg/pull/56492)) + +#### Components +- Site editor: Do not use navigator's internal classname. ([56911](https://github.com/WordPress/gutenberg/pull/56911)) + +#### Data Views +- DataViews: Remove TanStack. ([56873](https://github.com/WordPress/gutenberg/pull/56873)) + + +### Tools + +- Env: Migrate to Compose V2. ([51339](https://github.com/WordPress/gutenberg/pull/51339)) +- Scripts: Fix CSS imports not minified. ([56516](https://github.com/WordPress/gutenberg/pull/56516)) +- wp-env: Make env-cwd option work on Windows. ([56265](https://github.com/WordPress/gutenberg/pull/56265)) + +#### Testing +- Migrate 'editor multi entity saving' end-to-end tests to Playwright. ([56670](https://github.com/WordPress/gutenberg/pull/56670)) +- Migrate 'inner-blocks-locking-all-embed' end-to-end tests to Playwright. ([56673](https://github.com/WordPress/gutenberg/pull/56673)) +- Migrate 'site editor export' end-to-end tests to Playwright. ([56675](https://github.com/WordPress/gutenberg/pull/56675)) +- RN: Add watch mode for native tests. ([56788](https://github.com/WordPress/gutenberg/pull/56788)) +- Scripts: Enable skipping Playwright browser installation. ([56594](https://github.com/WordPress/gutenberg/pull/56594)) +- Tabs: Implement `ariakit/test` in unit tests. ([56835](https://github.com/WordPress/gutenberg/pull/56835)) +- `CustomSelectControl`: Add additional unit tests. ([56575](https://github.com/WordPress/gutenberg/pull/56575)) + + +### Various + +- Copy/fix capitalization of WordPress. ([56834](https://github.com/WordPress/gutenberg/pull/56834)) + +#### Site Editor +- Improve text and design of the block removal warnings. ([56869](https://github.com/WordPress/gutenberg/pull/56869)) + +#### Global Styles +- Global styles welcome guide: Add a space between translated strings. ([56839](https://github.com/WordPress/gutenberg/pull/56839)) + +#### Block Library +- Simplify page list edit warning. ([56829](https://github.com/WordPress/gutenberg/pull/56829)) + +#### Patterns +- End pattern page descriptions with a period. ([56828](https://github.com/WordPress/gutenberg/pull/56828)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @benoitchantre: Scripts: Fix CSS imports not minified. ([56516](https://github.com/WordPress/gutenberg/pull/56516)) +- @kmanijak: Decode some characters if used in taxonomy name so it's displayed correctly in Query Loop filters. ([50376](https://github.com/WordPress/gutenberg/pull/50376)) +- @lithrel: Env: Migrate to Compose V2. ([51339](https://github.com/WordPress/gutenberg/pull/51339)) +- @nk-o: Fix: PHP 8.1 deprecated warning strpos(). ([56171](https://github.com/WordPress/gutenberg/pull/56171)) +- @taylorgorman: Link to Dashicons. ([56872](https://github.com/WordPress/gutenberg/pull/56872)) +- @valerogarte: #55702 - Control dimensions (margin and padding) of the list-item block. ([55874](https://github.com/WordPress/gutenberg/pull/55874)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @ajlende @alexstine @andrewhayward @andrewserong @apeatling @atachibana @Aurorum @benoitchantre @bph @brookewp @chad1008 @ciampo @colorful-tones @dcalhoun @derekblank @draganescu @ellatrix @fluiddot @geriux @getdave @jameskoster @jasmussen @jeherve @jeryj @jffng @jonathanbossenger @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @kmanijak @lithrel @luisherranz @Mamaduka @matiasbenedetto @mikachan @miminari @mtias @ndiego @nk-o @ntsekouras @oandregal @ramonjd @richtabor @scruffian @SiobhyB @t-hamano @talldan @taylorgorman @tellthemachines @tyxla @valerogarte @WunderBart @youknowriad + + = 17.2.1 = ## Changelog From 03d1470590a7f1a9efaec1d3c1208ee407c85af8 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 13 Dec 2023 10:19:01 +0000 Subject: [PATCH 153/325] Update changelog files --- packages/a11y/CHANGELOG.md | 2 ++ packages/a11y/package.json | 2 +- packages/annotations/CHANGELOG.md | 2 ++ packages/annotations/package.json | 2 +- packages/api-fetch/CHANGELOG.md | 2 ++ packages/api-fetch/package.json | 2 +- packages/autop/CHANGELOG.md | 2 ++ packages/autop/package.json | 2 +- packages/babel-plugin-import-jsx-pragma/CHANGELOG.md | 2 ++ packages/babel-plugin-import-jsx-pragma/package.json | 2 +- packages/babel-plugin-makepot/CHANGELOG.md | 2 ++ packages/babel-plugin-makepot/package.json | 2 +- packages/babel-preset-default/CHANGELOG.md | 2 ++ packages/babel-preset-default/package.json | 2 +- packages/base-styles/CHANGELOG.md | 2 ++ packages/base-styles/package.json | 2 +- packages/blob/CHANGELOG.md | 2 ++ packages/blob/package.json | 2 +- packages/block-directory/CHANGELOG.md | 2 ++ packages/block-directory/package.json | 2 +- packages/block-editor/CHANGELOG.md | 2 ++ packages/block-editor/package.json | 2 +- packages/block-library/CHANGELOG.md | 2 ++ packages/block-library/package.json | 2 +- packages/block-serialization-default-parser/CHANGELOG.md | 2 ++ packages/block-serialization-default-parser/package.json | 2 +- packages/block-serialization-spec-parser/CHANGELOG.md | 2 ++ packages/block-serialization-spec-parser/package.json | 2 +- packages/blocks/CHANGELOG.md | 2 ++ packages/blocks/package.json | 2 +- packages/browserslist-config/CHANGELOG.md | 2 ++ packages/browserslist-config/package.json | 2 +- packages/commands/CHANGELOG.md | 2 ++ packages/commands/package.json | 2 +- packages/components/CHANGELOG.md | 2 ++ packages/components/package.json | 2 +- packages/compose/CHANGELOG.md | 2 ++ packages/compose/package.json | 2 +- packages/core-commands/CHANGELOG.md | 2 ++ packages/core-commands/package.json | 2 +- packages/core-data/CHANGELOG.md | 2 ++ packages/core-data/package.json | 2 +- packages/create-block-interactive-template/CHANGELOG.md | 2 ++ packages/create-block-interactive-template/package.json | 2 +- packages/create-block-tutorial-template/CHANGELOG.md | 2 ++ packages/create-block-tutorial-template/package.json | 2 +- packages/create-block/CHANGELOG.md | 2 ++ packages/create-block/package.json | 2 +- packages/customize-widgets/CHANGELOG.md | 2 ++ packages/customize-widgets/package.json | 2 +- packages/data-controls/CHANGELOG.md | 2 ++ packages/data-controls/package.json | 2 +- packages/data/CHANGELOG.md | 2 ++ packages/data/package.json | 2 +- packages/dataviews/CHANGELOG.md | 2 ++ packages/dataviews/package.json | 2 +- packages/date/CHANGELOG.md | 2 ++ packages/date/package.json | 2 +- packages/dependency-extraction-webpack-plugin/CHANGELOG.md | 2 ++ packages/dependency-extraction-webpack-plugin/package.json | 2 +- packages/deprecated/CHANGELOG.md | 2 ++ packages/deprecated/package.json | 2 +- packages/docgen/CHANGELOG.md | 2 ++ packages/docgen/package.json | 2 +- packages/dom-ready/CHANGELOG.md | 2 ++ packages/dom-ready/package.json | 2 +- packages/dom/CHANGELOG.md | 2 ++ packages/dom/package.json | 2 +- packages/e2e-test-utils-playwright/CHANGELOG.md | 2 ++ packages/e2e-test-utils-playwright/package.json | 2 +- packages/e2e-test-utils/CHANGELOG.md | 2 ++ packages/e2e-test-utils/package.json | 2 +- packages/e2e-tests/CHANGELOG.md | 2 ++ packages/e2e-tests/package.json | 2 +- packages/edit-post/CHANGELOG.md | 2 ++ packages/edit-post/package.json | 2 +- packages/edit-site/CHANGELOG.md | 2 ++ packages/edit-site/package.json | 2 +- packages/edit-widgets/CHANGELOG.md | 2 ++ packages/edit-widgets/package.json | 2 +- packages/editor/CHANGELOG.md | 2 ++ packages/editor/package.json | 2 +- packages/element/CHANGELOG.md | 2 ++ packages/element/package.json | 2 +- packages/env/CHANGELOG.md | 2 ++ packages/env/package.json | 2 +- packages/escape-html/CHANGELOG.md | 2 ++ packages/escape-html/package.json | 2 +- packages/eslint-plugin/CHANGELOG.md | 2 ++ packages/eslint-plugin/package.json | 2 +- packages/format-library/CHANGELOG.md | 2 ++ packages/format-library/package.json | 2 +- packages/hooks/CHANGELOG.md | 2 ++ packages/hooks/package.json | 2 +- packages/html-entities/CHANGELOG.md | 2 ++ packages/html-entities/package.json | 2 +- packages/i18n/CHANGELOG.md | 2 ++ packages/i18n/package.json | 2 +- packages/icons/CHANGELOG.md | 2 ++ packages/icons/package.json | 2 +- packages/interactivity/CHANGELOG.md | 2 ++ packages/interactivity/package.json | 2 +- packages/interface/CHANGELOG.md | 2 ++ packages/interface/package.json | 2 +- packages/is-shallow-equal/CHANGELOG.md | 2 ++ packages/is-shallow-equal/package.json | 2 +- packages/jest-console/CHANGELOG.md | 2 ++ packages/jest-console/package.json | 2 +- packages/jest-preset-default/CHANGELOG.md | 2 ++ packages/jest-preset-default/package.json | 2 +- packages/jest-puppeteer-axe/CHANGELOG.md | 2 ++ packages/jest-puppeteer-axe/package.json | 2 +- packages/keyboard-shortcuts/CHANGELOG.md | 2 ++ packages/keyboard-shortcuts/package.json | 2 +- packages/keycodes/CHANGELOG.md | 2 ++ packages/keycodes/package.json | 2 +- packages/lazy-import/CHANGELOG.md | 2 ++ packages/lazy-import/package.json | 2 +- packages/list-reusable-blocks/CHANGELOG.md | 2 ++ packages/list-reusable-blocks/package.json | 2 +- packages/media-utils/CHANGELOG.md | 2 ++ packages/media-utils/package.json | 2 +- packages/notices/CHANGELOG.md | 2 ++ packages/notices/package.json | 2 +- packages/npm-package-json-lint-config/CHANGELOG.md | 2 ++ packages/npm-package-json-lint-config/package.json | 2 +- packages/nux/CHANGELOG.md | 2 ++ packages/nux/package.json | 2 +- packages/patterns/CHANGELOG.md | 2 ++ packages/patterns/package.json | 2 +- packages/plugins/CHANGELOG.md | 2 ++ packages/plugins/package.json | 2 +- packages/postcss-plugins-preset/CHANGELOG.md | 2 ++ packages/postcss-plugins-preset/package.json | 2 +- packages/postcss-themes/CHANGELOG.md | 2 ++ packages/postcss-themes/package.json | 2 +- packages/preferences-persistence/CHANGELOG.md | 2 ++ packages/preferences-persistence/package.json | 2 +- packages/preferences/CHANGELOG.md | 2 ++ packages/preferences/package.json | 2 +- packages/prettier-config/CHANGELOG.md | 2 ++ packages/prettier-config/package.json | 2 +- packages/primitives/CHANGELOG.md | 2 ++ packages/primitives/package.json | 2 +- packages/priority-queue/CHANGELOG.md | 2 ++ packages/priority-queue/package.json | 2 +- packages/private-apis/CHANGELOG.md | 2 ++ packages/private-apis/package.json | 2 +- packages/project-management-automation/CHANGELOG.md | 2 ++ packages/project-management-automation/package.json | 2 +- packages/react-i18n/CHANGELOG.md | 2 ++ packages/react-i18n/package.json | 2 +- packages/readable-js-assets-webpack-plugin/CHANGELOG.md | 2 ++ packages/readable-js-assets-webpack-plugin/package.json | 2 +- packages/redux-routine/CHANGELOG.md | 2 ++ packages/redux-routine/package.json | 2 +- packages/reusable-blocks/CHANGELOG.md | 2 ++ packages/reusable-blocks/package.json | 2 +- packages/rich-text/CHANGELOG.md | 2 ++ packages/rich-text/package.json | 2 +- packages/router/CHANGELOG.md | 2 ++ packages/router/package.json | 2 +- packages/scripts/CHANGELOG.md | 2 ++ packages/scripts/package.json | 2 +- packages/server-side-render/CHANGELOG.md | 2 ++ packages/server-side-render/package.json | 2 +- packages/shortcode/CHANGELOG.md | 2 ++ packages/shortcode/package.json | 2 +- packages/style-engine/CHANGELOG.md | 2 ++ packages/style-engine/package.json | 2 +- packages/stylelint-config/CHANGELOG.md | 2 ++ packages/stylelint-config/package.json | 2 +- packages/sync/CHANGELOG.md | 2 ++ packages/sync/package.json | 2 +- packages/token-list/CHANGELOG.md | 2 ++ packages/token-list/package.json | 2 +- packages/undo-manager/CHANGELOG.md | 2 ++ packages/undo-manager/package.json | 2 +- packages/url/CHANGELOG.md | 2 ++ packages/url/package.json | 2 +- packages/viewport/CHANGELOG.md | 2 ++ packages/viewport/package.json | 2 +- packages/warning/CHANGELOG.md | 2 ++ packages/warning/package.json | 2 +- packages/widgets/CHANGELOG.md | 2 ++ packages/widgets/package.json | 2 +- packages/wordcount/CHANGELOG.md | 2 ++ packages/wordcount/package.json | 2 +- 188 files changed, 282 insertions(+), 94 deletions(-) diff --git a/packages/a11y/CHANGELOG.md b/packages/a11y/CHANGELOG.md index 2e25cf3d19bcb0..b78906a8b23e15 100644 --- a/packages/a11y/CHANGELOG.md +++ b/packages/a11y/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/a11y/package.json b/packages/a11y/package.json index 61362916dd66f4..2cf9d38e1c416d 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/CHANGELOG.md b/packages/annotations/CHANGELOG.md index 525f9b99dbcdb9..98c1d6e0f96219 100644 --- a/packages/annotations/CHANGELOG.md +++ b/packages/annotations/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.48.0 (2023-12-13) + ## 2.47.0 (2023-11-29) ## 2.46.0 (2023-11-16) diff --git a/packages/annotations/package.json b/packages/annotations/package.json index ece531d439d31e..27dd6a9bf8a23b 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.47.0", + "version": "2.48.0-prerelease", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/CHANGELOG.md b/packages/api-fetch/CHANGELOG.md index f098d18f5b204b..cd9a3d8582dd93 100644 --- a/packages/api-fetch/CHANGELOG.md +++ b/packages/api-fetch/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.45.0 (2023-12-13) + ## 6.44.0 (2023-11-29) ## 6.43.0 (2023-11-16) diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index ae71a5ead84805..90cdd359911432 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.44.0", + "version": "6.45.0-prerelease", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/CHANGELOG.md b/packages/autop/CHANGELOG.md index edc06ebf114c5d..301f58946ecdd8 100644 --- a/packages/autop/CHANGELOG.md +++ b/packages/autop/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/autop/package.json b/packages/autop/package.json index 28673b8a203b5e..d3cbd8c0b87792 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md index af37e931573901..bab957e21b3aee 100644 --- a/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md +++ b/packages/babel-plugin-import-jsx-pragma/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.31.0 (2023-12-13) + ## 4.30.0 (2023-11-29) ## 4.29.0 (2023-11-16) diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index c2783bb6b87a21..28fa650be4259b 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.30.0", + "version": "4.31.0-prerelease", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/CHANGELOG.md b/packages/babel-plugin-makepot/CHANGELOG.md index 60d74f5f387f4c..af6099fa128a14 100644 --- a/packages/babel-plugin-makepot/CHANGELOG.md +++ b/packages/babel-plugin-makepot/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.32.0 (2023-12-13) + ## 5.31.0 (2023-11-29) ## 5.30.0 (2023-11-16) diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index e20bd7ca63e7a3..069d71f3dcc8f3 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.31.0", + "version": "5.32.0-prerelease", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/CHANGELOG.md b/packages/babel-preset-default/CHANGELOG.md index b26756aaec9eae..1b4af03b532b01 100644 --- a/packages/babel-preset-default/CHANGELOG.md +++ b/packages/babel-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.32.0 (2023-12-13) + ## 7.31.0 (2023-11-29) ## 7.30.0 (2023-11-16) diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 7cb8f46ec26587..884c1618082e3b 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.31.0", + "version": "7.32.0-prerelease", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/CHANGELOG.md b/packages/base-styles/CHANGELOG.md index 6fba6e0f84f018..3f1e818df2efdd 100644 --- a/packages/base-styles/CHANGELOG.md +++ b/packages/base-styles/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.39.0 (2023-12-13) + ## 4.38.0 (2023-11-29) ## 4.37.0 (2023-11-16) diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index 9035296c46b492..9b611219425ffa 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.38.0", + "version": "4.39.0-prerelease", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/CHANGELOG.md b/packages/blob/CHANGELOG.md index 45eb062c71626b..7dd54454a04885 100644 --- a/packages/blob/CHANGELOG.md +++ b/packages/blob/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/blob/package.json b/packages/blob/package.json index b976ab10159f72..f3bd5731306b45 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/CHANGELOG.md b/packages/block-directory/CHANGELOG.md index b8ff14ec3adea2..2a9b8c670db457 100644 --- a/packages/block-directory/CHANGELOG.md +++ b/packages/block-directory/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index 04bcd1c50d8b33..dda633af694b22 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.24.1", + "version": "4.25.0-prerelease", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md index b53b86140f5a34..64763eb66f7a87 100644 --- a/packages/block-editor/CHANGELOG.md +++ b/packages/block-editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.16.0 (2023-12-13) + ## 12.15.0 (2023-11-29) ## 12.14.0 (2023-11-16) diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index ec173413693822..8cb42b3e100555 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.15.0", + "version": "12.16.0-prerelease", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/CHANGELOG.md b/packages/block-library/CHANGELOG.md index c9c647c16105f3..d5079fbbb4ac2c 100644 --- a/packages/block-library/CHANGELOG.md +++ b/packages/block-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.25.0 (2023-12-13) + ## 8.24.0 (2023-11-29) ## 8.23.0 (2023-11-16) diff --git a/packages/block-library/package.json b/packages/block-library/package.json index bcb7a843b3232d..3fc1ad0881e910 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.24.1", + "version": "8.25.0-prerelease", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-default-parser/CHANGELOG.md b/packages/block-serialization-default-parser/CHANGELOG.md index a44ee8ed86efde..5e8e65ddea45bd 100644 --- a/packages/block-serialization-default-parser/CHANGELOG.md +++ b/packages/block-serialization-default-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index 53dc592315f534..ab434505b0370f 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/CHANGELOG.md b/packages/block-serialization-spec-parser/CHANGELOG.md index f61da25760bb5a..81813855697518 100644 --- a/packages/block-serialization-spec-parser/CHANGELOG.md +++ b/packages/block-serialization-spec-parser/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 1844feeaf7844b..2016f17d10bb87 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/CHANGELOG.md b/packages/blocks/CHANGELOG.md index d9e5e2616efd0c..0cce621f5b6cdb 100644 --- a/packages/blocks/CHANGELOG.md +++ b/packages/blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 12.25.0 (2023-12-13) + ## 12.24.0 (2023-11-29) ## 12.23.0 (2023-11-16) diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 928d9d94740b4f..09d7d3ac8da9bf 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.24.0", + "version": "12.25.0-prerelease", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/browserslist-config/CHANGELOG.md b/packages/browserslist-config/CHANGELOG.md index 4fdb2307059f8c..451716f50edb96 100644 --- a/packages/browserslist-config/CHANGELOG.md +++ b/packages/browserslist-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.31.0 (2023-12-13) + ## 5.30.0 (2023-11-29) ## 5.29.0 (2023-11-16) diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index f0a6895b3d8d92..11b202415b9a2c 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.30.0", + "version": "5.31.0-prerelease", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index e99fa417e0e4c8..f162c1d28e4919 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.19.0 (2023-12-13) + ## 0.18.0 (2023-11-29) ## 0.17.0 (2023-11-16) diff --git a/packages/commands/package.json b/packages/commands/package.json index 98ba9be50d9003..64ec865399d8cd 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.18.0", + "version": "0.19.0-prerelease", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 5e73e4319a1ba0..01a31c58d01c81 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 25.14.0 (2023-12-13) + ### Enhancements - `FormToggle`: fix sass deprecation warning ([#56672](https://github.com/WordPress/gutenberg/pull/56672)). diff --git a/packages/components/package.json b/packages/components/package.json index 25ddcf603fb32a..74a96368913b04 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.13.0", + "version": "25.14.0-prerelease", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/compose/CHANGELOG.md b/packages/compose/CHANGELOG.md index 4218db129273da..d6938f4f26f63f 100644 --- a/packages/compose/CHANGELOG.md +++ b/packages/compose/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.25.0 (2023-12-13) + ## 6.24.0 (2023-11-29) ## 6.23.0 (2023-11-16) diff --git a/packages/compose/package.json b/packages/compose/package.json index eca27f66245657..8648783bebb77a 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "6.24.0", + "version": "6.25.0-prerelease", "description": "WordPress higher-order components (HOCs).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-commands/CHANGELOG.md b/packages/core-commands/CHANGELOG.md index e090ad810fc529..1abb8f289e7d9e 100644 --- a/packages/core-commands/CHANGELOG.md +++ b/packages/core-commands/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.17.0 (2023-12-13) + ## 0.16.0 (2023-11-29) ## 0.15.0 (2023-11-16) diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index 25d90652709643..224b8e3269cf26 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-commands", - "version": "0.16.0", + "version": "0.17.0-prerelease", "description": "WordPress core reusable commands.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/CHANGELOG.md b/packages/core-data/CHANGELOG.md index 12d2f6d3cac7bc..ae8d7543f8fbd5 100644 --- a/packages/core-data/CHANGELOG.md +++ b/packages/core-data/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.25.0 (2023-12-13) + ## 6.24.0 (2023-11-29) ## 6.23.0 (2023-11-16) diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 8998e8419bac7e..26f78d01c3d9b5 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "6.24.0", + "version": "6.25.0-prerelease", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-interactive-template/CHANGELOG.md b/packages/create-block-interactive-template/CHANGELOG.md index 388c9de959e437..47a8aec6c92a31 100644 --- a/packages/create-block-interactive-template/CHANGELOG.md +++ b/packages/create-block-interactive-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.11.0 (2023-12-13) + - Add all files to the generated plugin zip. [#56943](https://github.com/WordPress/gutenberg/pull/56943) - Prevent crash when Gutenberg plugin is not installed. [#56941](https://github.com/WordPress/gutenberg/pull/56941) diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 3bc6b1f646c265..749d6a36db3815 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.10.1", + "version": "1.11.0-prerelease", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-tutorial-template/CHANGELOG.md b/packages/create-block-tutorial-template/CHANGELOG.md index b64329f7c7856c..021972a2178bbb 100644 --- a/packages/create-block-tutorial-template/CHANGELOG.md +++ b/packages/create-block-tutorial-template/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.2.0 (2023-12-13) + ## 3.1.0 (2023-11-29) ## 3.0.0 (2023-11-16) diff --git a/packages/create-block-tutorial-template/package.json b/packages/create-block-tutorial-template/package.json index 0fb9df38f3ae72..f562c560cb70c7 100644 --- a/packages/create-block-tutorial-template/package.json +++ b/packages/create-block-tutorial-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-tutorial-template", - "version": "3.1.0", + "version": "3.2.0-prerelease", "description": "This is a template for @wordpress/create-block that creates an example 'Copyright Date' block. This block is used in the official WordPress block development Quick Start Guide.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index b01e01038d12fa..7f6a5a0c428161 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.32.0 (2023-12-13) + ## 4.31.0 (2023-11-29) ## 4.30.0 (2023-11-16) diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 025e290929a200..8494abf2414c2d 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block", - "version": "4.31.0", + "version": "4.32.0-prerelease", "description": "Generates PHP, JS and CSS code for registering a block for a WordPress plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/CHANGELOG.md b/packages/customize-widgets/CHANGELOG.md index 7a0d7900ca5fe8..733a2e88914e89 100644 --- a/packages/customize-widgets/CHANGELOG.md +++ b/packages/customize-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index 14aff02afb0167..ebafa5c8b00f86 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.24.1", + "version": "4.25.0-prerelease", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data-controls/CHANGELOG.md b/packages/data-controls/CHANGELOG.md index f8ee4e0ccff3d1..a5be93faab0043 100644 --- a/packages/data-controls/CHANGELOG.md +++ b/packages/data-controls/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.17.0 (2023-12-13) + ## 3.16.0 (2023-11-29) ## 3.15.0 (2023-11-16) diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index 8f8e92e390d7a9..bec45518d12598 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data-controls", - "version": "3.16.0", + "version": "3.17.0-prerelease", "description": "A set of common controls for the @wordpress/data api.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 0cb8eb1f397a1a..328928d93d509b 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.18.0 (2023-12-13) + ## 9.17.0 (2023-11-29) ## 9.16.0 (2023-11-16) diff --git a/packages/data/package.json b/packages/data/package.json index 2ffac0ab80d85b..ff1a267bff3703 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "9.17.0", + "version": "9.18.0-prerelease", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md index 6ed52df1077824..bd4afefab43fef 100644 --- a/packages/dataviews/CHANGELOG.md +++ b/packages/dataviews/CHANGELOG.md @@ -1,3 +1,5 @@ <!-- Learn how to maintain this file at https://github.com/WordPress/gutenberg/tree/HEAD/packages#maintaining-changelogs. --> ## Unreleased + +## 0.2.0 (2023-12-13) diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index 1872480d759c37..e3abb744db2482 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dataviews", - "version": "0.1.0", + "version": "0.2.0-prerelease", "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/CHANGELOG.md b/packages/date/CHANGELOG.md index d2acf021c7de43..73ad2bc3daa06f 100644 --- a/packages/date/CHANGELOG.md +++ b/packages/date/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/date/package.json b/packages/date/package.json index 553cda7f431d47..2bd2bd469df0e2 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index 50e894da1b905c..58d248129ff201 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.31.0 (2023-12-13) + ## 4.30.0 (2023-11-29) ## 4.29.0 (2023-11-16) diff --git a/packages/dependency-extraction-webpack-plugin/package.json b/packages/dependency-extraction-webpack-plugin/package.json index cd97d3ccc3a533..38d05bbcb36f96 100644 --- a/packages/dependency-extraction-webpack-plugin/package.json +++ b/packages/dependency-extraction-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.30.0", + "version": "4.31.0-prerelease", "description": "Extract WordPress script dependencies from webpack bundles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/deprecated/CHANGELOG.md b/packages/deprecated/CHANGELOG.md index beeda0252ba186..f55a064fe5dcf6 100644 --- a/packages/deprecated/CHANGELOG.md +++ b/packages/deprecated/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/deprecated/package.json b/packages/deprecated/package.json index 21f6a5c04399bd..7161ed67c61785 100644 --- a/packages/deprecated/package.json +++ b/packages/deprecated/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/deprecated", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Deprecation utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/docgen/CHANGELOG.md b/packages/docgen/CHANGELOG.md index 3efd1867d4cb60..9ce1823b8bb6c5 100644 --- a/packages/docgen/CHANGELOG.md +++ b/packages/docgen/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.57.0 (2023-12-13) + ## 1.56.0 (2023-11-29) ## 1.55.0 (2023-11-16) diff --git a/packages/docgen/package.json b/packages/docgen/package.json index eaff29998d5938..dca0e2431e85b0 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/docgen", - "version": "1.56.0", + "version": "1.57.0-prerelease", "description": "Autogenerate public API documentation from exports and JSDoc comments.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom-ready/CHANGELOG.md b/packages/dom-ready/CHANGELOG.md index 78e95106b384ff..5def1df47ebae7 100644 --- a/packages/dom-ready/CHANGELOG.md +++ b/packages/dom-ready/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/dom-ready/package.json b/packages/dom-ready/package.json index 53a6d17dce2c6a..c01d3c4a7d36d1 100644 --- a/packages/dom-ready/package.json +++ b/packages/dom-ready/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom-ready", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Execute callback after the DOM is loaded.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/CHANGELOG.md b/packages/dom/CHANGELOG.md index cf594423e28d1f..6e76f6e1e3276a 100644 --- a/packages/dom/CHANGELOG.md +++ b/packages/dom/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/dom/package.json b/packages/dom/package.json index f8499debc0fe10..b0d5e52b155200 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "DOM utilities module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils-playwright/CHANGELOG.md b/packages/e2e-test-utils-playwright/CHANGELOG.md index f9b414cdd4927e..cd848a23461ae2 100644 --- a/packages/e2e-test-utils-playwright/CHANGELOG.md +++ b/packages/e2e-test-utils-playwright/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.16.0 (2023-12-13) + ## 0.15.0 (2023-11-29) ## 0.14.0 (2023-11-16) diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index 2e21a1ec485fdd..1c10d906a867d3 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.15.0", + "version": "0.16.0-prerelease", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils/CHANGELOG.md b/packages/e2e-test-utils/CHANGELOG.md index e22c44b53f7db3..9602fa9a33434e 100644 --- a/packages/e2e-test-utils/CHANGELOG.md +++ b/packages/e2e-test-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 10.19.0 (2023-12-13) + ## 10.18.0 (2023-11-29) ## 10.17.0 (2023-11-16) diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index 3927c478a3433c..0f47cea5f35e5a 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils", - "version": "10.18.0", + "version": "10.19.0-prerelease", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/CHANGELOG.md b/packages/e2e-tests/CHANGELOG.md index 2543ddb4cb12e2..bf448011997806 100644 --- a/packages/e2e-tests/CHANGELOG.md +++ b/packages/e2e-tests/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.19.0 (2023-12-13) + ## 7.18.0 (2023-11-29) ## 7.17.0 (2023-11-16) diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 103daf0498b539..9f37582eee4a68 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "7.18.1", + "version": "7.19.0-prerelease", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index 70c562e812f1ea..abf6e5b1c1c081 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.25.0 (2023-12-13) + ## 7.24.0 (2023-11-29) ## 7.23.0 (2023-11-16) diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index eea3306a2665ff..3556c10e61c998 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.24.1", + "version": "7.25.0-prerelease", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/CHANGELOG.md b/packages/edit-site/CHANGELOG.md index de75adec15e5a9..ff3e85639d1dee 100644 --- a/packages/edit-site/CHANGELOG.md +++ b/packages/edit-site/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.25.0 (2023-12-13) + ## 5.24.0 (2023-11-29) ## 5.23.0 (2023-11-16) diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index eba0a06012da78..fe030c77857102 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.24.1", + "version": "5.25.0-prerelease", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/CHANGELOG.md b/packages/edit-widgets/CHANGELOG.md index 885b7d5aa489b8..34fb6a54d8c25d 100644 --- a/packages/edit-widgets/CHANGELOG.md +++ b/packages/edit-widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.25.0 (2023-12-13) + ## 5.24.0 (2023-11-29) ## 5.23.0 (2023-11-16) diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index a983c1893ed127..5d6c1a71b6b588 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.24.1", + "version": "5.25.0-prerelease", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index 34019a6aad78d9..e1a7943e79f3e8 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 13.25.0 (2023-12-13) + ## 13.24.0 (2023-11-29) ## 13.23.0 (2023-11-16) diff --git a/packages/editor/package.json b/packages/editor/package.json index 70344e9dc3e72d..5114f29519172a 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.24.1", + "version": "13.25.0-prerelease", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/element/CHANGELOG.md b/packages/element/CHANGELOG.md index 574f801a1d7b7e..89c422ad27a0bb 100644 --- a/packages/element/CHANGELOG.md +++ b/packages/element/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.25.0 (2023-12-13) + ## 5.24.0 (2023-11-29) ## 5.23.0 (2023-11-16) diff --git a/packages/element/package.json b/packages/element/package.json index c40b80802d4a0a..41574c5e3cfbed 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "5.24.0", + "version": "5.25.0-prerelease", "description": "Element React module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/env/CHANGELOG.md b/packages/env/CHANGELOG.md index 8b39bea46f785e..6605add7800961 100644 --- a/packages/env/CHANGELOG.md +++ b/packages/env/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.0.0 (2023-12-13) + ### Breaking Change - Update Docker usage to `docker compose` V2 following [deprecation](https://docs.docker.com/compose/migrate/) of `docker-compose` V1. diff --git a/packages/env/package.json b/packages/env/package.json index cb362b6c9f3d1a..b201792c1659a5 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/env", - "version": "8.13.0", + "version": "9.0.0-prerelease", "description": "A zero-config, self contained local WordPress environment for development and testing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/escape-html/CHANGELOG.md b/packages/escape-html/CHANGELOG.md index 8be716a9504a8f..7b5f651626528e 100644 --- a/packages/escape-html/CHANGELOG.md +++ b/packages/escape-html/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.48.0 (2023-12-13) + ## 2.47.0 (2023-11-29) ## 2.46.0 (2023-11-16) diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index 7eacfe4f0e0a22..ee056314383099 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/escape-html", - "version": "2.47.0", + "version": "2.48.0-prerelease", "description": "Escape HTML utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 111dcbde574e63..533c5e7788c663 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 17.5.0 (2023-12-13) + ## 17.4.0 (2023-11-29) ## 17.3.0 (2023-11-16) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 4ea51fab8bab98..a7a16a0c52d755 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "17.4.0", + "version": "17.5.0-prerelease", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/CHANGELOG.md b/packages/format-library/CHANGELOG.md index 19da743a909436..8b3d314e9a0f25 100644 --- a/packages/format-library/CHANGELOG.md +++ b/packages/format-library/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 1f1ff7700a002e..8427193345a1c7 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.24.0", + "version": "4.25.0-prerelease", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/hooks/CHANGELOG.md b/packages/hooks/CHANGELOG.md index d8355c0fd472a3..c428f26ed17f10 100644 --- a/packages/hooks/CHANGELOG.md +++ b/packages/hooks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/hooks/package.json b/packages/hooks/package.json index e33b95790c7692..91d2bbe7d79b02 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/hooks", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "WordPress hooks library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/CHANGELOG.md b/packages/html-entities/CHANGELOG.md index a7cb330e988496..d7c1b049e37900 100644 --- a/packages/html-entities/CHANGELOG.md +++ b/packages/html-entities/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index 1b5a775a08ec0f..4bbd118210a262 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/i18n/CHANGELOG.md b/packages/i18n/CHANGELOG.md index 56b7c0ba885c56..f227f81571087f 100644 --- a/packages/i18n/CHANGELOG.md +++ b/packages/i18n/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 33a9e59fe2d51b..f0b77007903fad 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/i18n", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "WordPress internationalization (i18n) library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/CHANGELOG.md b/packages/icons/CHANGELOG.md index 827234758766d0..df0e3e43b1d816 100644 --- a/packages/icons/CHANGELOG.md +++ b/packages/icons/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 9.39.0 (2023-12-13) + ## 9.38.0 (2023-11-29) ## 9.37.0 (2023-11-16) diff --git a/packages/icons/package.json b/packages/icons/package.json index a357de3a39f261..bf254491a65f58 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "9.38.0", + "version": "9.39.0-prerelease", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index 3f54c4c6046d9a..ce90835dda23d3 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.1.0 (2023-12-13) + ## 3.0.0 (2023-11-29) ### Breaking Change diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index bf8576fd67ae73..53e67ff3b8fada 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "3.0.1", + "version": "3.1.0-prerelease", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/CHANGELOG.md b/packages/interface/CHANGELOG.md index e160bb6cef4b99..90e0326e02a47e 100644 --- a/packages/interface/CHANGELOG.md +++ b/packages/interface/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.25.0 (2023-12-13) + ## 5.24.0 (2023-11-29) ## 5.23.0 (2023-11-16) diff --git a/packages/interface/package.json b/packages/interface/package.json index f59c65af7366be..ff69679181fa74 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.24.0", + "version": "5.25.0-prerelease", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/is-shallow-equal/CHANGELOG.md b/packages/is-shallow-equal/CHANGELOG.md index 192a27bc9fabed..46ad3508ad0416 100644 --- a/packages/is-shallow-equal/CHANGELOG.md +++ b/packages/is-shallow-equal/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 0603d2b1499312..88e69d86eedd50 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/is-shallow-equal", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "Test for shallow equality between two objects or arrays.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/CHANGELOG.md b/packages/jest-console/CHANGELOG.md index 75fe088a70e767..bf9aba56a98e12 100644 --- a/packages/jest-console/CHANGELOG.md +++ b/packages/jest-console/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 7.19.0 (2023-12-13) + ## 7.18.0 (2023-11-29) ## 7.17.0 (2023-11-16) diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 2f2f062c4f0996..088c9ef869cfe6 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.18.0", + "version": "7.19.0-prerelease", "description": "Custom Jest matchers for the Console object.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/CHANGELOG.md b/packages/jest-preset-default/CHANGELOG.md index de33e4d54e133a..5b945545507814 100644 --- a/packages/jest-preset-default/CHANGELOG.md +++ b/packages/jest-preset-default/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 11.19.0 (2023-12-13) + ## 11.18.0 (2023-11-29) ## 11.17.0 (2023-11-16) diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 061feb00ab9e4f..9545c856501d74 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "11.18.0", + "version": "11.19.0-prerelease", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-puppeteer-axe/CHANGELOG.md b/packages/jest-puppeteer-axe/CHANGELOG.md index 55961c5abf1196..9114f0e0437727 100644 --- a/packages/jest-puppeteer-axe/CHANGELOG.md +++ b/packages/jest-puppeteer-axe/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.19.0 (2023-12-13) + ## 6.18.0 (2023-11-29) ## 6.17.0 (2023-11-16) diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index 9b6b6d66a91f08..e93fc9b21b8185 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.18.0", + "version": "6.19.0-prerelease", "description": "Axe API integration with Jest and Puppeteer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keyboard-shortcuts/CHANGELOG.md b/packages/keyboard-shortcuts/CHANGELOG.md index 1fb3ec9ea2b005..32175acb451f57 100644 --- a/packages/keyboard-shortcuts/CHANGELOG.md +++ b/packages/keyboard-shortcuts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index ec4b1f3e108998..001bf5d4673df3 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.24.0", + "version": "4.25.0-prerelease", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/CHANGELOG.md b/packages/keycodes/CHANGELOG.md index 3d24d2c0cb2e0c..b65e8f808d595a 100644 --- a/packages/keycodes/CHANGELOG.md +++ b/packages/keycodes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 9531b4980c1e20..18ca5c0c0ba525 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/CHANGELOG.md b/packages/lazy-import/CHANGELOG.md index 352132ddaa0146..87ebbe936eaa4c 100644 --- a/packages/lazy-import/CHANGELOG.md +++ b/packages/lazy-import/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.35.0 (2023-12-13) + ## 1.34.0 (2023-11-29) ## 1.33.0 (2023-11-16) diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index b490a38eccf86f..4d504ad140375d 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.34.0", + "version": "1.35.0-prerelease", "description": "Lazily import a module, installing it automatically if missing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/CHANGELOG.md b/packages/list-reusable-blocks/CHANGELOG.md index 5946c665d11d62..d1dd89a70ad630 100644 --- a/packages/list-reusable-blocks/CHANGELOG.md +++ b/packages/list-reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 29a0ab2479d926..849c17e2fdff96 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.24.0", + "version": "4.25.0-prerelease", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/media-utils/CHANGELOG.md b/packages/media-utils/CHANGELOG.md index 0329f2ea74c53c..8d8a13b4ae6ab8 100644 --- a/packages/media-utils/CHANGELOG.md +++ b/packages/media-utils/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.39.0 (2023-12-13) + ## 4.38.0 (2023-11-29) ## 4.37.0 (2023-11-16) diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index 1abe16387376c0..bf2f95dc0fbfdc 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.38.0", + "version": "4.39.0-prerelease", "description": "WordPress Media Upload Utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/CHANGELOG.md b/packages/notices/CHANGELOG.md index 12abde127dc9ec..0b4c95da740056 100644 --- a/packages/notices/CHANGELOG.md +++ b/packages/notices/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.16.0 (2023-12-13) + ## 4.15.0 (2023-11-29) ## 4.14.0 (2023-11-16) diff --git a/packages/notices/package.json b/packages/notices/package.json index b5fdfe0377dabb..553a702a6b0028 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.15.0", + "version": "4.16.0-prerelease", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/npm-package-json-lint-config/CHANGELOG.md b/packages/npm-package-json-lint-config/CHANGELOG.md index aa45ed933d69eb..2eaaf4c0df1e92 100644 --- a/packages/npm-package-json-lint-config/CHANGELOG.md +++ b/packages/npm-package-json-lint-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.33.0 (2023-12-13) + ## 4.32.0 (2023-11-29) ## 4.31.0 (2023-11-16) diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index 09df294498cdbb..8eebe21ebc6711 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.32.0", + "version": "4.33.0-prerelease", "description": "WordPress npm-package-json-lint shareable configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/CHANGELOG.md b/packages/nux/CHANGELOG.md index 959e582b402946..9980b2d629d0ba 100644 --- a/packages/nux/CHANGELOG.md +++ b/packages/nux/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 8.10.0 (2023-12-13) + ## 8.9.0 (2023-11-29) ## 8.8.0 (2023-11-16) diff --git a/packages/nux/package.json b/packages/nux/package.json index e61e63703c5d6d..3bb59d190f93f5 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.9.0", + "version": "8.10.0-prerelease", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/CHANGELOG.md b/packages/patterns/CHANGELOG.md index 416d2bfd7c22c7..566bd024cf3805 100644 --- a/packages/patterns/CHANGELOG.md +++ b/packages/patterns/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.9.0 (2023-12-13) + ## 1.8.0 (2023-11-29) ## 1.7.0 (2023-11-16) diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 2fa13bc3fdddfd..93986dc6d7305b 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.8.0", + "version": "1.9.0-prerelease", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/CHANGELOG.md b/packages/plugins/CHANGELOG.md index 27888ee6a6cfbd..b41c2e9f112bda 100644 --- a/packages/plugins/CHANGELOG.md +++ b/packages/plugins/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.16.0 (2023-12-13) + ## 6.15.0 (2023-11-29) ## 6.14.0 (2023-11-16) diff --git a/packages/plugins/package.json b/packages/plugins/package.json index f28b5e46de9077..5ef7609d832a83 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "6.15.0", + "version": "6.16.0-prerelease", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-plugins-preset/CHANGELOG.md b/packages/postcss-plugins-preset/CHANGELOG.md index 8c1512fb1d7e84..00e7fb9a8c6c4a 100644 --- a/packages/postcss-plugins-preset/CHANGELOG.md +++ b/packages/postcss-plugins-preset/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.32.0 (2023-12-13) + ## 4.31.0 (2023-11-29) ## 4.30.0 (2023-11-16) diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index 8f697aee2d0826..78fe995fe80ae6 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-plugins-preset", - "version": "4.31.0", + "version": "4.32.0-prerelease", "description": "PostCSS sharable plugins preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-themes/CHANGELOG.md b/packages/postcss-themes/CHANGELOG.md index 4948f7afbfae42..7c696adb50733f 100644 --- a/packages/postcss-themes/CHANGELOG.md +++ b/packages/postcss-themes/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.31.0 (2023-12-13) + ## 5.30.0 (2023-11-29) ## 5.29.0 (2023-11-16) diff --git a/packages/postcss-themes/package.json b/packages/postcss-themes/package.json index 7f0d39b7a5b171..51810de560248d 100644 --- a/packages/postcss-themes/package.json +++ b/packages/postcss-themes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-themes", - "version": "5.30.0", + "version": "5.31.0-prerelease", "description": "PostCSS plugin to generate theme colors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences-persistence/CHANGELOG.md b/packages/preferences-persistence/CHANGELOG.md index 11066a94f0dcde..b227aa5ad23739 100644 --- a/packages/preferences-persistence/CHANGELOG.md +++ b/packages/preferences-persistence/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.40.0 (2023-12-13) + ## 1.39.0 (2023-11-29) ## 1.38.0 (2023-11-16) diff --git a/packages/preferences-persistence/package.json b/packages/preferences-persistence/package.json index ce3375ad43d67e..84217935b84de0 100644 --- a/packages/preferences-persistence/package.json +++ b/packages/preferences-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences-persistence", - "version": "1.39.0", + "version": "1.40.0-prerelease", "description": "Persistence utilities for `wordpress/preferences`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences/CHANGELOG.md b/packages/preferences/CHANGELOG.md index dc6c84ad6cd5b4..363f62c70b3d93 100644 --- a/packages/preferences/CHANGELOG.md +++ b/packages/preferences/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.25.0 (2023-12-13) + ## 3.24.0 (2023-11-29) ## 3.23.0 (2023-11-16) diff --git a/packages/preferences/package.json b/packages/preferences/package.json index c2c81c22f782af..8c46ee4d1c6644 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences", - "version": "3.24.0", + "version": "3.25.0-prerelease", "description": "Utilities for managing WordPress preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/prettier-config/CHANGELOG.md b/packages/prettier-config/CHANGELOG.md index 2ef95f2fb1d02f..be847570c06f78 100644 --- a/packages/prettier-config/CHANGELOG.md +++ b/packages/prettier-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.5.0 (2023-12-13) + ## 3.4.0 (2023-11-29) ## 3.3.0 (2023-11-16) diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json index fc7a79934737f0..8656f9390c98a5 100644 --- a/packages/prettier-config/package.json +++ b/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/prettier-config", - "version": "3.4.0", + "version": "3.5.0-prerelease", "description": "WordPress Prettier shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/primitives/CHANGELOG.md b/packages/primitives/CHANGELOG.md index b18081f02bf16f..bbea3951de7c95 100644 --- a/packages/primitives/CHANGELOG.md +++ b/packages/primitives/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-12-13) + ## 3.45.0 (2023-11-29) ## 3.44.0 (2023-11-16) diff --git a/packages/primitives/package.json b/packages/primitives/package.json index 226b6f7998c0a9..e5b1db342f8779 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/primitives", - "version": "3.45.0", + "version": "3.46.0-prerelease", "description": "WordPress cross-platform primitives.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/priority-queue/CHANGELOG.md b/packages/priority-queue/CHANGELOG.md index df26c90b131aa5..dc82da748d2eb0 100644 --- a/packages/priority-queue/CHANGELOG.md +++ b/packages/priority-queue/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.48.0 (2023-12-13) + ## 2.47.0 (2023-11-29) ## 2.46.0 (2023-11-16) diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json index cad513efb2583c..eaecf6cc483659 100644 --- a/packages/priority-queue/package.json +++ b/packages/priority-queue/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/priority-queue", - "version": "2.47.0", + "version": "2.48.0-prerelease", "description": "Generic browser priority queue.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/private-apis/CHANGELOG.md b/packages/private-apis/CHANGELOG.md index 16cfc9f3bd5a04..4951a8b29a7dc1 100644 --- a/packages/private-apis/CHANGELOG.md +++ b/packages/private-apis/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.30.0 (2023-12-13) + ## 0.29.0 (2023-11-29) ## 0.28.0 (2023-11-16) diff --git a/packages/private-apis/package.json b/packages/private-apis/package.json index aa19e14d284ea9..23a5e25a1aa3d0 100644 --- a/packages/private-apis/package.json +++ b/packages/private-apis/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/private-apis", - "version": "0.29.0", + "version": "0.30.0-prerelease", "description": "Internal experimental APIs for WordPress core.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/project-management-automation/CHANGELOG.md b/packages/project-management-automation/CHANGELOG.md index 5ae304b4c04780..09247de37995de 100644 --- a/packages/project-management-automation/CHANGELOG.md +++ b/packages/project-management-automation/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.47.0 (2023-12-13) + ## 1.46.0 (2023-11-29) ## 1.45.0 (2023-11-16) diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index 613f580fb0e5f2..60aebb934cac27 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/project-management-automation", - "version": "1.46.0", + "version": "1.47.0-prerelease", "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", diff --git a/packages/react-i18n/CHANGELOG.md b/packages/react-i18n/CHANGELOG.md index d6acc305c22f39..4e15abf8fadfb5 100644 --- a/packages/react-i18n/CHANGELOG.md +++ b/packages/react-i18n/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.46.0 (2023-12-13) + ## 3.45.0 (2023-11-29) ## 3.44.0 (2023-11-16) diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index c6a953397ef99e..f2cc6f2d9badc5 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-i18n", - "version": "3.45.0", + "version": "3.46.0-prerelease", "description": "React bindings for @wordpress/i18n.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md index f266e64d9d0f80..3a5dad2c518521 100644 --- a/packages/readable-js-assets-webpack-plugin/CHANGELOG.md +++ b/packages/readable-js-assets-webpack-plugin/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.31.0 (2023-12-13) + ## 2.30.0 (2023-11-29) ## 2.29.0 (2023-11-16) diff --git a/packages/readable-js-assets-webpack-plugin/package.json b/packages/readable-js-assets-webpack-plugin/package.json index d7cf02db881c37..ea88ce304d0ad3 100644 --- a/packages/readable-js-assets-webpack-plugin/package.json +++ b/packages/readable-js-assets-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.30.0", + "version": "2.31.0-prerelease", "description": "Generate a readable JS file for each JS asset.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/redux-routine/CHANGELOG.md b/packages/redux-routine/CHANGELOG.md index d39cab61e6dd97..9559858c133e45 100644 --- a/packages/redux-routine/CHANGELOG.md +++ b/packages/redux-routine/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.48.0 (2023-12-13) + ## 4.47.0 (2023-11-29) ## 4.46.0 (2023-11-16) diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index 3dbd8557a6a609..d4a1b05ef646fa 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/redux-routine", - "version": "4.47.0", + "version": "4.48.0-prerelease", "description": "Redux middleware for generator coroutines.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/CHANGELOG.md b/packages/reusable-blocks/CHANGELOG.md index 15a11093d280ca..989f649f161f23 100644 --- a/packages/reusable-blocks/CHANGELOG.md +++ b/packages/reusable-blocks/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 2ab00edaba81ad..130a07e909bdc2 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "4.24.0", + "version": "4.25.0-prerelease", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/CHANGELOG.md b/packages/rich-text/CHANGELOG.md index 363ba40911fc65..ebc6157a84c1b1 100644 --- a/packages/rich-text/CHANGELOG.md +++ b/packages/rich-text/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 6.25.0 (2023-12-13) + ## 6.24.0 (2023-11-29) ## 6.23.0 (2023-11-16) diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index d5cfb022b662c3..5792c4998e7265 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "6.24.0", + "version": "6.25.0-prerelease", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/router/CHANGELOG.md b/packages/router/CHANGELOG.md index 5ab30cce439239..90f93aa1f97001 100644 --- a/packages/router/CHANGELOG.md +++ b/packages/router/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.17.0 (2023-12-13) + ## 0.16.0 (2023-11-29) ## 0.15.0 (2023-11-16) diff --git a/packages/router/package.json b/packages/router/package.json index d74bf2c233627d..6c4a0328bfea5f 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/router", - "version": "0.16.0", + "version": "0.17.0-prerelease", "description": "Router API for WordPress pages.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 7ad87c315df80b..9e2f0ff87a3abf 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 26.19.0 (2023-12-13) + ### Bug Fix - Fix CSS imports not minified ([#56516](https://github.com/WordPress/gutenberg/pull/56516)). diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 1002c1817b4025..3684b1ba788a6b 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "26.18.0", + "version": "26.19.0-prerelease", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/server-side-render/CHANGELOG.md b/packages/server-side-render/CHANGELOG.md index 6f036a521989fc..89f9150ade561e 100644 --- a/packages/server-side-render/CHANGELOG.md +++ b/packages/server-side-render/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 4.25.0 (2023-12-13) + ## 4.24.0 (2023-11-29) ## 4.23.0 (2023-11-16) diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 26d999e4a10e4b..8b1d729d8e3e5b 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "4.24.0", + "version": "4.25.0-prerelease", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/shortcode/CHANGELOG.md b/packages/shortcode/CHANGELOG.md index cc4bd058236a62..6d4bb284be7d4a 100644 --- a/packages/shortcode/CHANGELOG.md +++ b/packages/shortcode/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index fe383905127534..c2b13aa061131f 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/shortcode", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "Shortcode module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/style-engine/CHANGELOG.md b/packages/style-engine/CHANGELOG.md index 9f22a226e6037f..35331cb4fe9a4a 100644 --- a/packages/style-engine/CHANGELOG.md +++ b/packages/style-engine/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 1.31.0 (2023-12-13) + ## 1.30.0 (2023-11-29) ## 1.29.0 (2023-11-16) diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json index f6c1aa2c0c2bae..a32fcd14f8b17a 100644 --- a/packages/style-engine/package.json +++ b/packages/style-engine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/style-engine", - "version": "1.30.0", + "version": "1.31.0-prerelease", "description": "A suite of parsers and compilers for WordPress styles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/stylelint-config/CHANGELOG.md b/packages/stylelint-config/CHANGELOG.md index 7bbe9a2da0592e..b7b6cb712ff759 100644 --- a/packages/stylelint-config/CHANGELOG.md +++ b/packages/stylelint-config/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 21.31.0 (2023-12-13) + ## 21.30.0 (2023-11-29) ## 21.29.0 (2023-11-16) diff --git a/packages/stylelint-config/package.json b/packages/stylelint-config/package.json index ddf7f120fb5497..3bab91b1932a60 100644 --- a/packages/stylelint-config/package.json +++ b/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/stylelint-config", - "version": "21.30.0", + "version": "21.31.0-prerelease", "description": "stylelint config for WordPress development.", "author": "The WordPress Contributors", "license": "MIT", diff --git a/packages/sync/CHANGELOG.md b/packages/sync/CHANGELOG.md index fa1810c68c5799..206d4957a12874 100644 --- a/packages/sync/CHANGELOG.md +++ b/packages/sync/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.10.0 (2023-12-13) + ## 0.9.0 (2023-11-29) ## 0.8.0 (2023-11-16) diff --git a/packages/sync/package.json b/packages/sync/package.json index 8bef91b2689333..6542c6d3bdd4be 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/sync", - "version": "0.9.0", + "version": "0.10.0-prerelease", "description": "Sync Data.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/token-list/CHANGELOG.md b/packages/token-list/CHANGELOG.md index 3c769bcec7fb77..7488e836b0c137 100644 --- a/packages/token-list/CHANGELOG.md +++ b/packages/token-list/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.48.0 (2023-12-13) + ## 2.47.0 (2023-11-29) ## 2.46.0 (2023-11-16) diff --git a/packages/token-list/package.json b/packages/token-list/package.json index c818471e31aa46..911e4181a57c71 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/token-list", - "version": "2.47.0", + "version": "2.48.0-prerelease", "description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/undo-manager/CHANGELOG.md b/packages/undo-manager/CHANGELOG.md index 412281eefcee7f..2ff22f8020a32e 100644 --- a/packages/undo-manager/CHANGELOG.md +++ b/packages/undo-manager/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 0.8.0 (2023-12-13) + ## 0.7.0 (2023-11-29) ## 0.6.0 (2023-11-16) diff --git a/packages/undo-manager/package.json b/packages/undo-manager/package.json index 040e88dcffd059..1a83e4d658ccf0 100644 --- a/packages/undo-manager/package.json +++ b/packages/undo-manager/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/undo-manager", - "version": "0.7.0", + "version": "0.8.0-prerelease", "description": "A small package to manage undo/redo.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/CHANGELOG.md b/packages/url/CHANGELOG.md index 5bed123ffbfe70..29ca8923380d96 100644 --- a/packages/url/CHANGELOG.md +++ b/packages/url/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.49.0 (2023-12-13) + ## 3.48.0 (2023-11-29) ## 3.47.0 (2023-11-16) diff --git a/packages/url/package.json b/packages/url/package.json index d1327f4b6e8a22..3f19518a41ecd8 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "3.48.0", + "version": "3.49.0-prerelease", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/CHANGELOG.md b/packages/viewport/CHANGELOG.md index 7f32eaea5b2931..4d03197988aeca 100644 --- a/packages/viewport/CHANGELOG.md +++ b/packages/viewport/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 5.25.0 (2023-12-13) + ## 5.24.0 (2023-11-29) ## 5.23.0 (2023-11-16) diff --git a/packages/viewport/package.json b/packages/viewport/package.json index 2eca13e8f73066..ef4bd098e46be5 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "5.24.0", + "version": "5.25.0-prerelease", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/warning/CHANGELOG.md b/packages/warning/CHANGELOG.md index 16dec4f7ea711b..629291f88171be 100644 --- a/packages/warning/CHANGELOG.md +++ b/packages/warning/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 2.48.0 (2023-12-13) + ## 2.47.0 (2023-11-29) ## 2.46.0 (2023-11-16) diff --git a/packages/warning/package.json b/packages/warning/package.json index 42ad8f73a289d8..f5af9e94540f49 100644 --- a/packages/warning/package.json +++ b/packages/warning/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/warning", - "version": "2.47.0", + "version": "2.48.0-prerelease", "description": "Warning utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/CHANGELOG.md b/packages/widgets/CHANGELOG.md index b4e8a97665eaae..a24187330ba58a 100644 --- a/packages/widgets/CHANGELOG.md +++ b/packages/widgets/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.25.0 (2023-12-13) + ## 3.24.0 (2023-11-29) ## 3.23.0 (2023-11-16) diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 9118333a4e356f..1983faf7822168 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "3.24.0", + "version": "3.25.0-prerelease", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/wordcount/CHANGELOG.md b/packages/wordcount/CHANGELOG.md index 75b25aaec0e97e..9fab4788c9f41c 100644 --- a/packages/wordcount/CHANGELOG.md +++ b/packages/wordcount/CHANGELOG.md @@ -2,6 +2,8 @@ ## Unreleased +## 3.48.0 (2023-12-13) + ## 3.47.0 (2023-11-29) ## 3.46.0 (2023-11-16) diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json index f100df8c7b0a51..97fb14c25c9610 100644 --- a/packages/wordcount/package.json +++ b/packages/wordcount/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/wordcount", - "version": "3.47.0", + "version": "3.48.0-prerelease", "description": "WordPress word count utility.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From a1dde74c4aa6d8217b1dbced219729ef6f93dc4c Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 13 Dec 2023 10:20:59 +0000 Subject: [PATCH 154/325] chore(release): publish - @wordpress/a11y@3.48.0 - @wordpress/annotations@2.48.0 - @wordpress/api-fetch@6.45.0 - @wordpress/autop@3.48.0 - @wordpress/babel-plugin-import-jsx-pragma@4.31.0 - @wordpress/babel-plugin-makepot@5.32.0 - @wordpress/babel-preset-default@7.32.0 - @wordpress/base-styles@4.39.0 - @wordpress/blob@3.48.0 - @wordpress/block-directory@4.25.0 - @wordpress/block-editor@12.16.0 - @wordpress/block-library@8.25.0 - @wordpress/block-serialization-default-parser@4.48.0 - @wordpress/block-serialization-spec-parser@4.48.0 - @wordpress/blocks@12.25.0 - @wordpress/browserslist-config@5.31.0 - @wordpress/commands@0.19.0 - @wordpress/components@25.14.0 - @wordpress/compose@6.25.0 - @wordpress/core-commands@0.17.0 - @wordpress/core-data@6.25.0 - @wordpress/create-block@4.32.0 - @wordpress/create-block-interactive-template@1.11.0 - @wordpress/create-block-tutorial-template@3.2.0 - @wordpress/customize-widgets@4.25.0 - @wordpress/data@9.18.0 - @wordpress/data-controls@3.17.0 - @wordpress/dataviews@0.2.0 - @wordpress/date@4.48.0 - @wordpress/dependency-extraction-webpack-plugin@4.31.0 - @wordpress/deprecated@3.48.0 - @wordpress/docgen@1.57.0 - @wordpress/dom@3.48.0 - @wordpress/dom-ready@3.48.0 - @wordpress/e2e-test-utils@10.19.0 - @wordpress/e2e-test-utils-playwright@0.16.0 - @wordpress/e2e-tests@7.19.0 - @wordpress/edit-post@7.25.0 - @wordpress/edit-site@5.25.0 - @wordpress/edit-widgets@5.25.0 - @wordpress/editor@13.25.0 - @wordpress/element@5.25.0 - @wordpress/env@9.0.0 - @wordpress/escape-html@2.48.0 - @wordpress/eslint-plugin@17.5.0 - @wordpress/format-library@4.25.0 - @wordpress/hooks@3.48.0 - @wordpress/html-entities@3.48.0 - @wordpress/i18n@4.48.0 - @wordpress/icons@9.39.0 - @wordpress/interactivity@3.1.0 - @wordpress/interface@5.25.0 - @wordpress/is-shallow-equal@4.48.0 - @wordpress/jest-console@7.19.0 - @wordpress/jest-preset-default@11.19.0 - @wordpress/jest-puppeteer-axe@6.19.0 - @wordpress/keyboard-shortcuts@4.25.0 - @wordpress/keycodes@3.48.0 - @wordpress/lazy-import@1.35.0 - @wordpress/list-reusable-blocks@4.25.0 - @wordpress/media-utils@4.39.0 - @wordpress/notices@4.16.0 - @wordpress/npm-package-json-lint-config@4.33.0 - @wordpress/nux@8.10.0 - @wordpress/patterns@1.9.0 - @wordpress/plugins@6.16.0 - @wordpress/postcss-plugins-preset@4.32.0 - @wordpress/postcss-themes@5.31.0 - @wordpress/preferences@3.25.0 - @wordpress/preferences-persistence@1.40.0 - @wordpress/prettier-config@3.5.0 - @wordpress/primitives@3.46.0 - @wordpress/priority-queue@2.48.0 - @wordpress/private-apis@0.30.0 - @wordpress/project-management-automation@1.47.0 - @wordpress/react-i18n@3.46.0 - @wordpress/readable-js-assets-webpack-plugin@2.31.0 - @wordpress/redux-routine@4.48.0 - @wordpress/reusable-blocks@4.25.0 - @wordpress/rich-text@6.25.0 - @wordpress/router@0.17.0 - @wordpress/scripts@26.19.0 - @wordpress/server-side-render@4.25.0 - @wordpress/shortcode@3.48.0 - @wordpress/style-engine@1.31.0 - @wordpress/stylelint-config@21.31.0 - @wordpress/sync@0.10.0 - @wordpress/token-list@2.48.0 - @wordpress/undo-manager@0.8.0 - @wordpress/url@3.49.0 - @wordpress/viewport@5.25.0 - @wordpress/warning@2.48.0 - @wordpress/widgets@3.25.0 - @wordpress/wordcount@3.48.0 --- package-lock.json | 186 +++++++++--------- packages/a11y/package.json | 2 +- packages/annotations/package.json | 2 +- packages/api-fetch/package.json | 2 +- packages/autop/package.json | 2 +- .../package.json | 2 +- packages/babel-plugin-makepot/package.json | 2 +- packages/babel-preset-default/package.json | 2 +- packages/base-styles/package.json | 2 +- packages/blob/package.json | 2 +- packages/block-directory/package.json | 2 +- packages/block-editor/package.json | 2 +- packages/block-library/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- packages/blocks/package.json | 2 +- packages/browserslist-config/package.json | 2 +- packages/commands/package.json | 2 +- packages/components/package.json | 2 +- packages/compose/package.json | 2 +- packages/core-commands/package.json | 2 +- packages/core-data/package.json | 2 +- .../package.json | 2 +- .../package.json | 2 +- packages/create-block/package.json | 2 +- packages/customize-widgets/package.json | 2 +- packages/data-controls/package.json | 2 +- packages/data/package.json | 2 +- packages/dataviews/package.json | 2 +- packages/date/package.json | 2 +- .../package.json | 2 +- packages/deprecated/package.json | 2 +- packages/docgen/package.json | 2 +- packages/dom-ready/package.json | 2 +- packages/dom/package.json | 2 +- .../e2e-test-utils-playwright/package.json | 2 +- packages/e2e-test-utils/package.json | 2 +- packages/e2e-tests/package.json | 2 +- packages/edit-post/package.json | 2 +- packages/edit-site/package.json | 2 +- packages/edit-widgets/package.json | 2 +- packages/editor/package.json | 2 +- packages/element/package.json | 2 +- packages/env/package.json | 2 +- packages/escape-html/package.json | 2 +- packages/eslint-plugin/package.json | 2 +- packages/format-library/package.json | 2 +- packages/hooks/package.json | 2 +- packages/html-entities/package.json | 2 +- packages/i18n/package.json | 2 +- packages/icons/package.json | 2 +- packages/interactivity/package.json | 2 +- packages/interface/package.json | 2 +- packages/is-shallow-equal/package.json | 2 +- packages/jest-console/package.json | 2 +- packages/jest-preset-default/package.json | 2 +- packages/jest-puppeteer-axe/package.json | 2 +- packages/keyboard-shortcuts/package.json | 2 +- packages/keycodes/package.json | 2 +- packages/lazy-import/package.json | 2 +- packages/list-reusable-blocks/package.json | 2 +- packages/media-utils/package.json | 2 +- packages/notices/package.json | 2 +- .../npm-package-json-lint-config/package.json | 2 +- packages/nux/package.json | 2 +- packages/patterns/package.json | 2 +- packages/plugins/package.json | 2 +- packages/postcss-plugins-preset/package.json | 2 +- packages/postcss-themes/package.json | 2 +- packages/preferences-persistence/package.json | 2 +- packages/preferences/package.json | 2 +- packages/prettier-config/package.json | 2 +- packages/primitives/package.json | 2 +- packages/priority-queue/package.json | 2 +- packages/private-apis/package.json | 2 +- .../package.json | 2 +- packages/react-i18n/package.json | 2 +- .../package.json | 2 +- packages/redux-routine/package.json | 2 +- packages/reusable-blocks/package.json | 2 +- packages/rich-text/package.json | 2 +- packages/router/package.json | 2 +- packages/scripts/package.json | 2 +- packages/server-side-render/package.json | 2 +- packages/shortcode/package.json | 2 +- packages/style-engine/package.json | 2 +- packages/stylelint-config/package.json | 2 +- packages/sync/package.json | 2 +- packages/token-list/package.json | 2 +- packages/undo-manager/package.json | 2 +- packages/url/package.json | 2 +- packages/viewport/package.json | 2 +- packages/warning/package.json | 2 +- packages/widgets/package.json | 2 +- packages/wordcount/package.json | 2 +- 95 files changed, 187 insertions(+), 187 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0c9500416a962..3d707b51aab83e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54275,7 +54275,7 @@ }, "packages/a11y": { "name": "@wordpress/a11y", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54288,7 +54288,7 @@ }, "packages/annotations": { "name": "@wordpress/annotations", - "version": "2.47.0", + "version": "2.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54316,7 +54316,7 @@ }, "packages/api-fetch": { "name": "@wordpress/api-fetch", - "version": "6.44.0", + "version": "6.45.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54329,7 +54329,7 @@ }, "packages/autop": { "name": "@wordpress/autop", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54340,7 +54340,7 @@ }, "packages/babel-plugin-import-jsx-pragma": { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.30.0", + "version": "4.31.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54352,7 +54352,7 @@ }, "packages/babel-plugin-makepot": { "name": "@wordpress/babel-plugin-makepot", - "version": "5.31.0", + "version": "5.32.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54369,7 +54369,7 @@ }, "packages/babel-preset-default": { "name": "@wordpress/babel-preset-default", - "version": "7.31.0", + "version": "7.32.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54392,13 +54392,13 @@ }, "packages/base-styles": { "name": "@wordpress/base-styles", - "version": "4.38.0", + "version": "4.39.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/blob": { "name": "@wordpress/blob", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54409,7 +54409,7 @@ }, "packages/block-directory": { "name": "@wordpress/block-directory", - "version": "4.24.1", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54444,7 +54444,7 @@ }, "packages/block-editor": { "name": "@wordpress/block-editor", - "version": "12.15.0", + "version": "12.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54541,7 +54541,7 @@ }, "packages/block-library": { "name": "@wordpress/block-library", - "version": "8.24.1", + "version": "8.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54603,7 +54603,7 @@ }, "packages/block-serialization-default-parser": { "name": "@wordpress/block-serialization-default-parser", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -54614,7 +54614,7 @@ }, "packages/block-serialization-spec-parser": { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "pegjs": "^0.10.0", @@ -54626,7 +54626,7 @@ }, "packages/blocks": { "name": "@wordpress/blocks", - "version": "12.24.0", + "version": "12.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54674,7 +54674,7 @@ }, "packages/browserslist-config": { "name": "@wordpress/browserslist-config", - "version": "5.30.0", + "version": "5.31.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -54683,7 +54683,7 @@ }, "packages/commands": { "name": "@wordpress/commands", - "version": "0.18.0", + "version": "0.19.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54708,7 +54708,7 @@ }, "packages/components": { "name": "@wordpress/components", - "version": "25.13.0", + "version": "25.14.0", "license": "GPL-2.0-or-later", "dependencies": { "@ariakit/react": "^0.3.5", @@ -54814,7 +54814,7 @@ }, "packages/compose": { "name": "@wordpress/compose", - "version": "6.24.0", + "version": "6.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54840,7 +54840,7 @@ }, "packages/core-commands": { "name": "@wordpress/core-commands", - "version": "0.16.0", + "version": "0.17.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54865,7 +54865,7 @@ }, "packages/core-data": { "name": "@wordpress/core-data", - "version": "6.24.0", + "version": "6.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54909,7 +54909,7 @@ }, "packages/create-block": { "name": "@wordpress/create-block", - "version": "4.31.0", + "version": "4.32.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -54937,13 +54937,13 @@ }, "packages/create-block-tutorial-template": { "name": "@wordpress/create-block-tutorial-template", - "version": "3.1.0", + "version": "3.2.0", "dev": true, "license": "GPL-2.0-or-later" }, "packages/customize-widgets": { "name": "@wordpress/customize-widgets", - "version": "4.24.1", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -54980,7 +54980,7 @@ }, "packages/data": { "name": "@wordpress/data", - "version": "9.17.0", + "version": "9.18.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55008,7 +55008,7 @@ }, "packages/data-controls": { "name": "@wordpress/data-controls", - "version": "3.16.0", + "version": "3.17.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55025,7 +55025,7 @@ }, "packages/dataviews": { "name": "@wordpress/dataviews", - "version": "0.1.0", + "version": "0.2.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55049,7 +55049,7 @@ }, "packages/date": { "name": "@wordpress/date", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55063,7 +55063,7 @@ }, "packages/dependency-extraction-webpack-plugin": { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.30.0", + "version": "4.31.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55079,7 +55079,7 @@ }, "packages/deprecated": { "name": "@wordpress/deprecated", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55091,7 +55091,7 @@ }, "packages/docgen": { "name": "@wordpress/docgen", - "version": "1.56.0", + "version": "1.57.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55109,7 +55109,7 @@ }, "packages/dom": { "name": "@wordpress/dom", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55121,7 +55121,7 @@ }, "packages/dom-ready": { "name": "@wordpress/dom-ready", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55132,7 +55132,7 @@ }, "packages/e2e-test-utils": { "name": "@wordpress/e2e-test-utils", - "version": "10.18.0", + "version": "10.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55154,7 +55154,7 @@ }, "packages/e2e-test-utils-playwright": { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.15.0", + "version": "0.16.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55177,7 +55177,7 @@ }, "packages/e2e-tests": { "name": "@wordpress/e2e-tests", - "version": "7.18.1", + "version": "7.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55216,7 +55216,7 @@ }, "packages/edit-post": { "name": "@wordpress/edit-post", - "version": "7.24.1", + "version": "7.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55264,7 +55264,7 @@ }, "packages/edit-site": { "name": "@wordpress/edit-site", - "version": "5.24.1", + "version": "5.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55329,7 +55329,7 @@ }, "packages/edit-widgets": { "name": "@wordpress/edit-widgets", - "version": "5.24.1", + "version": "5.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55371,7 +55371,7 @@ }, "packages/editor": { "name": "@wordpress/editor", - "version": "13.24.1", + "version": "13.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55422,7 +55422,7 @@ }, "packages/element": { "name": "@wordpress/element", - "version": "5.24.0", + "version": "5.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55440,7 +55440,7 @@ }, "packages/env": { "name": "@wordpress/env", - "version": "8.13.0", + "version": "9.0.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55574,7 +55574,7 @@ }, "packages/escape-html": { "name": "@wordpress/escape-html", - "version": "2.47.0", + "version": "2.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55585,7 +55585,7 @@ }, "packages/eslint-plugin": { "name": "@wordpress/eslint-plugin", - "version": "17.4.0", + "version": "17.5.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55628,7 +55628,7 @@ }, "packages/format-library": { "name": "@wordpress/format-library", - "version": "4.24.0", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55654,7 +55654,7 @@ }, "packages/hooks": { "name": "@wordpress/hooks", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55665,7 +55665,7 @@ }, "packages/html-entities": { "name": "@wordpress/html-entities", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55676,7 +55676,7 @@ }, "packages/i18n": { "name": "@wordpress/i18n", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55695,7 +55695,7 @@ }, "packages/icons": { "name": "@wordpress/icons", - "version": "9.38.0", + "version": "9.39.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55708,7 +55708,7 @@ }, "packages/interactivity": { "name": "@wordpress/interactivity", - "version": "3.0.1", + "version": "3.1.0", "license": "GPL-2.0-or-later", "dependencies": { "@preact/signals": "^1.1.3", @@ -55721,7 +55721,7 @@ }, "packages/interface": { "name": "@wordpress/interface", - "version": "5.24.0", + "version": "5.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55748,7 +55748,7 @@ }, "packages/is-shallow-equal": { "name": "@wordpress/is-shallow-equal", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -55759,7 +55759,7 @@ }, "packages/jest-console": { "name": "@wordpress/jest-console", - "version": "7.18.0", + "version": "7.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55775,7 +55775,7 @@ }, "packages/jest-preset-default": { "name": "@wordpress/jest-preset-default", - "version": "11.18.0", + "version": "11.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55792,7 +55792,7 @@ }, "packages/jest-puppeteer-axe": { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.18.0", + "version": "6.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55814,7 +55814,7 @@ }, "packages/keyboard-shortcuts": { "name": "@wordpress/keyboard-shortcuts", - "version": "4.24.0", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55832,7 +55832,7 @@ }, "packages/keycodes": { "name": "@wordpress/keycodes", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55844,7 +55844,7 @@ }, "packages/lazy-import": { "name": "@wordpress/lazy-import", - "version": "1.34.0", + "version": "1.35.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55858,7 +55858,7 @@ }, "packages/list-reusable-blocks": { "name": "@wordpress/list-reusable-blocks", - "version": "4.24.0", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55880,7 +55880,7 @@ }, "packages/media-utils": { "name": "@wordpress/media-utils", - "version": "4.38.0", + "version": "4.39.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55895,7 +55895,7 @@ }, "packages/notices": { "name": "@wordpress/notices", - "version": "4.15.0", + "version": "4.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55911,7 +55911,7 @@ }, "packages/npm-package-json-lint-config": { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.32.0", + "version": "4.33.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -55923,7 +55923,7 @@ }, "packages/nux": { "name": "@wordpress/nux", - "version": "8.9.0", + "version": "8.10.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55946,7 +55946,7 @@ }, "packages/patterns": { "name": "@wordpress/patterns", - "version": "1.8.0", + "version": "1.9.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55976,7 +55976,7 @@ }, "packages/plugins": { "name": "@wordpress/plugins", - "version": "6.15.0", + "version": "6.16.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -55998,7 +55998,7 @@ }, "packages/postcss-plugins-preset": { "name": "@wordpress/postcss-plugins-preset", - "version": "4.31.0", + "version": "4.32.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56014,7 +56014,7 @@ }, "packages/postcss-themes": { "name": "@wordpress/postcss-themes", - "version": "5.30.0", + "version": "5.31.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56026,7 +56026,7 @@ }, "packages/preferences": { "name": "@wordpress/preferences", - "version": "3.24.0", + "version": "3.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56048,7 +56048,7 @@ }, "packages/preferences-persistence": { "name": "@wordpress/preferences-persistence", - "version": "1.39.0", + "version": "1.40.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56060,7 +56060,7 @@ }, "packages/prettier-config": { "name": "@wordpress/prettier-config", - "version": "3.4.0", + "version": "3.5.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56072,7 +56072,7 @@ }, "packages/primitives": { "name": "@wordpress/primitives", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56085,7 +56085,7 @@ }, "packages/priority-queue": { "name": "@wordpress/priority-queue", - "version": "2.47.0", + "version": "2.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56097,7 +56097,7 @@ }, "packages/private-apis": { "name": "@wordpress/private-apis", - "version": "0.29.0", + "version": "0.30.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56108,7 +56108,7 @@ }, "packages/project-management-automation": { "name": "@wordpress/project-management-automation", - "version": "1.46.0", + "version": "1.47.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56121,7 +56121,7 @@ }, "packages/react-i18n": { "name": "@wordpress/react-i18n", - "version": "3.45.0", + "version": "3.46.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56269,7 +56269,7 @@ }, "packages/readable-js-assets-webpack-plugin": { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.30.0", + "version": "2.31.0", "dev": true, "license": "GPL-2.0-or-later", "engines": { @@ -56281,7 +56281,7 @@ }, "packages/redux-routine": { "name": "@wordpress/redux-routine", - "version": "4.47.0", + "version": "4.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56325,7 +56325,7 @@ }, "packages/reusable-blocks": { "name": "@wordpress/reusable-blocks", - "version": "4.24.0", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56351,7 +56351,7 @@ }, "packages/rich-text": { "name": "@wordpress/rich-text", - "version": "6.24.0", + "version": "6.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56375,7 +56375,7 @@ }, "packages/router": { "name": "@wordpress/router", - "version": "0.16.0", + "version": "0.17.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56393,7 +56393,7 @@ }, "packages/scripts": { "name": "@wordpress/scripts", - "version": "26.18.0", + "version": "26.19.0", "dev": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -56615,7 +56615,7 @@ }, "packages/server-side-render": { "name": "@wordpress/server-side-render", - "version": "4.24.0", + "version": "4.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56640,7 +56640,7 @@ }, "packages/shortcode": { "name": "@wordpress/shortcode", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56652,7 +56652,7 @@ }, "packages/style-engine": { "name": "@wordpress/style-engine", - "version": "1.30.0", + "version": "1.31.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56664,7 +56664,7 @@ }, "packages/stylelint-config": { "name": "@wordpress/stylelint-config", - "version": "21.30.0", + "version": "21.31.0", "dev": true, "license": "MIT", "dependencies": { @@ -56680,7 +56680,7 @@ }, "packages/sync": { "name": "@wordpress/sync", - "version": "0.9.0", + "version": "0.10.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56700,7 +56700,7 @@ }, "packages/token-list": { "name": "@wordpress/token-list", - "version": "2.47.0", + "version": "2.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" @@ -56711,7 +56711,7 @@ }, "packages/undo-manager": { "name": "@wordpress/undo-manager", - "version": "0.7.0", + "version": "0.8.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56723,7 +56723,7 @@ }, "packages/url": { "name": "@wordpress/url", - "version": "3.48.0", + "version": "3.49.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56735,7 +56735,7 @@ }, "packages/viewport": { "name": "@wordpress/viewport", - "version": "5.24.0", + "version": "5.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56752,7 +56752,7 @@ }, "packages/warning": { "name": "@wordpress/warning", - "version": "2.47.0", + "version": "2.48.0", "license": "GPL-2.0-or-later", "engines": { "node": ">=12" @@ -56760,7 +56760,7 @@ }, "packages/widgets": { "name": "@wordpress/widgets", - "version": "3.24.0", + "version": "3.25.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0", @@ -56784,7 +56784,7 @@ }, "packages/wordcount": { "name": "@wordpress/wordcount", - "version": "3.47.0", + "version": "3.48.0", "license": "GPL-2.0-or-later", "dependencies": { "@babel/runtime": "^7.16.0" diff --git a/packages/a11y/package.json b/packages/a11y/package.json index 2cf9d38e1c416d..399b5bd451455c 100644 --- a/packages/a11y/package.json +++ b/packages/a11y/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/a11y", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Accessibility (a11y) utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/annotations/package.json b/packages/annotations/package.json index 27dd6a9bf8a23b..01ac7d7961f260 100644 --- a/packages/annotations/package.json +++ b/packages/annotations/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/annotations", - "version": "2.48.0-prerelease", + "version": "2.48.0", "description": "Annotate content in the Gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/api-fetch/package.json b/packages/api-fetch/package.json index 90cdd359911432..be6d4adb079ee9 100644 --- a/packages/api-fetch/package.json +++ b/packages/api-fetch/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/api-fetch", - "version": "6.45.0-prerelease", + "version": "6.45.0", "description": "Utility to make WordPress REST API requests.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/autop/package.json b/packages/autop/package.json index d3cbd8c0b87792..8acd56c43fc8c1 100644 --- a/packages/autop/package.json +++ b/packages/autop/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/autop", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "WordPress's automatic paragraph functions `autop` and `removep`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-import-jsx-pragma/package.json b/packages/babel-plugin-import-jsx-pragma/package.json index 28fa650be4259b..e9f2b1b19596fa 100644 --- a/packages/babel-plugin-import-jsx-pragma/package.json +++ b/packages/babel-plugin-import-jsx-pragma/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-import-jsx-pragma", - "version": "4.31.0-prerelease", + "version": "4.31.0", "description": "Babel transform plugin for automatically injecting an import to be used as the pragma for the React JSX Transform plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-plugin-makepot/package.json b/packages/babel-plugin-makepot/package.json index 069d71f3dcc8f3..14d1364e72bf1c 100644 --- a/packages/babel-plugin-makepot/package.json +++ b/packages/babel-plugin-makepot/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-plugin-makepot", - "version": "5.32.0-prerelease", + "version": "5.32.0", "description": "WordPress Babel internationalization (i18n) plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/babel-preset-default/package.json b/packages/babel-preset-default/package.json index 884c1618082e3b..2461fb083c3fac 100644 --- a/packages/babel-preset-default/package.json +++ b/packages/babel-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/babel-preset-default", - "version": "7.32.0-prerelease", + "version": "7.32.0", "description": "Default Babel preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/base-styles/package.json b/packages/base-styles/package.json index 9b611219425ffa..bc5984014ee6ce 100644 --- a/packages/base-styles/package.json +++ b/packages/base-styles/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/base-styles", - "version": "4.39.0-prerelease", + "version": "4.39.0", "description": "Base SCSS utilities and variables for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blob/package.json b/packages/blob/package.json index f3bd5731306b45..b1bc807dfd0b40 100644 --- a/packages/blob/package.json +++ b/packages/blob/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blob", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Blob utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-directory/package.json b/packages/block-directory/package.json index dda633af694b22..2962204ac36e60 100644 --- a/packages/block-directory/package.json +++ b/packages/block-directory/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-directory", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Extend editor with block directory features to search, download and install blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-editor/package.json b/packages/block-editor/package.json index 8cb42b3e100555..1f9be9e2608b92 100644 --- a/packages/block-editor/package.json +++ b/packages/block-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-editor", - "version": "12.16.0-prerelease", + "version": "12.16.0", "description": "Generic block editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-library/package.json b/packages/block-library/package.json index 3fc1ad0881e910..30e341d08923a0 100644 --- a/packages/block-library/package.json +++ b/packages/block-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-library", - "version": "8.25.0-prerelease", + "version": "8.25.0", "description": "Block library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-default-parser/package.json b/packages/block-serialization-default-parser/package.json index ab434505b0370f..56a0c56269addc 100644 --- a/packages/block-serialization-default-parser/package.json +++ b/packages/block-serialization-default-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-default-parser", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/block-serialization-spec-parser/package.json b/packages/block-serialization-spec-parser/package.json index 2016f17d10bb87..cdd2884211afe4 100644 --- a/packages/block-serialization-spec-parser/package.json +++ b/packages/block-serialization-spec-parser/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/block-serialization-spec-parser", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "Block serialization specification parser for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/blocks/package.json b/packages/blocks/package.json index 09d7d3ac8da9bf..5f124f9d4e7d7d 100644 --- a/packages/blocks/package.json +++ b/packages/blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/blocks", - "version": "12.25.0-prerelease", + "version": "12.25.0", "description": "Block API for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/browserslist-config/package.json b/packages/browserslist-config/package.json index 11b202415b9a2c..815fe6eba00fe8 100644 --- a/packages/browserslist-config/package.json +++ b/packages/browserslist-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/browserslist-config", - "version": "5.31.0-prerelease", + "version": "5.31.0", "description": "WordPress Browserslist shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/commands/package.json b/packages/commands/package.json index 64ec865399d8cd..20fcea5c67b118 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/commands", - "version": "0.19.0-prerelease", + "version": "0.19.0", "description": "Handles the commands menu.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/components/package.json b/packages/components/package.json index 74a96368913b04..b7581679d90947 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/components", - "version": "25.14.0-prerelease", + "version": "25.14.0", "description": "UI components for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/compose/package.json b/packages/compose/package.json index 8648783bebb77a..0dec375ed22430 100644 --- a/packages/compose/package.json +++ b/packages/compose/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/compose", - "version": "6.25.0-prerelease", + "version": "6.25.0", "description": "WordPress higher-order components (HOCs).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-commands/package.json b/packages/core-commands/package.json index 224b8e3269cf26..a3e42ee2c236ad 100644 --- a/packages/core-commands/package.json +++ b/packages/core-commands/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-commands", - "version": "0.17.0-prerelease", + "version": "0.17.0", "description": "WordPress core reusable commands.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/core-data/package.json b/packages/core-data/package.json index 26f78d01c3d9b5..dfbdc2a073765a 100644 --- a/packages/core-data/package.json +++ b/packages/core-data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/core-data", - "version": "6.25.0-prerelease", + "version": "6.25.0", "description": "Access to and manipulation of core WordPress entities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-interactive-template/package.json b/packages/create-block-interactive-template/package.json index 749d6a36db3815..4b89f9d49ae837 100644 --- a/packages/create-block-interactive-template/package.json +++ b/packages/create-block-interactive-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-interactive-template", - "version": "1.11.0-prerelease", + "version": "1.11.0", "description": "Template for @wordpress/create-block to create interactive blocks with the Interactivity API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block-tutorial-template/package.json b/packages/create-block-tutorial-template/package.json index f562c560cb70c7..994294adf3a482 100644 --- a/packages/create-block-tutorial-template/package.json +++ b/packages/create-block-tutorial-template/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block-tutorial-template", - "version": "3.2.0-prerelease", + "version": "3.2.0", "description": "This is a template for @wordpress/create-block that creates an example 'Copyright Date' block. This block is used in the official WordPress block development Quick Start Guide.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/create-block/package.json b/packages/create-block/package.json index 8494abf2414c2d..32a04f4b9857e5 100644 --- a/packages/create-block/package.json +++ b/packages/create-block/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/create-block", - "version": "4.32.0-prerelease", + "version": "4.32.0", "description": "Generates PHP, JS and CSS code for registering a block for a WordPress plugin.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/customize-widgets/package.json b/packages/customize-widgets/package.json index ebafa5c8b00f86..88b32f5a23bec8 100644 --- a/packages/customize-widgets/package.json +++ b/packages/customize-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/customize-widgets", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Widgets blocks in Customizer Module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data-controls/package.json b/packages/data-controls/package.json index bec45518d12598..5018027820afe2 100644 --- a/packages/data-controls/package.json +++ b/packages/data-controls/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data-controls", - "version": "3.17.0-prerelease", + "version": "3.17.0", "description": "A set of common controls for the @wordpress/data api.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/data/package.json b/packages/data/package.json index ff1a267bff3703..37e9d8753cda02 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/data", - "version": "9.18.0-prerelease", + "version": "9.18.0", "description": "Data module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dataviews/package.json b/packages/dataviews/package.json index e3abb744db2482..fd506e6f821c40 100644 --- a/packages/dataviews/package.json +++ b/packages/dataviews/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dataviews", - "version": "0.2.0-prerelease", + "version": "0.2.0", "description": "DataViews is a component that provides an API to render datasets using different types of layouts (table, grid, list, etc.).", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/date/package.json b/packages/date/package.json index 2bd2bd469df0e2..65f346132f928c 100644 --- a/packages/date/package.json +++ b/packages/date/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/date", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "Date module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dependency-extraction-webpack-plugin/package.json b/packages/dependency-extraction-webpack-plugin/package.json index 38d05bbcb36f96..bdb5cfc1210715 100644 --- a/packages/dependency-extraction-webpack-plugin/package.json +++ b/packages/dependency-extraction-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dependency-extraction-webpack-plugin", - "version": "4.31.0-prerelease", + "version": "4.31.0", "description": "Extract WordPress script dependencies from webpack bundles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/deprecated/package.json b/packages/deprecated/package.json index 7161ed67c61785..b67260df2cfe36 100644 --- a/packages/deprecated/package.json +++ b/packages/deprecated/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/deprecated", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Deprecation utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/docgen/package.json b/packages/docgen/package.json index dca0e2431e85b0..55b7675309f4c6 100644 --- a/packages/docgen/package.json +++ b/packages/docgen/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/docgen", - "version": "1.57.0-prerelease", + "version": "1.57.0", "description": "Autogenerate public API documentation from exports and JSDoc comments.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom-ready/package.json b/packages/dom-ready/package.json index c01d3c4a7d36d1..f4716f6bce3056 100644 --- a/packages/dom-ready/package.json +++ b/packages/dom-ready/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom-ready", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Execute callback after the DOM is loaded.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/dom/package.json b/packages/dom/package.json index b0d5e52b155200..8d783a97255924 100644 --- a/packages/dom/package.json +++ b/packages/dom/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/dom", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "DOM utilities module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils-playwright/package.json b/packages/e2e-test-utils-playwright/package.json index 1c10d906a867d3..6f84aa51be554a 100644 --- a/packages/e2e-test-utils-playwright/package.json +++ b/packages/e2e-test-utils-playwright/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils-playwright", - "version": "0.16.0-prerelease", + "version": "0.16.0", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-test-utils/package.json b/packages/e2e-test-utils/package.json index 0f47cea5f35e5a..b95b6cfb58d36f 100644 --- a/packages/e2e-test-utils/package.json +++ b/packages/e2e-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-test-utils", - "version": "10.19.0-prerelease", + "version": "10.19.0", "description": "End-To-End (E2E) test utils for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/e2e-tests/package.json b/packages/e2e-tests/package.json index 9f37582eee4a68..e5f9dc46c683c0 100644 --- a/packages/e2e-tests/package.json +++ b/packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/e2e-tests", - "version": "7.19.0-prerelease", + "version": "7.19.0", "description": "End-To-End (E2E) tests for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-post/package.json b/packages/edit-post/package.json index 3556c10e61c998..1b3b107a83413f 100644 --- a/packages/edit-post/package.json +++ b/packages/edit-post/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-post", - "version": "7.25.0-prerelease", + "version": "7.25.0", "description": "Edit Post module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index fe030c77857102..e560e5e6827978 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-site", - "version": "5.25.0-prerelease", + "version": "5.25.0", "description": "Edit Site Page module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 5d6c1a71b6b588..1ef98df31269f6 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/edit-widgets", - "version": "5.25.0-prerelease", + "version": "5.25.0", "description": "Widgets Page module for WordPress..", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/editor/package.json b/packages/editor/package.json index 5114f29519172a..63656899e587c0 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/editor", - "version": "13.25.0-prerelease", + "version": "13.25.0", "description": "Enhanced block editor for WordPress posts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/element/package.json b/packages/element/package.json index 41574c5e3cfbed..6ce4714c25f0bf 100644 --- a/packages/element/package.json +++ b/packages/element/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/element", - "version": "5.25.0-prerelease", + "version": "5.25.0", "description": "Element React module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/env/package.json b/packages/env/package.json index b201792c1659a5..cd3fd075568e2e 100644 --- a/packages/env/package.json +++ b/packages/env/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/env", - "version": "9.0.0-prerelease", + "version": "9.0.0", "description": "A zero-config, self contained local WordPress environment for development and testing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/escape-html/package.json b/packages/escape-html/package.json index ee056314383099..2618d5818400c6 100644 --- a/packages/escape-html/package.json +++ b/packages/escape-html/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/escape-html", - "version": "2.48.0-prerelease", + "version": "2.48.0", "description": "Escape HTML utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index a7a16a0c52d755..ef0931dfb2c6c7 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/eslint-plugin", - "version": "17.5.0-prerelease", + "version": "17.5.0", "description": "ESLint plugin for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/format-library/package.json b/packages/format-library/package.json index 8427193345a1c7..d2ea9062cf79e3 100644 --- a/packages/format-library/package.json +++ b/packages/format-library/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/format-library", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Format library for the WordPress editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/hooks/package.json b/packages/hooks/package.json index 91d2bbe7d79b02..0c29dacb178950 100644 --- a/packages/hooks/package.json +++ b/packages/hooks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/hooks", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "WordPress hooks library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/html-entities/package.json b/packages/html-entities/package.json index 4bbd118210a262..92e85598707b90 100644 --- a/packages/html-entities/package.json +++ b/packages/html-entities/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/html-entities", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "HTML entity utilities for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index f0b77007903fad..7346330946e19d 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/i18n", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "WordPress internationalization (i18n) library.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/icons/package.json b/packages/icons/package.json index bf254491a65f58..31ebc3c57d1290 100644 --- a/packages/icons/package.json +++ b/packages/icons/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/icons", - "version": "9.39.0-prerelease", + "version": "9.39.0", "description": "WordPress Icons package, based on dashicon.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interactivity/package.json b/packages/interactivity/package.json index 53e67ff3b8fada..c856628c5d79e2 100644 --- a/packages/interactivity/package.json +++ b/packages/interactivity/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interactivity", - "version": "3.1.0-prerelease", + "version": "3.1.0", "description": "Package that provides a standard and simple way to handle the frontend interactivity of Gutenberg blocks.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/interface/package.json b/packages/interface/package.json index ff69679181fa74..df3d53990e0f59 100644 --- a/packages/interface/package.json +++ b/packages/interface/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/interface", - "version": "5.25.0-prerelease", + "version": "5.25.0", "description": "Interface module for WordPress. The package contains shared functionality across the modern JavaScript-based WordPress screens.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/is-shallow-equal/package.json b/packages/is-shallow-equal/package.json index 88e69d86eedd50..6969d6a2fe8a05 100644 --- a/packages/is-shallow-equal/package.json +++ b/packages/is-shallow-equal/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/is-shallow-equal", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "Test for shallow equality between two objects or arrays.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-console/package.json b/packages/jest-console/package.json index 088c9ef869cfe6..4a6f249a104d27 100644 --- a/packages/jest-console/package.json +++ b/packages/jest-console/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-console", - "version": "7.19.0-prerelease", + "version": "7.19.0", "description": "Custom Jest matchers for the Console object.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-preset-default/package.json b/packages/jest-preset-default/package.json index 9545c856501d74..7bda9f2e279f25 100644 --- a/packages/jest-preset-default/package.json +++ b/packages/jest-preset-default/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-preset-default", - "version": "11.19.0-prerelease", + "version": "11.19.0", "description": "Default Jest preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/jest-puppeteer-axe/package.json b/packages/jest-puppeteer-axe/package.json index e93fc9b21b8185..cdd09f7b791654 100644 --- a/packages/jest-puppeteer-axe/package.json +++ b/packages/jest-puppeteer-axe/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/jest-puppeteer-axe", - "version": "6.19.0-prerelease", + "version": "6.19.0", "description": "Axe API integration with Jest and Puppeteer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keyboard-shortcuts/package.json b/packages/keyboard-shortcuts/package.json index 001bf5d4673df3..af36a49371a0be 100644 --- a/packages/keyboard-shortcuts/package.json +++ b/packages/keyboard-shortcuts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keyboard-shortcuts", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Handling keyboard shortcuts.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/keycodes/package.json b/packages/keycodes/package.json index 18ca5c0c0ba525..ecebbb58eb99f4 100644 --- a/packages/keycodes/package.json +++ b/packages/keycodes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/keycodes", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Keycodes utilities for WordPress. Used to check for keyboard events across browsers/operating systems.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/lazy-import/package.json b/packages/lazy-import/package.json index 4d504ad140375d..b1967c0d4bedb0 100644 --- a/packages/lazy-import/package.json +++ b/packages/lazy-import/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/lazy-import", - "version": "1.35.0-prerelease", + "version": "1.35.0", "description": "Lazily import a module, installing it automatically if missing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/list-reusable-blocks/package.json b/packages/list-reusable-blocks/package.json index 849c17e2fdff96..bba7e2a7680e59 100644 --- a/packages/list-reusable-blocks/package.json +++ b/packages/list-reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/list-reusable-blocks", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Adding Export/Import support to the reusable blocks listing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/media-utils/package.json b/packages/media-utils/package.json index bf2f95dc0fbfdc..1086a79722cb06 100644 --- a/packages/media-utils/package.json +++ b/packages/media-utils/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/media-utils", - "version": "4.39.0-prerelease", + "version": "4.39.0", "description": "WordPress Media Upload Utils.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/notices/package.json b/packages/notices/package.json index 553a702a6b0028..7c40413adea511 100644 --- a/packages/notices/package.json +++ b/packages/notices/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/notices", - "version": "4.16.0-prerelease", + "version": "4.16.0", "description": "State management for notices.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/npm-package-json-lint-config/package.json b/packages/npm-package-json-lint-config/package.json index 8eebe21ebc6711..3bbc4c6c2e47fd 100644 --- a/packages/npm-package-json-lint-config/package.json +++ b/packages/npm-package-json-lint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/npm-package-json-lint-config", - "version": "4.33.0-prerelease", + "version": "4.33.0", "description": "WordPress npm-package-json-lint shareable configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/nux/package.json b/packages/nux/package.json index 3bb59d190f93f5..64511b2fa42d02 100644 --- a/packages/nux/package.json +++ b/packages/nux/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/nux", - "version": "8.10.0-prerelease", + "version": "8.10.0", "description": "NUX (New User eXperience) module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/patterns/package.json b/packages/patterns/package.json index 93986dc6d7305b..54ec5178d640d6 100644 --- a/packages/patterns/package.json +++ b/packages/patterns/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/patterns", - "version": "1.9.0-prerelease", + "version": "1.9.0", "description": "Management of user pattern editing.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/plugins/package.json b/packages/plugins/package.json index 5ef7609d832a83..b8b63a3c381d70 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/plugins", - "version": "6.16.0-prerelease", + "version": "6.16.0", "description": "Plugins module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-plugins-preset/package.json b/packages/postcss-plugins-preset/package.json index 78fe995fe80ae6..078c6b87a2ec16 100644 --- a/packages/postcss-plugins-preset/package.json +++ b/packages/postcss-plugins-preset/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-plugins-preset", - "version": "4.32.0-prerelease", + "version": "4.32.0", "description": "PostCSS sharable plugins preset for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/postcss-themes/package.json b/packages/postcss-themes/package.json index 51810de560248d..845911ad918e5d 100644 --- a/packages/postcss-themes/package.json +++ b/packages/postcss-themes/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/postcss-themes", - "version": "5.31.0-prerelease", + "version": "5.31.0", "description": "PostCSS plugin to generate theme colors.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences-persistence/package.json b/packages/preferences-persistence/package.json index 84217935b84de0..e17080383749e8 100644 --- a/packages/preferences-persistence/package.json +++ b/packages/preferences-persistence/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences-persistence", - "version": "1.40.0-prerelease", + "version": "1.40.0", "description": "Persistence utilities for `wordpress/preferences`.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/preferences/package.json b/packages/preferences/package.json index 8c46ee4d1c6644..dc44878577aaf4 100644 --- a/packages/preferences/package.json +++ b/packages/preferences/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/preferences", - "version": "3.25.0-prerelease", + "version": "3.25.0", "description": "Utilities for managing WordPress preferences.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/prettier-config/package.json b/packages/prettier-config/package.json index 8656f9390c98a5..047963ddde99b9 100644 --- a/packages/prettier-config/package.json +++ b/packages/prettier-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/prettier-config", - "version": "3.5.0-prerelease", + "version": "3.5.0", "description": "WordPress Prettier shared configuration.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/primitives/package.json b/packages/primitives/package.json index e5b1db342f8779..614e586c7efe27 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/primitives", - "version": "3.46.0-prerelease", + "version": "3.46.0", "description": "WordPress cross-platform primitives.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/priority-queue/package.json b/packages/priority-queue/package.json index eaecf6cc483659..4bfbb09da8d57c 100644 --- a/packages/priority-queue/package.json +++ b/packages/priority-queue/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/priority-queue", - "version": "2.48.0-prerelease", + "version": "2.48.0", "description": "Generic browser priority queue.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/private-apis/package.json b/packages/private-apis/package.json index 23a5e25a1aa3d0..654c53a02210e9 100644 --- a/packages/private-apis/package.json +++ b/packages/private-apis/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/private-apis", - "version": "0.30.0-prerelease", + "version": "0.30.0", "description": "Internal experimental APIs for WordPress core.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/project-management-automation/package.json b/packages/project-management-automation/package.json index 60aebb934cac27..5e3d1f85314e23 100644 --- a/packages/project-management-automation/package.json +++ b/packages/project-management-automation/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/project-management-automation", - "version": "1.47.0-prerelease", + "version": "1.47.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", diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json index f2cc6f2d9badc5..753f5138a2a0f9 100644 --- a/packages/react-i18n/package.json +++ b/packages/react-i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-i18n", - "version": "3.46.0-prerelease", + "version": "3.46.0", "description": "React bindings for @wordpress/i18n.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/readable-js-assets-webpack-plugin/package.json b/packages/readable-js-assets-webpack-plugin/package.json index ea88ce304d0ad3..d27c530ca15107 100644 --- a/packages/readable-js-assets-webpack-plugin/package.json +++ b/packages/readable-js-assets-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/readable-js-assets-webpack-plugin", - "version": "2.31.0-prerelease", + "version": "2.31.0", "description": "Generate a readable JS file for each JS asset.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/redux-routine/package.json b/packages/redux-routine/package.json index d4a1b05ef646fa..2bdfb5fe8bffdb 100644 --- a/packages/redux-routine/package.json +++ b/packages/redux-routine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/redux-routine", - "version": "4.48.0-prerelease", + "version": "4.48.0", "description": "Redux middleware for generator coroutines.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/reusable-blocks/package.json b/packages/reusable-blocks/package.json index 130a07e909bdc2..c6e6df921270ab 100644 --- a/packages/reusable-blocks/package.json +++ b/packages/reusable-blocks/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/reusable-blocks", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "Reusable blocks utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/rich-text/package.json b/packages/rich-text/package.json index 5792c4998e7265..645beb47bfa2a6 100644 --- a/packages/rich-text/package.json +++ b/packages/rich-text/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/rich-text", - "version": "6.25.0-prerelease", + "version": "6.25.0", "description": "Rich text value and manipulation API.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/router/package.json b/packages/router/package.json index 6c4a0328bfea5f..d9d640a9fe75c3 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/router", - "version": "0.17.0-prerelease", + "version": "0.17.0", "description": "Router API for WordPress pages.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/scripts/package.json b/packages/scripts/package.json index 3684b1ba788a6b..eabc1335aace53 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/scripts", - "version": "26.19.0-prerelease", + "version": "26.19.0", "description": "Collection of reusable scripts for WordPress development.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/server-side-render/package.json b/packages/server-side-render/package.json index 8b1d729d8e3e5b..c3e3bb66680e7a 100644 --- a/packages/server-side-render/package.json +++ b/packages/server-side-render/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/server-side-render", - "version": "4.25.0-prerelease", + "version": "4.25.0", "description": "The component used with WordPress to server-side render a preview of dynamic blocks to display in the editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/shortcode/package.json b/packages/shortcode/package.json index c2b13aa061131f..1e29a5e03343c2 100644 --- a/packages/shortcode/package.json +++ b/packages/shortcode/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/shortcode", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "Shortcode module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/style-engine/package.json b/packages/style-engine/package.json index a32fcd14f8b17a..d11bc37c122bc2 100644 --- a/packages/style-engine/package.json +++ b/packages/style-engine/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/style-engine", - "version": "1.31.0-prerelease", + "version": "1.31.0", "description": "A suite of parsers and compilers for WordPress styles.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/stylelint-config/package.json b/packages/stylelint-config/package.json index 3bab91b1932a60..ff3ed08296c5e9 100644 --- a/packages/stylelint-config/package.json +++ b/packages/stylelint-config/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/stylelint-config", - "version": "21.31.0-prerelease", + "version": "21.31.0", "description": "stylelint config for WordPress development.", "author": "The WordPress Contributors", "license": "MIT", diff --git a/packages/sync/package.json b/packages/sync/package.json index 6542c6d3bdd4be..7f3e20f6bd75ad 100644 --- a/packages/sync/package.json +++ b/packages/sync/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/sync", - "version": "0.10.0-prerelease", + "version": "0.10.0", "description": "Sync Data.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/token-list/package.json b/packages/token-list/package.json index 911e4181a57c71..3a877f9365d753 100644 --- a/packages/token-list/package.json +++ b/packages/token-list/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/token-list", - "version": "2.48.0-prerelease", + "version": "2.48.0", "description": "Constructable, plain JavaScript DOMTokenList implementation, supporting non-browser runtimes.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/undo-manager/package.json b/packages/undo-manager/package.json index 1a83e4d658ccf0..f1d47b82de47d8 100644 --- a/packages/undo-manager/package.json +++ b/packages/undo-manager/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/undo-manager", - "version": "0.8.0-prerelease", + "version": "0.8.0", "description": "A small package to manage undo/redo.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/url/package.json b/packages/url/package.json index 3f19518a41ecd8..62cb1727928b4f 100644 --- a/packages/url/package.json +++ b/packages/url/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/url", - "version": "3.49.0-prerelease", + "version": "3.49.0", "description": "WordPress URL utilities.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/viewport/package.json b/packages/viewport/package.json index ef4bd098e46be5..9251dabe8752d8 100644 --- a/packages/viewport/package.json +++ b/packages/viewport/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/viewport", - "version": "5.25.0-prerelease", + "version": "5.25.0", "description": "Viewport module for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/warning/package.json b/packages/warning/package.json index f5af9e94540f49..7a7e3958a922e5 100644 --- a/packages/warning/package.json +++ b/packages/warning/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/warning", - "version": "2.48.0-prerelease", + "version": "2.48.0", "description": "Warning utility for WordPress.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/widgets/package.json b/packages/widgets/package.json index 1983faf7822168..60d7c7c5c46968 100644 --- a/packages/widgets/package.json +++ b/packages/widgets/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/widgets", - "version": "3.25.0-prerelease", + "version": "3.25.0", "description": "Functionality used by the widgets block editor in the Widgets screen and the Customizer.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/wordcount/package.json b/packages/wordcount/package.json index 97fb14c25c9610..5e76d6ed042c6a 100644 --- a/packages/wordcount/package.json +++ b/packages/wordcount/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/wordcount", - "version": "3.48.0-prerelease", + "version": "3.48.0", "description": "WordPress word count utility.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 27b9a31f1cee3da002a543824d74f9e6c9adccbd Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 13 Dec 2023 11:46:12 +0100 Subject: [PATCH 155/325] SlotFill: Allow contextual SlotFillProviders (#56779) --- packages/block-editor/src/components/provider/index.js | 2 +- packages/components/CHANGELOG.md | 1 + packages/components/src/slot-fill/index.tsx | 7 +++++-- packages/components/src/slot-fill/types.ts | 5 +++++ 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/provider/index.js b/packages/block-editor/src/components/provider/index.js index 0fa3f042053d08..c3a87dfb5ff004 100644 --- a/packages/block-editor/src/components/provider/index.js +++ b/packages/block-editor/src/components/provider/index.js @@ -45,7 +45,7 @@ export const ExperimentalBlockEditorProvider = withRegistryProvider( useBlockSync( props ); return ( - <SlotFillProvider> + <SlotFillProvider passthrough> <KeyboardShortcuts.Register /> <BlockRefsProvider>{ children }</BlockRefsProvider> </SlotFillProvider> diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 01a31c58d01c81..659fafc9d1c5a2 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -19,6 +19,7 @@ - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). - `BorderControl`: adjust `BorderControlDropdown` Button size to fix misaligned border ([#56730](https://github.com/WordPress/gutenberg/pull/56730)). +- `SlotFillProvider`: Restore contextual Slot/Fills within SlotFillProvider ([#56779](https://github.com/WordPress/gutenberg/pull/56779)). ### Internal diff --git a/packages/components/src/slot-fill/index.tsx b/packages/components/src/slot-fill/index.tsx index fb1a08bc2207f9..b2df054973a5ba 100644 --- a/packages/components/src/slot-fill/index.tsx +++ b/packages/components/src/slot-fill/index.tsx @@ -55,9 +55,12 @@ export function UnforwardedSlot( } export const Slot = forwardRef( UnforwardedSlot ); -export function Provider( { children }: SlotFillProviderProps ) { +export function Provider( { + children, + passthrough = false, +}: SlotFillProviderProps ) { const parent = useContext( SlotFillContext ); - if ( ! parent.isDefault ) { + if ( ! parent.isDefault && passthrough ) { return <>{ children }</>; } return ( diff --git a/packages/components/src/slot-fill/types.ts b/packages/components/src/slot-fill/types.ts index 8abb9b941c527c..f3a8f2255f2874 100644 --- a/packages/components/src/slot-fill/types.ts +++ b/packages/components/src/slot-fill/types.ts @@ -96,6 +96,11 @@ export type SlotFillProviderProps = { * The children elements. */ children: ReactNode; + + /** + * Whether to pass slots to the parent provider if existent. + */ + passthrough?: boolean; }; export type SlotFillBubblesVirtuallySlotRef = RefObject< HTMLElement >; From 5ae7946e0589cae51e15fe03ae56805a10bc926a Mon Sep 17 00:00:00 2001 From: Dave Smith <getdavemail@gmail.com> Date: Wed, 13 Dec 2023 10:50:08 +0000 Subject: [PATCH 156/325] Add basic test coverage for Navigation Menu editing mode (#56871) * Smoke test for key behaviours * Update test/e2e/specs/site-editor/navigation-editor.spec.js Co-authored-by: Ben Dwyer <ben@scruffian.com> * Use assertion * Prefer role selector * Use steps to break up test --------- Co-authored-by: Andrei Draganescu <me@andreidraganescu.info> Co-authored-by: Ben Dwyer <ben@scruffian.com> --- .../site-editor/navigation-editor.spec.js | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 test/e2e/specs/site-editor/navigation-editor.spec.js diff --git a/test/e2e/specs/site-editor/navigation-editor.spec.js b/test/e2e/specs/site-editor/navigation-editor.spec.js new file mode 100644 index 00000000000000..3ac72ac3b78d60 --- /dev/null +++ b/test/e2e/specs/site-editor/navigation-editor.spec.js @@ -0,0 +1,155 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Editing Navigation Menus', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllMenus(); + } ); + + test( 'it should lock the root Navigation block in the editor', async ( { + admin, + page, + pageUtils, + requestUtils, + editor, + } ) => { + await test.step( 'Manually browse to focus mode for a Navigation Menu', async () => { + // We could Navigate directly to editing the Navigation Menu but we intentionally do not do this. + // + // Why? To provide coverage for a bug that caused the Navigation Editor behaviours to fail + // only when navigating through the editor screens (rather than going directly to the editor by URL). + // See: https://github.com/WordPress/gutenberg/pull/56856. + // + // Example (what we could do): + // await admin.visitSiteEditor( { + // postId: createdMenu?.id, + // postType: 'wp_navigation', + // } ); + // + await admin.visitSiteEditor(); + + // create a Navigation Menu called "Test Menu" using the REST API helpers + const createdMenu = await requestUtils.createNavigationMenu( { + title: 'Primary Menu', + content: + '<!-- wp:navigation-link {"label":"WordPress","type":"custom","url":"http://www.wordpress.org/","kind":"custom"} /-->', + } ); + + // Add another so we get a list of Navigation menus in the editor. + await requestUtils.createNavigationMenu( { + title: 'Another One', + content: + '<!-- wp:navigation-link {"label":"Another Item","type":"custom","url":"http://www.wordpress.org/","kind":"custom"} /-->', + } ); + + const editorSidebar = page.getByRole( 'region', { + name: 'Navigation', + } ); + + await editorSidebar + .getByRole( 'button', { + name: 'Navigation', + } ) + .click(); + + // Wait for list of Navigations to appear. + await expect( + editorSidebar.getByRole( 'heading', { + name: 'Navigation', + level: 1, + } ) + ).toBeVisible(); + + await editorSidebar + .getByRole( 'button', { + name: 'Primary Menu', + } ) + .click(); + + await expect( page ).toHaveURL( + `wp-admin/site-editor.php?postId=${ createdMenu?.id }&postType=wp_navigation` + ); + + // Wait for list of Navigations to appear. + editorSidebar.getByRole( 'heading', { + name: 'Primary Menu', + level: 1, + } ); + + // Switch to editing the Navigation Menu + await editorSidebar + .getByRole( 'link', { + name: 'Edit', + } ) + .click(); + } ); + + await test.step( 'Check Navigation block is present and locked', async () => { + // Open List View. + await pageUtils.pressKeys( 'access+o' ); + + const listView = page + .getByRole( 'region', { + name: 'List View', + } ) + .getByRole( 'treegrid', { + name: 'Block navigation structure', + } ); + + await expect( listView ).toBeVisible(); + + const navBlockNode = listView.getByRole( 'link', { + name: 'Navigation (locked)', + exact: true, + } ); + + // The Navigation block should be present and locked. + await expect( navBlockNode ).toBeVisible(); + + // The block should have no options menu. + await expect( + listView.getByRole( 'button', { + name: 'Options for Navigation', + exact: true, + } ) + ).toBeHidden(); + + // Select the Navigation block. + await navBlockNode.click(); + } ); + + await test.step( 'Check Navigation block has no controls other than editable list view', async () => { + // Open the document settings sidebar + await editor.openDocumentSettingsSidebar(); + + const sidebar = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + await expect( sidebar ).toBeVisible(); + + // Check that the `Menu` control is visible. + // This is effectively the contents of the "List View" tab. + await expect( + sidebar.getByRole( 'heading', { name: 'Menu', exact: true } ) + ).toBeVisible(); + + // Check the standard tabs are not present. + await expect( + sidebar.getByRole( 'tab', { name: 'List View' } ) + ).toBeHidden(); + await expect( + sidebar.getByRole( 'tab', { name: 'Settings' } ) + ).toBeHidden(); + await expect( + sidebar.getByRole( 'tab', { name: 'Styles' } ) + ).toBeHidden(); + } ); + } ); +} ); From 3444583734a6d902ee2d011a137493ac5af284af Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Wed, 13 Dec 2023 10:51:15 +0000 Subject: [PATCH 157/325] remove links to excalidraw diagrams from images (#56980) --- docs/getting-started/fundamentals/block-json.md | 10 ++++++++-- .../fundamentals/file-structure-of-a-block.md | 9 ++++----- .../fundamentals/javascript-in-the-block-editor.md | 3 ++- .../fundamentals/registration-of-a-block.md | 3 ++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md index 5959547804290a..1f572a12598136 100644 --- a/docs/getting-started/fundamentals/block-json.md +++ b/docs/getting-started/fundamentals/block-json.md @@ -2,7 +2,7 @@ The `block.json` file simplifies the processs of defining and registering a block by using the same block's definition in JSON format to register the block in both the server and the client. -[![Open block.json diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-json.png)](https://excalidraw.com/#json=v1GrIkGsYGKv8P14irBy6,Yy0vl8q7DTTL2VsH5Ww27A "Open block.json diagram in excalidraw") +[![Open block.json diagram image](https://developer.wordpress.org/files/2023/11/block-json.png)](https://developer.wordpress.org/files/2023/11/block-json.png "Open block.json diagram image") <div class="callout callout-tip"> Click <a href="https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd">here</a> to see a full block example and check <a href="https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-supports-6aa4dd/src/block.json">its <code>block.json</code></a> @@ -79,7 +79,7 @@ _See how the attributes are passed to the [`Edit` component](https://github.com/ Check the <a href="https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/"> <code>attributes</code> </a> reference page for full info about the Attributes API. </div> -[![Open Attributes diagram in excalidraw](https://developer.wordpress.org/files/2023/11/attributes.png)](https://excalidraw.com/#json=pSgCZy8q9GbH7r0oz2fL1,MFCLd6ddQHqi_UqNp5ZSgg "Open Attributes diagram in excalidraw") +[![Open Attributes diagram image](https://developer.wordpress.org/files/2023/11/attributes.png)](https://developer.wordpress.org/files/2023/11/attributes.png "Open Attributes diagram image") ## Enable UI settings panels for the block with `supports` @@ -113,3 +113,9 @@ _See the [full block example](https://github.com/WordPress/block-development-exa <div class="callout callout-info"> Check the <a href="https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/"> <code>supports</code> </a> reference page for full info about the Supports API. </div> + + +## Additional resources + +- [block.json diagram](https://excalidraw.com/#json=v1GrIkGsYGKv8P14irBy6,Yy0vl8q7DTTL2VsH5Ww27A) +- [Attributes diagram](https://excalidraw.com/#json=pSgCZy8q9GbH7r0oz2fL1,MFCLd6ddQHqi_UqNp5ZSgg) \ No newline at end of file diff --git a/docs/getting-started/fundamentals/file-structure-of-a-block.md b/docs/getting-started/fundamentals/file-structure-of-a-block.md index 660c7aa8aff8b8..72531ccfb2b272 100644 --- a/docs/getting-started/fundamentals/file-structure-of-a-block.md +++ b/docs/getting-started/fundamentals/file-structure-of-a-block.md @@ -4,7 +4,9 @@ It is recommended to **register blocks within plugins** to ensure they stay avai The files generated by `create-block` are a good reference of the files that can be involved in the definition and registration of a block. -[![Open File Structure of a Block Diagram in excalidraw](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw "Open File Structure of a Block Diagram in excalidraw") +[![Open File Structure of a Block diagram image](https://developer.wordpress.org/files/2023/11/file-structure-block.png)](https://developer.wordpress.org/files/2023/11/file-structure-block.png "Open File Structure of a Block diagram image") + +## Folders and files involved in a block's definition and registration ### `<plugin-file>.php` @@ -80,7 +82,4 @@ In a standard project, the `build` folder contains the generated files in [the b ## Additional resources -- [Metadata in block.json](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/) -- [`wp-scripts build`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#build) -- [`wp-scripts start`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-scripts/#start) -- [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog +- [File Structure of a Block diagram](https://excalidraw.com/#json=YYpeR-kY1ZMhFKVZxGhMi,mVZewfwNAh_oL-7bj4gmdw) diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 615f7f74ce151a..9dc542a5a24c9d 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -15,7 +15,7 @@ Browsers cannot interpret or run ESNext and JSX syntaxes, so a transformation st Among other things, with `wp-scripts` package you can use Javascript modules to distribute your code among different files and get a few bundled files at the end of the build process (see [example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/data-basics-59c8f8)). -[![Build Process Diagram](https://developer.wordpress.org/files/2023/11/build-process.png)](https://excalidraw.com/#json=4aNG9JUti3pMnsfoga35b,ihEAI8p5dwkpjWr6gQmjuw "Open Build Process Diagram in Excalidraw") +[![Open Build Process diagram image](https://developer.wordpress.org/files/2023/11/build-process.png)](https://developer.wordpress.org/files/2023/11/build-process.png "Open Build Process diagram image") With the [proper `package.json` scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/#basic-usage) you can launch the build process with `wp-scripts` in production and development mode: @@ -49,3 +49,4 @@ Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/ho - [block-development-examples](https://github.com/WordPress/block-development-examples) | GitHub repository - [block-theme-examples](https://github.com/wptrainingteam/block-theme-examples) | GitHub repository - [How webpack and WordPress packages interact](https://developer.wordpress.org/news/2023/04/how-webpack-and-wordpress-packages-interact/) | Developer Blog +- [Build Process Diagram](https://excalidraw.com/#json=4aNG9JUti3pMnsfoga35b,ihEAI8p5dwkpjWr6gQmjuw) diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index a330d46e676d5c..28e1618605a200 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -6,7 +6,7 @@ Although technically, blocks could be registered only in the client, **registeri For example, to allow a block [to be styled via `theme.json`](https://developer.wordpress.org/themes/global-settings-and-styles/settings/blocks/), it needs to be registered on the server, otherwise, any styles assigned to it in `theme.json` will be ignored. -[![Open Block Registration diagram in excalidraw](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://excalidraw.com/#json=PUQu7jpvbKsUHYfpHWn7s,61QnhpZtjykp3s44lbUN_g "Open Block Registration diagram in excalidraw") +[![Open Block Registration diagram image](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png "Open Block Registration diagram image") ### Registration of the block with PHP (server-side) @@ -96,3 +96,4 @@ _See the [code above](https://github.com/WordPress/block-development-examples/bl - [`register_block_type` PHP function](https://developer.wordpress.org/reference/functions/register_block_type/) - [`registerBlockType` JS function](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) - [Why a block needs to be registered in both the server and the client?](https://github.com/WordPress/gutenberg/discussions/55884) | GitHub Discussion +- [Block Registration diagram](https://excalidraw.com/#json=PUQu7jpvbKsUHYfpHWn7s,61QnhpZtjykp3s44lbUN_g) From 029e90a4e270b9cba3c5515fde0c777a5f58f94b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 13 Dec 2023 12:53:57 +0100 Subject: [PATCH 158/325] DataViews: close menu upon switching layouts (#57015) --- packages/dataviews/src/view-actions.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index a5330c08f299ce..628ded53f169d7 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -60,9 +60,7 @@ function ViewTypeMenu( { view, onChangeView, supportedLayouts } ) { <Icon icon={ check } /> ) } - onSelect={ ( event ) => { - // We need to handle this on DropDown component probably.. - event.preventDefault(); + onSelect={ () => { onChangeView( { ...view, type: availableView.type, From dc4f3e6ea11b89304de6c544594d176fe9ba9d4d Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Wed, 13 Dec 2023 12:56:04 +0100 Subject: [PATCH 159/325] Navigator: use CSS animations instead of framer-motion (#56909) * Move navigator provider styles to separate file * Move navigator screen styles to separate file, use CSS animations instead of framer motion * Remove unused import * Spacing * Use standard ease-in-out easing function * Remove stale comments * Remove animation-specific tests (as they can't be tested in jsdom) * CHANGELOG * Add comment * Avoid running the `css` function when unnecessary --- packages/components/CHANGELOG.md | 1 + .../navigator-provider/component.tsx | 7 +- .../navigator/navigator-screen/component.tsx | 108 +++--------------- packages/components/src/navigator/styles.ts | 71 ++++++++++++ .../components/src/navigator/test/index.tsx | 64 ----------- 5 files changed, 90 insertions(+), 161 deletions(-) create mode 100644 packages/components/src/navigator/styles.ts diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 659fafc9d1c5a2..702aabb14a59d9 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,7 @@ ### Enhancements +- `Navigator`: use vanilla CSS animations instead of `framer-motion` ([#56909](https://github.com/WordPress/gutenberg/pull/56909)). - `FormToggle`: fix sass deprecation warning ([#56672](https://github.com/WordPress/gutenberg/pull/56672)). - `QueryControls`: Add opt-in prop for 40px default size ([#56576](https://github.com/WordPress/gutenberg/pull/56576)). - `CheckboxControl`: Add option to not render label ([#56158](https://github.com/WordPress/gutenberg/pull/56158)). diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index cccbb84f0d093a..cd38bea5748134 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -2,7 +2,6 @@ * External dependencies */ import type { ForwardedRef } from 'react'; -import { css } from '@emotion/react'; /** * WordPress dependencies @@ -23,15 +22,16 @@ import isShallowEqual from '@wordpress/is-shallow-equal'; import type { WordPressComponentProps } from '../../context'; import { contextConnect, useContextSystem } from '../../context'; import { useCx } from '../../utils/hooks/use-cx'; +import { patternMatch, findParent } from '../utils/router'; import { View } from '../../view'; import { NavigatorContext } from '../context'; +import * as styles from '../styles'; import type { NavigatorProviderProps, NavigatorLocation, NavigatorContext as NavigatorContextType, Screen, } from '../types'; -import { patternMatch, findParent } from '../utils/router'; type MatchedPath = ReturnType< typeof patternMatch >; type ScreenAction = { type: string; screen: Screen }; @@ -248,8 +248,7 @@ function UnconnectedNavigatorProvider( const cx = useCx(); const classes = useMemo( - // Prevents horizontal overflow while animating screen transitions. - () => cx( css( { overflowX: 'hidden' } ), className ), + () => cx( styles.navigatorProviderWrapper, className ), [ className, cx ] ); diff --git a/packages/components/src/navigator/navigator-screen/component.tsx b/packages/components/src/navigator/navigator-screen/component.tsx index ed4ab9629d3a8d..29981d46770eed 100644 --- a/packages/components/src/navigator/navigator-screen/component.tsx +++ b/packages/components/src/navigator/navigator-screen/component.tsx @@ -2,11 +2,6 @@ * External dependencies */ import type { ForwardedRef } from 'react'; -// eslint-disable-next-line no-restricted-imports -import type { MotionProps } from 'framer-motion'; -// eslint-disable-next-line no-restricted-imports -import { motion } from 'framer-motion'; -import { css } from '@emotion/react'; /** * WordPress dependencies @@ -19,8 +14,8 @@ import { useRef, useId, } from '@wordpress/element'; -import { useReducedMotion, useMergeRefs } from '@wordpress/compose'; -import { isRTL } from '@wordpress/i18n'; +import { useMergeRefs } from '@wordpress/compose'; +import { isRTL as isRTLFn } from '@wordpress/i18n'; import { escapeAttribute } from '@wordpress/escape-html'; /** @@ -31,22 +26,11 @@ import { contextConnect, useContextSystem } from '../../context'; import { useCx } from '../../utils/hooks/use-cx'; import { View } from '../../view'; import { NavigatorContext } from '../context'; +import * as styles from '../styles'; import type { NavigatorScreenProps } from '../types'; -const animationEnterDelay = 0; -const animationEnterDuration = 0.14; -const animationExitDuration = 0.14; -const animationExitDelay = 0; - -// Props specific to `framer-motion` can't be currently passed to `NavigatorScreen`, -// as some of them would overlap with HTML props (e.g. `onAnimationStart`, ...) -type Props = Omit< - WordPressComponentProps< NavigatorScreenProps, 'div', false >, - Exclude< keyof MotionProps, 'style' | 'children' > ->; - function UnconnectedNavigatorScreen( - props: Props, + props: WordPressComponentProps< NavigatorScreenProps, 'div', false >, forwardedRef: ForwardedRef< any > ) { const screenId = useId(); @@ -55,7 +39,6 @@ function UnconnectedNavigatorScreen( 'NavigatorScreen' ); - const prefersReducedMotion = useReducedMotion(); const { location, match, addScreen, removeScreen } = useContext( NavigatorContext ); const isMatch = match === screenId; @@ -70,19 +53,20 @@ function UnconnectedNavigatorScreen( return () => removeScreen( screen ); }, [ screenId, path, addScreen, removeScreen ] ); + const isRTL = isRTLFn(); + const { isInitial, isBack } = location; const cx = useCx(); const classes = useMemo( () => cx( - css( { - // Ensures horizontal overflow is visually accessible. - overflowX: 'auto', - // In case the root has a height, it should not be exceeded. - maxHeight: '100%', + styles.navigatorScreen( { + isInitial, + isBack, + isRTL, } ), className ), - [ className, cx ] + [ className, cx, isInitial, isBack, isRTL ] ); const locationRef = useRef( location ); @@ -149,73 +133,11 @@ function UnconnectedNavigatorScreen( const mergedWrapperRef = useMergeRefs( [ forwardedRef, wrapperRef ] ); - if ( ! isMatch ) { - return null; - } - - if ( prefersReducedMotion ) { - return ( - <View - ref={ mergedWrapperRef } - className={ classes } - { ...otherProps } - > - { children } - </View> - ); - } - - const animate = { - opacity: 1, - transition: { - delay: animationEnterDelay, - duration: animationEnterDuration, - ease: 'easeInOut', - }, - x: 0, - }; - // Disable the initial animation if the screen is the very first screen to be - // rendered within the current `NavigatorProvider`. - const initial = - location.isInitial && ! location.isBack - ? false - : { - opacity: 0, - x: - ( isRTL() && location.isBack ) || - ( ! isRTL() && ! location.isBack ) - ? 50 - : -50, - }; - const exit = { - delay: animationExitDelay, - opacity: 0, - x: - ( ! isRTL() && location.isBack ) || ( isRTL() && ! location.isBack ) - ? 50 - : -50, - transition: { - duration: animationExitDuration, - ease: 'easeInOut', - }, - }; - - const animatedProps = { - animate, - exit, - initial, - }; - - return ( - <motion.div - ref={ mergedWrapperRef } - className={ classes } - { ...otherProps } - { ...animatedProps } - > + return isMatch ? ( + <View ref={ mergedWrapperRef } className={ classes } { ...otherProps }> { children } - </motion.div> - ); + </View> + ) : null; } /** diff --git a/packages/components/src/navigator/styles.ts b/packages/components/src/navigator/styles.ts new file mode 100644 index 00000000000000..8ec5f11da16d3a --- /dev/null +++ b/packages/components/src/navigator/styles.ts @@ -0,0 +1,71 @@ +/** + * External dependencies + */ +import { css, keyframes } from '@emotion/react'; + +export const navigatorProviderWrapper = css` + /* Prevents horizontal overflow while animating screen transitions */ + overflow-x: hidden; + /* Mark this subsection of the DOM as isolated, providing performance benefits + * by limiting calculations of layout, style, paint, size, or any combination + * to a DOM subtree rather than the entire page. + */ + contain: strict; +`; + +const fadeInFromRight = keyframes( { + '0%': { + opacity: 0, + transform: `translateX( 50px )`, + }, + '100%': { opacity: 1, transform: 'none' }, +} ); + +const fadeInFromLeft = keyframes( { + '0%': { + opacity: 0, + transform: `translateX( -50px )`, + }, + '100%': { opacity: 1, transform: 'none' }, +} ); + +type NavigatorScreenAnimationProps = { + isInitial?: boolean; + isBack?: boolean; + isRTL: boolean; +}; + +const navigatorScreenAnimation = ( { + isInitial, + isBack, + isRTL, +}: NavigatorScreenAnimationProps ) => { + if ( isInitial && ! isBack ) { + return; + } + + const animationName = + ( isRTL && isBack ) || ( ! isRTL && ! isBack ) + ? fadeInFromRight + : fadeInFromLeft; + + return css` + animation-duration: 0.14s; + animation-timing-function: ease-in-out; + will-change: transform, opacity; + animation-name: ${ animationName }; + + @media ( prefers-reduced-motion ) { + animation-duration: 0s; + } + `; +}; + +export const navigatorScreen = ( props: NavigatorScreenAnimationProps ) => css` + /* Ensures horizontal overflow is visually accessible */ + overflow-x: auto; + /* In case the root has a height, it should not be exceeded */ + max-height: 100%; + + ${ navigatorScreenAnimation( props ) } +`; diff --git a/packages/components/src/navigator/test/index.tsx b/packages/components/src/navigator/test/index.tsx index 5a711b8730224a..b83bd70d9d7444 100644 --- a/packages/components/src/navigator/test/index.tsx +++ b/packages/components/src/navigator/test/index.tsx @@ -769,68 +769,4 @@ describe( 'Navigator', () => { ).toHaveFocus(); } ); } ); - - describe( 'animation', () => { - it( 'should not animate the initial screen', async () => { - const onHomeAnimationStartSpy = jest.fn(); - - render( - <NavigatorProvider initialPath="/"> - <NavigatorScreen - path="/" - onAnimationStart={ onHomeAnimationStartSpy } - > - <CustomNavigatorButton path="/child"> - To child - </CustomNavigatorButton> - </NavigatorScreen> - </NavigatorProvider> - ); - - expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled(); - } ); - - it( 'should animate all other screens (including the initial screen when navigating back)', async () => { - const user = userEvent.setup(); - - const onHomeAnimationStartSpy = jest.fn(); - const onChildAnimationStartSpy = jest.fn(); - - render( - <NavigatorProvider initialPath="/"> - <NavigatorScreen - path="/" - onAnimationStart={ onHomeAnimationStartSpy } - > - <CustomNavigatorButton path="/child"> - To child - </CustomNavigatorButton> - </NavigatorScreen> - <NavigatorScreen - path="/child" - onAnimationStart={ onChildAnimationStartSpy } - > - <CustomNavigatorBackButton> - Back to home - </CustomNavigatorBackButton> - </NavigatorScreen> - </NavigatorProvider> - ); - - expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled(); - expect( onChildAnimationStartSpy ).not.toHaveBeenCalled(); - - await user.click( - screen.getByRole( 'button', { name: 'To child' } ) - ); - expect( onChildAnimationStartSpy ).toHaveBeenCalledTimes( 1 ); - expect( onHomeAnimationStartSpy ).not.toHaveBeenCalled(); - - await user.click( - screen.getByRole( 'button', { name: 'Back to home' } ) - ); - expect( onChildAnimationStartSpy ).toHaveBeenCalledTimes( 1 ); - expect( onHomeAnimationStartSpy ).toHaveBeenCalledTimes( 1 ); - } ); - } ); } ); From cb9cc9c1a01a00ed231746a5c6f8c8d7ceeb4d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:13:47 +0100 Subject: [PATCH 160/325] DataViews: add list layout to templates (#57014) Co-authored-by: James Koster <james@jameskoster.co.uk> --- packages/dataviews/src/style.scss | 1 + packages/dataviews/src/view-list.js | 2 +- .../src/components/layout/style.scss | 10 +- .../src/components/page-pages/index.js | 4 +- .../page-templates/dataviews-templates.js | 111 +++++++++++++----- .../side-editor.js => post-preview/index.js} | 2 +- 6 files changed, 92 insertions(+), 38 deletions(-) rename packages/edit-site/src/components/{page-pages/side-editor.js => post-preview/index.js} (79%) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 3ff82d32382661..e7e06e1acd27e4 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -196,6 +196,7 @@ border-radius: $grid-unit-05; overflow: hidden; position: relative; + margin-top: $grid-unit-05; &::after { content: ""; diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index 516264946d1f67..c50a0f1ca2682c 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -64,7 +64,7 @@ export default function ViewList( { ) } onClick={ () => onSelectionChange( [ item ] ) } > - <HStack spacing={ 3 }> + <HStack spacing={ 3 } alignment="flex-start"> <div className="dataviews-list-view__media-wrapper"> { mediaField?.render( { item } ) || ( <div className="dataviews-list-view__media-placeholder"></div> diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 3bea97862b1c4c..8e60397f6bdf8d 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -156,14 +156,20 @@ } } +.edit-site-template-pages-list-view { + max-width: $nav-sidebar-width; +} + // This shouldn't be necessary (we should have a way to say that a skeletton is relative .edit-site-layout__canvas .interface-interface-skeleton, -.edit-site-page-pages-preview .interface-interface-skeleton { +.edit-site-page-pages-preview .interface-interface-skeleton, +.edit-site-template-pages-preview .interface-interface-skeleton { position: relative !important; min-height: 100% !important; } -.edit-site-page-pages-preview { +.edit-site-page-pages-preview, +.edit-site-template-pages-preview { height: 100%; } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index c92ce35ebe46dc..861aa48fcd5cb6 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -37,7 +37,7 @@ import { viewPostAction, useEditPostAction, } from '../actions'; -import SideEditor from './side-editor'; +import PostPreview from '../post-preview'; import Media from '../media'; import { unlock } from '../../lock-unlock'; const { useLocation } = unlock( routerPrivateApis ); @@ -338,7 +338,7 @@ export default function PagePages() { <Page> <div className="edit-site-page-pages-preview"> { pageId !== null ? ( - <SideEditor + <PostPreview postId={ pageId } postType={ postType } /> diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js index 07de0cb73ff445..1121eeb3daa5d8 100644 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ b/packages/edit-site/src/components/page-templates/dataviews-templates.js @@ -14,7 +14,7 @@ import { __experimentalVStack as VStack, VisuallyHidden, } from '@wordpress/components'; -import { __, _x } from '@wordpress/i18n'; +import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; @@ -38,6 +38,7 @@ import { OPERATOR_NOT_IN, LAYOUT_GRID, LAYOUT_TABLE, + LAYOUT_LIST, } from '../../utils/constants'; import { useResetTemplateAction, @@ -46,6 +47,7 @@ import { } from './template-actions'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; +import PostPreview from '../post-preview'; const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis @@ -59,6 +61,10 @@ const defaultConfigPerViewType = { mediaField: 'preview', primaryField: 'title', }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'preview', + }, }; const DEFAULT_VIEW = { @@ -77,10 +83,16 @@ function normalizeSearchInput( input = '' ) { return removeAccents( input.trim().toLowerCase() ); } -// TODO: these are going to be reused in the template part list. -// That's the reason for leaving the template parts code for now. -function TemplateTitle( { item } ) { - const { isCustomized } = useAddedBy( item.type, item.id ); +function TemplateTitle( { item, view } ) { + if ( view.type === LAYOUT_LIST ) { + return ( + <> + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + </> + ); + } + return ( <VStack spacing={ 1 }> <View as="span" className="edit-site-list-title__customized-info"> @@ -95,24 +107,18 @@ function TemplateTitle( { item } ) { __( '(no title)' ) } </Link> </View> - { isCustomized && ( - <span className="edit-site-list-added-by__customized-info"> - { item.type === TEMPLATE_POST_TYPE - ? _x( 'Customized', 'template' ) - : _x( 'Customized', 'template part' ) } - </span> - ) } </VStack> ); } -function AuthorField( { item } ) { +function AuthorField( { item, view } ) { const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + const withIcon = view.type !== LAYOUT_LIST; + return ( <HStack alignment="left" spacing={ 1 }> - { imageUrl ? ( - <AvatarImage imageUrl={ imageUrl } /> - ) : ( + { withIcon && imageUrl && <AvatarImage imageUrl={ imageUrl } /> } + { withIcon && ! imageUrl && ( <div className="edit-site-list-added-by__icon"> <Icon icon={ icon } /> </div> @@ -151,12 +157,16 @@ function TemplatePreview( { content, viewType } ) { } export default function DataviewsTemplates() { + const [ templateId, setTemplateId ] = useState( null ); const [ view, setView ] = useState( DEFAULT_VIEW ); const { records: allTemplates, isResolving: isLoadingData } = useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); + const onSelectionChange = ( items ) => + setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); + const authors = useMemo( () => { if ( ! allTemplates ) { return EMPTY_ARRAY; @@ -192,7 +202,9 @@ export default function DataviewsTemplates() { header: __( 'Template' ), id: 'title', getValue: ( { item } ) => item.title?.rendered || item.slug, - render: ( { item } ) => <TemplateTitle item={ item } />, + render: ( { item } ) => ( + <TemplateTitle item={ item } view={ view } /> + ), maxWidth: 400, enableHiding: false, }, @@ -222,7 +234,7 @@ export default function DataviewsTemplates() { id: 'author', getValue: ( { item } ) => item.author_text, render: ( { item } ) => { - return <AuthorField item={ item } />; + return <AuthorField view={ view } item={ item } />; }, enableHiding: false, type: ENUMERATION_TYPE, @@ -342,19 +354,54 @@ export default function DataviewsTemplates() { [ view, setView ] ); return ( - <Page title={ __( 'Templates' ) }> - <DataViews - paginationInfo={ paginationInfo } - fields={ fields } - actions={ actions } - data={ shownTemplates } - getItemId={ ( item ) => item.id } - isLoading={ isLoadingData } - view={ view } - onChangeView={ onChangeView } - supportedLayouts={ [ LAYOUT_TABLE, LAYOUT_GRID ] } - deferredRendering={ ! view.hiddenFields?.includes( 'preview' ) } - /> - </Page> + <> + <Page + className={ + view.type === LAYOUT_LIST + ? 'edit-site-template-pages-list-view' + : null + } + title={ __( 'Templates' ) } + > + <DataViews + paginationInfo={ paginationInfo } + fields={ fields } + actions={ actions } + data={ shownTemplates } + getItemId={ ( item ) => item.id } + isLoading={ isLoadingData } + view={ view } + onChangeView={ onChangeView } + onSelectionChange={ onSelectionChange } + deferredRendering={ + ! view.hiddenFields?.includes( 'preview' ) + } + /> + </Page> + { view.type === LAYOUT_LIST && ( + <Page> + <div className="edit-site-template-pages-preview"> + { templateId !== null ? ( + <PostPreview + postId={ templateId } + postType={ TEMPLATE_POST_TYPE } + /> + ) : ( + <div + style={ { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + textAlign: 'center', + height: '100%', + } } + > + <p>{ __( 'Select a template to preview' ) }</p> + </div> + ) } + </div> + </Page> + ) } + </> ); } diff --git a/packages/edit-site/src/components/page-pages/side-editor.js b/packages/edit-site/src/components/post-preview/index.js similarity index 79% rename from packages/edit-site/src/components/page-pages/side-editor.js rename to packages/edit-site/src/components/post-preview/index.js index fca561cf9f4d5d..de66ef1aad7455 100644 --- a/packages/edit-site/src/components/page-pages/side-editor.js +++ b/packages/edit-site/src/components/post-preview/index.js @@ -4,7 +4,7 @@ import Editor from '../editor'; import { useInitEditedEntity } from '../sync-state-with-url/use-init-edited-entity-from-url'; -export default function SideEditor( { postType, postId } ) { +export default function PostPreview( { postType, postId } ) { useInitEditedEntity( { postId, postType, From e1aa88348c5f6b1600c58033bb003a05a2113360 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 13 Dec 2023 15:20:17 +0100 Subject: [PATCH 161/325] Editor: Unify revision panel between post and site editors (#57010) --- .../components/sidebar/last-revision/index.js | 17 ---- .../sidebar/last-revision/style.scss | 10 --- .../sidebar/settings-sidebar/index.js | 5 +- packages/edit-post/src/style.scss | 1 - .../sidebar-edit-mode/page-panels/index.js | 3 +- .../sidebar-edit-mode/template-panel/index.js | 37 +++++---- .../template-panel/last-revision.js | 82 ------------------- .../template-panel/style.scss | 4 - packages/editor/src/components/index.js | 1 + .../components/post-last-revision/panel.js | 22 +++++ .../components/post-last-revision/style.scss | 10 +++ 11 files changed, 57 insertions(+), 135 deletions(-) delete mode 100644 packages/edit-post/src/components/sidebar/last-revision/index.js delete mode 100644 packages/edit-post/src/components/sidebar/last-revision/style.scss delete mode 100644 packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js create mode 100644 packages/editor/src/components/post-last-revision/panel.js diff --git a/packages/edit-post/src/components/sidebar/last-revision/index.js b/packages/edit-post/src/components/sidebar/last-revision/index.js deleted file mode 100644 index 354d1104302889..00000000000000 --- a/packages/edit-post/src/components/sidebar/last-revision/index.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * WordPress dependencies - */ -import { PanelBody } from '@wordpress/components'; -import { PostLastRevision, PostLastRevisionCheck } from '@wordpress/editor'; - -function LastRevision() { - return ( - <PostLastRevisionCheck> - <PanelBody className="edit-post-last-revision__panel"> - <PostLastRevision /> - </PanelBody> - </PostLastRevisionCheck> - ); -} - -export default LastRevision; diff --git a/packages/edit-post/src/components/sidebar/last-revision/style.scss b/packages/edit-post/src/components/sidebar/last-revision/style.scss deleted file mode 100644 index fbc3bd465e2a66..00000000000000 --- a/packages/edit-post/src/components/sidebar/last-revision/style.scss +++ /dev/null @@ -1,10 +0,0 @@ -// Needs specificity, because this panel is just a button -.components-panel__body.is-opened.edit-post-last-revision__panel { - padding: 0; - height: $grid-unit-60; -} - -// Needs specificity to override button styles. -.editor-post-last-revision__title.components-button.components-button { - padding: $grid-unit-20; -} diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 9fa27c6ac2adeb..9b413d1858b589 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -11,14 +11,13 @@ import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { store as editorStore } from '@wordpress/editor'; +import { store as editorStore, PostLastRevisionPanel } from '@wordpress/editor'; /** * Internal dependencies */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import LastRevision from '../last-revision'; import PostTaxonomies from '../post-taxonomies'; import FeaturedImage from '../featured-image'; import PostExcerpt from '../post-excerpt'; @@ -79,7 +78,7 @@ const SidebarContent = ( { <> <PostStatus /> <PluginDocumentSettingPanel.Slot /> - <LastRevision /> + <PostLastRevisionPanel /> <PostTaxonomies /> <FeaturedImage /> <PostExcerpt /> diff --git a/packages/edit-post/src/style.scss b/packages/edit-post/src/style.scss index 88916bf70f76d3..fe03b2f7133735 100644 --- a/packages/edit-post/src/style.scss +++ b/packages/edit-post/src/style.scss @@ -8,7 +8,6 @@ @import "./components/meta-boxes/meta-boxes-area/style.scss"; @import "./components/secondary-sidebar/style.scss"; @import "./components/sidebar/style.scss"; -@import "./components/sidebar/last-revision/style.scss"; @import "./components/sidebar/post-format/style.scss"; @import "./components/sidebar/post-slug/style.scss"; @import "./components/sidebar/post-visibility/style.scss"; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index bbf4b55c052874..d23dc87f42543c 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -12,7 +12,7 @@ import { humanTimeDiff } from '@wordpress/date'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { store as editorStore } from '@wordpress/editor'; +import { PostLastRevisionPanel, store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -94,6 +94,7 @@ export default function PagePanels() { <PageContent /> </PanelBody> ) } + <PostLastRevisionPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 6956667852d690..2364053c834d71 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -3,7 +3,7 @@ */ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; -import { store as editorStore } from '@wordpress/editor'; +import { PostLastRevisionPanel, store as editorStore } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { navigation, symbol } from '@wordpress/icons'; @@ -14,7 +14,6 @@ import { navigation, symbol } from '@wordpress/icons'; import { store as editSiteStore } from '../../../store'; import TemplateActions from './template-actions'; import TemplateAreas from './template-areas'; -import LastRevision from './last-revision'; import SidebarCard from '../sidebar-card'; import PatternCategories from './pattern-categories'; import { PATTERN_TYPES } from '../../../utils/constants'; @@ -54,20 +53,24 @@ export default function TemplatePanel() { } return ( - <PanelBody className="edit-site-template-panel"> - <SidebarCard - className="edit-site-template-card" - title={ decodeEntities( title ) } - icon={ CARD_ICONS[ record?.type ] ?? icon } - description={ decodeEntities( description ) } - actions={ <TemplateActions template={ record } /> } - > - <TemplateAreas /> - </SidebarCard> - <LastRevision /> - { postType === PATTERN_TYPES.user && ( - <PatternCategories post={ record } /> - ) } - </PanelBody> + <> + <PanelBody> + <SidebarCard + className="edit-site-template-card" + title={ decodeEntities( title ) } + icon={ CARD_ICONS[ record?.type ] ?? icon } + description={ decodeEntities( description ) } + actions={ <TemplateActions template={ record } /> } + > + <TemplateAreas /> + </SidebarCard> + </PanelBody> + <PostLastRevisionPanel /> + <PanelBody> + { postType === PATTERN_TYPES.user && ( + <PatternCategories post={ record } /> + ) } + </PanelBody> + </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js deleted file mode 100644 index b81c1b8b6ddbec..00000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/last-revision.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * WordPress dependencies - */ -import { Button, PanelRow } from '@wordpress/components'; -import { sprintf, _n, __ } from '@wordpress/i18n'; -import { backup } from '@wordpress/icons'; -import { addQueryArgs } from '@wordpress/url'; -import { PostTypeSupportCheck } from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import useEditedEntityRecord from '../../use-edited-entity-record'; - -const useRevisionData = () => { - const { record: currentTemplate } = useEditedEntityRecord(); - - const lastRevisionId = - currentTemplate?._links?.[ 'predecessor-version' ]?.[ 0 ]?.id ?? null; - - const revisionsCount = - currentTemplate?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; - - return { - currentTemplate, - lastRevisionId, - revisionsCount, - }; -}; - -function PostLastRevisionCheck( { children } ) { - const { lastRevisionId, revisionsCount } = useRevisionData(); - - if ( ! process.env.IS_GUTENBERG_PLUGIN ) { - return null; - } - - if ( ! lastRevisionId || revisionsCount < 2 ) { - return null; - } - - return ( - <PostTypeSupportCheck supportKeys="revisions"> - { children } - </PostTypeSupportCheck> - ); -} - -const PostLastRevision = () => { - const { lastRevisionId, revisionsCount } = useRevisionData(); - - return ( - <PostLastRevisionCheck> - <PanelRow - header={ __( 'Editing history' ) } - className="edit-site-template-revisions" - > - <Button - href={ addQueryArgs( 'revision.php', { - revision: lastRevisionId, - } ) } - className="edit-site-template-last-revision__title" - icon={ backup } - > - { sprintf( - /* translators: %d: number of revisions */ - _n( '%d Revision', '%d Revisions', revisionsCount ), - revisionsCount - ) } - </Button> - </PanelRow> - </PostLastRevisionCheck> - ); -}; - -export default function LastRevision() { - return ( - <PostLastRevisionCheck> - <PostLastRevision /> - </PostLastRevisionCheck> - ); -} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss index 6eab753e8ad285..f2865195aa5b80 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/style.scss @@ -29,10 +29,6 @@ } } -.edit-site-template-revisions { - margin-left: math.div(-$grid-unit-10, 2); -} - h3.edit-site-template-card__template-areas-title { font-weight: 500; margin: 0 0 $grid-unit-10; diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 7efa33dc243b5d..a16c01c8c166ad 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -38,6 +38,7 @@ export { default as PostFormat } from './post-format'; export { default as PostFormatCheck } from './post-format/check'; export { default as PostLastRevision } from './post-last-revision'; export { default as PostLastRevisionCheck } from './post-last-revision/check'; +export { default as PostLastRevisionPanel } from './post-last-revision/panel'; export { default as PostLockedModal } from './post-locked-modal'; export { default as PostPendingStatus } from './post-pending-status'; export { default as PostPendingStatusCheck } from './post-pending-status/check'; diff --git a/packages/editor/src/components/post-last-revision/panel.js b/packages/editor/src/components/post-last-revision/panel.js new file mode 100644 index 00000000000000..de0aee0ab77503 --- /dev/null +++ b/packages/editor/src/components/post-last-revision/panel.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { PanelBody } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import PostLastRevision from './'; +import PostLastRevisionCheck from './check'; + +function PostLastRevisionPanel() { + return ( + <PostLastRevisionCheck> + <PanelBody className="editor-post-last-revision__panel"> + <PostLastRevision /> + </PanelBody> + </PostLastRevisionCheck> + ); +} + +export default PostLastRevisionPanel; diff --git a/packages/editor/src/components/post-last-revision/style.scss b/packages/editor/src/components/post-last-revision/style.scss index 7e4514aa2baca6..f41d43ed87f65e 100644 --- a/packages/editor/src/components/post-last-revision/style.scss +++ b/packages/editor/src/components/post-last-revision/style.scss @@ -21,3 +21,13 @@ border-radius: 0; } } + +// Needs specificity, because this panel is just a button +.components-panel__body.is-opened.editor-post-last-revision__panel { + padding: 0; + height: $grid-unit-60; + + .editor-post-last-revision__title.components-button.components-button { + padding: $grid-unit-20; + } +} From 000872e685e352e411b0958af9aef5da4d477532 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:51:16 +0900 Subject: [PATCH 162/325] Fix: Code editor title width in classic theme (#56922) --- packages/editor/src/components/post-title/style.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/editor/src/components/post-title/style.scss b/packages/editor/src/components/post-title/style.scss index 98bdfb9a2ebf3a..b43bbf2c95190a 100644 --- a/packages/editor/src/components/post-title/style.scss +++ b/packages/editor/src/components/post-title/style.scss @@ -2,4 +2,5 @@ .edit-post-text-editor__body .editor-post-title.is-raw-text { margin-bottom: $grid-unit-30; margin-top: 2px; // space for focus outline to appear. + max-width: none; } From a51534b6b374879e602c41ee171563452ac9115f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:13:02 +0100 Subject: [PATCH 163/325] DataViews: make filters footprint more condensed (#56983) Co-authored-by: James Koster <james@jameskoster.co.uk> --- packages/dataviews/src/add-filter.js | 328 ++++++++++++++---- packages/dataviews/src/filter-summary.js | 6 +- packages/dataviews/src/filters.js | 36 +- packages/dataviews/src/reset-filters.js | 9 + packages/dataviews/src/style.scss | 26 ++ .../src/components/layout/style.scss | 3 +- .../src/components/page-pages/index.js | 9 +- .../site-editor/new-templates-list.spec.js | 16 +- 8 files changed, 343 insertions(+), 90 deletions(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index 715135a533fb4b..5403d36703128c 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -6,101 +6,299 @@ import { Button, Icon, } from '@wordpress/components'; -import { chevronRightSmall, plus } from '@wordpress/icons'; -import { __ } from '@wordpress/i18n'; +import { chevronRightSmall, funnel, check } from '@wordpress/icons'; +import { __, sprintf } from '@wordpress/i18n'; +import { Children, Fragment } from '@wordpress/element'; /** * Internal dependencies */ import { unlock } from './lock-unlock'; -import { ENUMERATION_TYPE, OPERATOR_IN } from './constants'; +import { LAYOUT_LIST, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; const { DropdownMenuV2: DropdownMenu, + DropdownMenuGroupV2: DropdownMenuGroup, DropdownSubMenuV2: DropdownSubMenu, DropdownSubMenuTriggerV2: DropdownSubMenuTrigger, DropdownMenuItemV2: DropdownMenuItem, + DropdownMenuSeparatorV2: DropdownMenuSeparator, } = unlock( componentsPrivateApis ); -export default function AddFilter( { fields, view, onChangeView } ) { - const filters = []; - fields.forEach( ( field ) => { - if ( ! field.type ) { - return; - } - - switch ( field.type ) { - case ENUMERATION_TYPE: - filters.push( { - field: field.id, - name: field.header, - elements: field.elements || [], - isVisible: view.filters.some( - ( f ) => f.field === field.id - ), - } ); - } - } ); +function WithSeparators( { children } ) { + return Children.toArray( children ) + .filter( Boolean ) + .map( ( child, i ) => ( + <Fragment key={ i }> + { i > 0 && <DropdownMenuSeparator /> } + { child } + </Fragment> + ) ); +} +export default function AddFilter( { filters, view, onChangeView } ) { if ( filters.length === 0 ) { return null; } + const filterCount = view.filters.reduce( ( acc, filter ) => { + if ( filter.value !== undefined ) { + return acc + 1; + } + return acc; + }, 0 ); + return ( <DropdownMenu - label={ __( 'Add filter' ) } + label={ __( 'Filters' ) } trigger={ <Button - disabled={ filters.length === view.filters?.length } __experimentalIsFocusable + label={ __( 'Filters' ) } variant="tertiary" size="compact" + icon={ funnel } + className="dataviews-filters-button" > - <Icon icon={ plus } style={ { flexShrink: 0 } } /> - { __( 'Add filter' ) } + { view.type === LAYOUT_LIST && filterCount > 0 ? ( + <span className="dataviews-filters-count"> + { filterCount } + </span> + ) : null } </Button> } > - { filters.map( ( filter ) => { - if ( filter.isVisible ) { - return null; - } - - return ( - <DropdownSubMenu - key={ filter.field } - trigger={ - <DropdownSubMenuTrigger - suffix={ <Icon icon={ chevronRightSmall } /> } + <WithSeparators> + <DropdownMenuGroup> + { filters.map( ( filter ) => { + const filterInView = view.filters.find( + ( f ) => f.field === filter.field + ); + const activeElement = filter.elements.find( + ( element ) => element.value === filterInView?.value + ); + const activeOperator = + filterInView?.operator || filter.operators[ 0 ]; + return ( + <DropdownSubMenu + key={ filter.field } + trigger={ + <DropdownSubMenuTrigger + suffix={ + <> + { activeElement && + activeOperator === + OPERATOR_IN && + __( 'Is' ) } + { activeElement && + activeOperator === + OPERATOR_NOT_IN && + __( 'Is not' ) } + { activeElement && ' ' } + { activeElement?.label } + <Icon + icon={ chevronRightSmall } + /> + </> + } + > + { filter.name } + </DropdownSubMenuTrigger> + } > - { filter.name } - </DropdownSubMenuTrigger> - } - > - { filter.elements.map( ( element ) => ( - <DropdownMenuItem - key={ element.value } - onSelect={ () => { - onChangeView( ( currentView ) => ( { - ...currentView, - page: 1, - filters: [ - ...currentView.filters, - { - field: filter.field, - operator: OPERATOR_IN, - value: element.value, - }, - ], - } ) ); - } } - > - { element.label } - </DropdownMenuItem> - ) ) } - </DropdownSubMenu> - ); - } ) } + <WithSeparators> + <DropdownMenuGroup> + { filter.elements.map( ( element ) => ( + <DropdownMenuItem + key={ element.value } + role="menuitemradio" + aria-checked={ + activeElement?.value === + element.value + } + prefix={ + activeElement?.value === + element.value && ( + <Icon icon={ check } /> + ) + } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( + ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...currentView.filters.filter( + ( f ) => + f.field !== + filter.field + ), + { + field: filter.field, + operator: + activeOperator, + value: + activeElement?.value === + element.value + ? undefined + : element.value, + }, + ], + } ) + ); + } } + > + { element.label } + </DropdownMenuItem> + ) ) } + </DropdownMenuGroup> + { filter.operators.length > 1 && ( + <DropdownSubMenu + trigger={ + <DropdownSubMenuTrigger + suffix={ + <> + { activeOperator === + OPERATOR_IN && + __( 'Is' ) } + { activeOperator === + OPERATOR_NOT_IN && + __( 'Is not' ) } + <Icon + icon={ + chevronRightSmall + } + />{ ' ' } + </> + } + > + { __( 'Conditions' ) } + </DropdownSubMenuTrigger> + } + > + <DropdownMenuItem + key="in-filter" + role="menuitemradio" + aria-checked={ + activeOperator === + OPERATOR_IN + } + prefix={ + activeOperator === + OPERATOR_IN && ( + <Icon icon={ check } /> + ) + } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( + ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => + f.field !== + filter.field + ), + { + field: filter.field, + operator: + OPERATOR_IN, + value: filterInView?.value, + }, + ], + } ) + ); + } } + > + { __( 'Is' ) } + </DropdownMenuItem> + <DropdownMenuItem + key="not-in-filter" + role="menuitemradio" + aria-checked={ + activeOperator === + OPERATOR_NOT_IN + } + prefix={ + activeOperator === + OPERATOR_NOT_IN && ( + <Icon icon={ check } /> + ) + } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( + ( currentView ) => ( { + ...currentView, + page: 1, + filters: [ + ...view.filters.filter( + ( f ) => + f.field !== + filter.field + ), + { + field: filter.field, + operator: + OPERATOR_NOT_IN, + value: filterInView?.value, + }, + ], + } ) + ); + } } + > + { __( 'Is not' ) } + </DropdownMenuItem> + </DropdownSubMenu> + ) } + <DropdownMenuItem + key={ 'reset-filter-' + filter.name } + disabled={ ! activeElement } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: + currentView.filters.filter( + ( f ) => + f.field !== + filter.field + ), + } ) ); + } } + > + { sprintf( + /* translators: 1: Filter name. e.g.: "Reset Author". */ + __( 'Reset %1$s' ), + filter.name.toLowerCase() + ) } + </DropdownMenuItem> + </WithSeparators> + </DropdownSubMenu> + ); + } ) } + </DropdownMenuGroup> + <DropdownMenuItem + disabled={ + view.search === '' && view.filters?.length === 0 + } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( ( currentView ) => ( { + ...currentView, + page: 1, + filters: [], + } ) ); + } } + > + { __( 'Reset filters' ) } + </DropdownMenuItem> + </WithSeparators> </DropdownMenu> ); } diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 3c30c6837103a7..fc0f8848f6a939 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -13,7 +13,7 @@ import { Children, Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import { OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; +import { OPERATOR_IN, OPERATOR_NOT_IN, LAYOUT_LIST } from './constants'; import { unlock } from './lock-unlock'; const { @@ -73,6 +73,10 @@ function WithSeparators( { children } ) { } export default function FilterSummary( { filter, view, onChangeView } ) { + if ( view.type === LAYOUT_LIST ) { + return null; + } + const filterInView = view.filters.find( ( f ) => f.field === filter.field ); const activeElement = filter.elements.find( ( element ) => element.value === filterInView?.value diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index e2d24e7a848eea..153372379cf8d2 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -46,29 +46,31 @@ export default function Filters( { fields, view, onChangeView } ) { } } ); - const filterComponents = filters.map( ( filter ) => { - if ( ! filter.isVisible ) { - return null; - } - - return ( - <FilterSummary - key={ filter.field + '.' + filter.operator } - filter={ filter } - view={ view } - onChangeView={ onChangeView } - /> - ); - } ); - - filterComponents.push( + const addFilter = ( <AddFilter key="add-filter" - fields={ fields } + filters={ filters } view={ view } onChangeView={ onChangeView } /> ); + const filterComponents = [ + addFilter, + ...filters.map( ( filter ) => { + if ( ! filter.isVisible ) { + return null; + } + + return ( + <FilterSummary + key={ filter.field + '.' + filter.operator } + filter={ filter } + view={ view } + onChangeView={ onChangeView } + /> + ); + } ), + ]; if ( filterComponents.length > 1 ) { filterComponents.push( diff --git a/packages/dataviews/src/reset-filters.js b/packages/dataviews/src/reset-filters.js index d78c06624087a7..503892b6e07377 100644 --- a/packages/dataviews/src/reset-filters.js +++ b/packages/dataviews/src/reset-filters.js @@ -4,7 +4,16 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { LAYOUT_LIST } from './constants'; + export default ( { view, onChangeView } ) => { + if ( view.type === LAYOUT_LIST ) { + return null; + } + return ( <Button disabled={ view.search === '' && view.filters?.length === 0 } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index e7e06e1acd27e4..ccb565c0b58673 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -12,6 +12,32 @@ .dataviews__filters-view-actions { padding: $grid-unit-15 $grid-unit-40; + .components-search-control { + flex-grow: 1; + max-width: 240px; + } +} + +.dataviews-filters-button { + position: relative; +} + +.dataviews-filters-count { + position: absolute; + top: 0; + right: 0; + height: $grid-unit-20; + color: var(--wp-components-color-accent-inverted, $white); + background-color: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + border-radius: $grid-unit-10; + min-width: $grid-unit-20; + padding: 0 $grid-unit-05; + transform: translateX(40%) translateY(-40%); + display: flex; + align-items: center; + justify-content: center; + font-size: 11px; + font-weight: 300; } .dataviews-pagination { diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 8e60397f6bdf8d..72b8e4db49716f 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -156,7 +156,8 @@ } } -.edit-site-template-pages-list-view { +.edit-site-template-pages-list-view, +.edit-site-page-pages-list-view { max-width: $nav-sidebar-width; } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 861aa48fcd5cb6..55eb450f7ac7e3 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -320,7 +320,14 @@ export default function PagePages() { // TODO: we need to handle properly `data={ data || EMPTY_ARRAY }` for when `isLoading`. return ( <> - <Page title={ __( 'Pages' ) }> + <Page + className={ + view.type === LAYOUT_LIST + ? 'edit-site-page-pages-list-view' + : null + } + title={ __( 'Pages' ) } + > <DataViews paginationInfo={ paginationInfo } fields={ fields } diff --git a/test/e2e/specs/site-editor/new-templates-list.spec.js b/test/e2e/specs/site-editor/new-templates-list.spec.js index 34daeb6d40f09b..c95668d00b8452 100644 --- a/test/e2e/specs/site-editor/new-templates-list.spec.js +++ b/test/e2e/specs/site-editor/new-templates-list.spec.js @@ -54,16 +54,19 @@ test.describe( 'Templates', () => { await page.keyboard.type( 'tag' ); const titles = page .getByRole( 'region', { name: 'Template' } ) - .getByRole( 'link' ); + .getByRole( 'link', { includeHidden: true } ); await expect( titles ).toHaveCount( 1 ); await expect( titles.first() ).toHaveText( 'Tag Archives' ); await page.getByRole( 'button', { name: 'Reset filters' } ).click(); await expect( titles ).toHaveCount( 6 ); // Filter by author. - await page.getByRole( 'button', { name: 'Add filter' } ).click(); + await page + .getByRole( 'button', { name: 'Filters', exact: true } ) + .click(); await page.getByRole( 'menuitem', { name: 'Author' } ).hover(); - await page.getByRole( 'menuitem', { name: 'admin' } ).click(); + await page.getByRole( 'menuitemradio', { name: 'admin' } ).click(); + await page.keyboard.press( 'Escape' ); // close the menu. await expect( titles ).toHaveCount( 1 ); await expect( titles.first() ).toHaveText( 'Date Archives' ); @@ -72,9 +75,12 @@ test.describe( 'Templates', () => { await page.getByRole( 'searchbox', { name: 'Filter list' } ).click(); await page.keyboard.type( 'archives' ); await expect( titles ).toHaveCount( 3 ); - await page.getByRole( 'button', { name: 'Add filter' } ).click(); + await page + .getByRole( 'button', { name: 'Filters', exact: true } ) + .click(); await page.getByRole( 'menuitem', { name: 'Author' } ).hover(); - await page.getByRole( 'menuitem', { name: 'Emptytheme' } ).click(); + await page.getByRole( 'menuitemradio', { name: 'Emptytheme' } ).click(); + await page.keyboard.press( 'Escape' ); // close the menu. await expect( titles ).toHaveCount( 2 ); } ); test( 'Field visibility', async ( { admin, page } ) => { From 94ede3ff3b5de34ec1afb48963e9660bc23508fa Mon Sep 17 00:00:00 2001 From: Gerardo Pacheco <gerardo.pacheco@automattic.com> Date: Wed, 13 Dec 2023 18:18:07 +0100 Subject: [PATCH 164/325] [Mobile] Fix regressions with wrapper props and font size customization (#56985) * Mobile - Hooks - Use createBlockListBlockFilter * Mobile - Typography - Refactor the code to incorporate the latest changes from its web counterpart * Mobile - BlockList: Apply editor.BlockListBlock filter to fix issue with missing block props, as well as refactoring the getEditWrapperProps logic to use the same approach as its web counterpart * Mobile - Test helpers - Add more global styles data: font sizes and line height * Mobile - Font Size Picker - Improvde the accessibility label for the Font Size selector * Mobile - Paragraph tests - Add test for font size and line height customization * Mobile - Safe guard from an undefined wrapperProps value * Mobile - Fix having the default font sizes when there are theme font sizes available * Mobile - Global styles context test - Remove default font sizes * Mobile - Paragraph tests - Update tests to use modal helpers * Mobile - Paragraph tests - Adds test to check if the available font sizes are the ones expected with no duplicates * Update Changelog --- .../src/components/block-list/block.native.js | 59 +++++--- .../block-editor/src/hooks/index.native.js | 7 +- .../src/hooks/typography.native.js | 64 +++++---- .../test/__snapshots__/edit.native.js.snap | 12 ++ .../src/paragraph/test/edit.native.js | 114 ++++++++++++++++ .../src/font-size-picker/index.native.js | 8 +- .../test/fixtures/theme.native.js | 20 --- .../global-styles-context/utils.native.js | 45 +++--- packages/react-native-editor/CHANGELOG.md | 1 + .../get-global-styles.js | 128 ++++++++++++++++++ 10 files changed, 363 insertions(+), 95 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 70a66c445f58f9..027ed12a7483ae 100644 --- a/packages/block-editor/src/components/block-list/block.native.js +++ b/packages/block-editor/src/components/block-list/block.native.js @@ -2,6 +2,7 @@ * External dependencies */ import { Pressable, View } from 'react-native'; +import classnames from 'classnames'; /** * WordPress dependencies @@ -12,6 +13,7 @@ import { getMergedGlobalStyles, useMobileGlobalStylesColors, useGlobalStyles, + withFilters, } from '@wordpress/components'; import { __experimentalGetAccessibleBlockLabel as getAccessibleBlockLabel, @@ -42,20 +44,36 @@ import { useSettings } from '../use-settings'; const EMPTY_ARRAY = []; -// Helper function to memoize the wrapperProps since getEditWrapperProps always returns a new reference. -const wrapperPropsCache = new WeakMap(); -const emptyObj = {}; -function getWrapperProps( value, getWrapperPropsFunction ) { - if ( ! getWrapperPropsFunction ) { - return emptyObj; +/** + * Merges wrapper props with special handling for classNames and styles. + * + * @param {Object} propsA + * @param {Object} propsB + * + * @return {Object} Merged props. + */ +function mergeWrapperProps( propsA, propsB ) { + const newProps = { + ...propsA, + ...propsB, + }; + + // May be set to undefined, so check if the property is set! + if ( + propsA?.hasOwnProperty( 'className' ) && + propsB?.hasOwnProperty( 'className' ) + ) { + newProps.className = classnames( propsA.className, propsB.className ); } - const cachedValue = wrapperPropsCache.get( value ); - if ( ! cachedValue ) { - const wrapperProps = getWrapperPropsFunction( value ); - wrapperPropsCache.set( value, wrapperProps ); - return wrapperProps; + + if ( + propsA?.hasOwnProperty( 'style' ) && + propsB?.hasOwnProperty( 'style' ) + ) { + newProps.style = { ...propsA.style, ...propsB.style }; } - return cachedValue; + + return newProps; } function BlockWrapper( { @@ -136,6 +154,7 @@ function BlockListBlock( { rootClientId, setAttributes, toggleSelection, + wrapperProps, } ) { const { baseGlobalStyles, @@ -252,12 +271,11 @@ function BlockListBlock( { [ blockWidth, setBlockWidth ] ); - // Block level styles. - let wrapperProps = {}; + // Determine whether the block has props to apply to the wrapper. if ( blockType?.getEditWrapperProps ) { - wrapperProps = getWrapperProps( - attributes, - blockType.getEditWrapperProps + wrapperProps = mergeWrapperProps( + wrapperProps, + blockType.getEditWrapperProps( attributes ) ); } @@ -266,7 +284,7 @@ function BlockListBlock( { return getMergedGlobalStyles( baseGlobalStyles, globalStyle, - wrapperProps.style, + wrapperProps?.style, attributes, defaultColors, name, @@ -284,7 +302,7 @@ function BlockListBlock( { // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify( globalStyle ), // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify( wrapperProps.style ), + JSON.stringify( wrapperProps?.style ), // eslint-disable-next-line react-hooks/exhaustive-deps JSON.stringify( Object.fromEntries( @@ -651,5 +669,6 @@ export default compose( // Block is sometimes not mounted at the right time, causing it be undefined // see issue for more info // https://github.com/WordPress/gutenberg/issues/17013 - ifCondition( ( { block } ) => !! block ) + ifCondition( ( { block } ) => !! block ), + withFilters( 'editor.BlockListBlock' ) )( BlockListBlock ); diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index 3f1a4473c13891..c0530aedb37ca4 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -1,18 +1,19 @@ /** * Internal dependencies */ -import { createBlockEditFilter } from './utils'; +import { createBlockEditFilter, createBlockListBlockFilter } from './utils'; import './compat'; import align from './align'; import anchor from './anchor'; import './custom-class-name'; import './generated-class-name'; import style from './style'; -import './color'; -import './font-size'; +import color from './color'; +import fontSize from './font-size'; import './layout'; createBlockEditFilter( [ align, anchor, style ] ); +createBlockListBlockFilter( [ align, style, color, fontSize ] ); export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; diff --git a/packages/block-editor/src/hooks/typography.native.js b/packages/block-editor/src/hooks/typography.native.js index f6fe58edb28703..d8cbf71d84e13f 100644 --- a/packages/block-editor/src/hooks/typography.native.js +++ b/packages/block-editor/src/hooks/typography.native.js @@ -1,10 +1,8 @@ /** * WordPress dependencies */ -import { hasBlockSupport } from '@wordpress/blocks'; -/** - * External dependencies - */ +import { useSelect } from '@wordpress/data'; +import { pure } from '@wordpress/compose'; import { PanelBody } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -12,17 +10,12 @@ import { __ } from '@wordpress/i18n'; * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; +import { useHasTypographyPanel } from '../components/global-styles/typography-panel'; -import { - LINE_HEIGHT_SUPPORT_KEY, - LineHeightEdit, - useIsLineHeightDisabled, -} from './line-height'; -import { - FONT_SIZE_SUPPORT_KEY, - FontSizeEdit, - useIsFontSizeDisabled, -} from './font-size'; +import { store as blockEditorStore } from '../store'; + +import { LINE_HEIGHT_SUPPORT_KEY, LineHeightEdit } from './line-height'; +import { FONT_SIZE_SUPPORT_KEY, FontSizeEdit } from './font-size'; export const TYPOGRAPHY_SUPPORT_KEY = 'typography'; export const TYPOGRAPHY_SUPPORT_KEYS = [ @@ -30,11 +23,26 @@ export const TYPOGRAPHY_SUPPORT_KEYS = [ FONT_SIZE_SUPPORT_KEY, ]; -export function TypographyPanel( props ) { - const isDisabled = useIsTypographyDisabled( props ); - const isSupported = hasTypographySupport( props.name ); - - if ( isDisabled || ! isSupported ) return null; +function TypographyPanelPure( { clientId, setAttributes, settings } ) { + function selector( select ) { + const { style, fontFamily, fontSize } = + select( blockEditorStore ).getBlockAttributes( clientId ) || {}; + return { style, fontFamily, fontSize }; + } + const { style, fontSize } = useSelect( selector, [ clientId ] ); + const isEnabled = useHasTypographyPanel( settings ); + + if ( ! isEnabled ) { + return null; + } + + const props = { + attributes: { + fontSize, + style, + }, + setAttributes, + }; return ( <InspectorControls> @@ -46,17 +54,7 @@ export function TypographyPanel( props ) { ); } -const hasTypographySupport = ( blockName ) => { - return TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => - hasBlockSupport( blockName, key ) - ); -}; - -function useIsTypographyDisabled( props = {} ) { - const configs = [ - useIsFontSizeDisabled( props ), - useIsLineHeightDisabled( props ), - ]; - - return configs.filter( Boolean ).length === configs.length; -} +// We don't want block controls to re-render when typing inside a block. `pure` +// will prevent re-renders unless props change, so only pass the needed props +// and not the whole attributes object. +export const TypographyPanel = pure( TypographyPanelPure ); diff --git a/packages/block-library/src/paragraph/test/__snapshots__/edit.native.js.snap b/packages/block-library/src/paragraph/test/__snapshots__/edit.native.js.snap index adc6ab4210efa5..2910d1551ca28b 100644 --- a/packages/block-library/src/paragraph/test/__snapshots__/edit.native.js.snap +++ b/packages/block-library/src/paragraph/test/__snapshots__/edit.native.js.snap @@ -38,3 +38,15 @@ exports[`Paragraph block should render without crashing and match snapshot 1`] = /> </View> `; + +exports[`Paragraph block should set a font size value 1`] = ` +"<!-- wp:paragraph {"style":{"typography":{"fontSize":"30px"}}} --> +<p style="font-size:30px">A quick brown fox jumps over the lazy dog.</p> +<!-- /wp:paragraph -->" +`; + +exports[`Paragraph block should set a line height value 1`] = ` +"<!-- wp:paragraph {"style":{"typography":{"lineHeight":1.8}}} --> +<p style="line-height:1.8">A quick brown fox jumps over the lazy dog.</p> +<!-- /wp:paragraph -->" +`; diff --git a/packages/block-library/src/paragraph/test/edit.native.js b/packages/block-library/src/paragraph/test/edit.native.js index fdb082246171ba..3d09068c24930c 100644 --- a/packages/block-library/src/paragraph/test/edit.native.js +++ b/packages/block-library/src/paragraph/test/edit.native.js @@ -4,6 +4,7 @@ import { act, addBlock, + dismissModal, getBlock, typeInRichText, fireEvent, @@ -15,6 +16,7 @@ import { within, withFakeTimers, waitForElementToBeRemoved, + waitForModalVisible, } from 'test/helpers'; import Clipboard from '@react-native-clipboard/clipboard'; import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; @@ -687,6 +689,118 @@ describe( 'Paragraph block', () => { ` ); } ); + it( 'should show the expected font sizes values', async () => { + // Arrange + const screen = await initializeEditor( { withGlobalStyles: true } ); + await addBlock( screen, 'Paragraph' ); + + // Act + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + typeInRichText( + paragraphTextInput, + 'A quick brown fox jumps over the lazy dog.' + ); + // Open Block Settings. + fireEvent.press( screen.getByLabelText( 'Open Settings' ) ); + + // Wait for Block Settings to be visible. + const blockSettingsModal = screen.getByTestId( 'block-settings-modal' ); + await waitForModalVisible( blockSettingsModal ); + + // Open Font size settings + fireEvent.press( screen.getByLabelText( 'Font Size, Custom' ) ); + await waitFor( () => screen.getByLabelText( 'Selected: Default' ) ); + + // Assert + const modalContent = within( blockSettingsModal ); + expect( modalContent.getByLabelText( 'Small' ) ).toBeVisible(); + expect( modalContent.getByText( '14px' ) ).toBeVisible(); + expect( modalContent.getByLabelText( 'Medium' ) ).toBeVisible(); + expect( modalContent.getByText( '17px' ) ).toBeVisible(); + expect( modalContent.getByLabelText( 'Large' ) ).toBeVisible(); + expect( modalContent.getByText( '30px' ) ).toBeVisible(); + expect( modalContent.getByLabelText( 'Extra Large' ) ).toBeVisible(); + expect( modalContent.getByText( '40px' ) ).toBeVisible(); + expect( + modalContent.getByLabelText( 'Extra Extra Large' ) + ).toBeVisible(); + expect( modalContent.getByText( '52px' ) ).toBeVisible(); + } ); + + it( 'should set a font size value', async () => { + // Arrange + const screen = await initializeEditor( { withGlobalStyles: true } ); + await addBlock( screen, 'Paragraph' ); + + // Act + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + typeInRichText( + paragraphTextInput, + 'A quick brown fox jumps over the lazy dog.' + ); + // Open Block Settings. + fireEvent.press( screen.getByLabelText( 'Open Settings' ) ); + + // Wait for Block Settings to be visible. + const blockSettingsModal = screen.getByTestId( 'block-settings-modal' ); + await waitForModalVisible( blockSettingsModal ); + + // Open Font size settings + fireEvent.press( screen.getByLabelText( 'Font Size, Custom' ) ); + + // Tap one font size + fireEvent.press( screen.getByLabelText( 'Large' ) ); + + // Dismiss the Block Settings modal. + await dismissModal( blockSettingsModal ); + + // Assert + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'should set a line height value', async () => { + // Arrange + const screen = await initializeEditor( { withGlobalStyles: true } ); + await addBlock( screen, 'Paragraph' ); + + // Act + const paragraphBlock = getBlock( screen, 'Paragraph' ); + fireEvent.press( paragraphBlock ); + const paragraphTextInput = + within( paragraphBlock ).getByPlaceholderText( 'Start writing…' ); + typeInRichText( + paragraphTextInput, + 'A quick brown fox jumps over the lazy dog.' + ); + // Open Block Settings. + fireEvent.press( screen.getByLabelText( 'Open Settings' ) ); + + // Wait for Block Settings to be visible. + const blockSettingsModal = screen.getByTestId( 'block-settings-modal' ); + await waitForModalVisible( blockSettingsModal ); + + const lineHeightControl = screen.getByLabelText( /Line Height/ ); + fireEvent.press( + within( lineHeightControl ).getByText( '1.5', { hidden: true } ) + ); + const lineHeightTextInput = within( + lineHeightControl + ).getByDisplayValue( '1.5', { hidden: true } ); + fireEvent.changeText( lineHeightTextInput, '1.8' ); + + // Dismiss the Block Settings modal. + await dismissModal( blockSettingsModal ); + + // Assert + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + it( 'should focus on the previous Paragraph block when backspacing in an empty Paragraph block', async () => { // Arrange const screen = await initializeEditor(); diff --git a/packages/components/src/font-size-picker/index.native.js b/packages/components/src/font-size-picker/index.native.js index 9659c1dbe225fa..1d5d25cbc1b734 100644 --- a/packages/components/src/font-size-picker/index.native.js +++ b/packages/components/src/font-size-picker/index.native.js @@ -62,6 +62,12 @@ function FontSizePicker( { availableUnits: [ 'px', 'em', 'rem' ], } ); + const accessibilityLabel = sprintf( + // translators: %1$s: Font size name e.g. Small + __( 'Font Size, %1$s' ), + selectedOption.name + ); + return ( <BottomSheet.SubSheet navigationButton={ @@ -80,7 +86,7 @@ function FontSizePicker( { } onPress={ openSubSheet } accessibilityRole={ 'button' } - accessibilityLabel={ selectedOption.name } + accessibilityLabel={ accessibilityLabel } accessibilityHint={ sprintf( // translators: %s: Select control button label e.g. Small __( 'Navigates to select %s' ), diff --git a/packages/components/src/mobile/global-styles-context/test/fixtures/theme.native.js b/packages/components/src/mobile/global-styles-context/test/fixtures/theme.native.js index 68c4593446a7ce..c9732f7825c35c 100644 --- a/packages/components/src/mobile/global-styles-context/test/fixtures/theme.native.js +++ b/packages/components/src/mobile/global-styles-context/test/fixtures/theme.native.js @@ -231,26 +231,6 @@ export const RAW_FEATURES = { }, typography: { fontSizes: { - default: [ - { - name: 'Small', - slug: 'small', - size: '13px', - sizePx: '13px', - }, - { - name: 'Normal', - slug: 'normal', - size: '16px', - sizePx: '16px', - }, - { - name: 'Huge', - slug: 'huge', - size: '42px', - sizePx: '42px', - }, - ], theme: [ { name: 'Normal', diff --git a/packages/components/src/mobile/global-styles-context/utils.native.js b/packages/components/src/mobile/global-styles-context/utils.native.js index b56e28da46207c..ac77945c464cb7 100644 --- a/packages/components/src/mobile/global-styles-context/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/utils.native.js @@ -342,29 +342,38 @@ export function getMappedValues( features, palette ) { * @return {Object} normalized sizes. */ function normalizeFontSizes( fontSizes ) { - // Adds normalized PX values for each of the different keys. if ( ! fontSizes ) { return fontSizes; } - const normalizedFontSizes = {}; + const dimensions = Dimensions.get( 'window' ); + const normalizedFontSizes = {}; + const keysToProcess = []; - [ 'default', 'theme', 'custom' ].forEach( ( key ) => { - if ( fontSizes[ key ] ) { - normalizedFontSizes[ key ] = fontSizes[ key ]?.map( - ( fontSizeObject ) => { - fontSizeObject.sizePx = getPxFromCssUnit( - fontSizeObject.size, - { - width: dimensions.width, - height: dimensions.height, - fontSize: DEFAULT_FONT_SIZE, - } - ); - return fontSizeObject; - } - ); - } + // Check if 'theme' or 'custom' keys exist and add them to keysToProcess array + if ( fontSizes?.theme ) { + keysToProcess.push( 'theme' ); + } + if ( fontSizes?.custom ) { + keysToProcess.push( 'custom' ); + } + + // If neither 'theme' nor 'custom' exist, add 'default' if it exists + if ( keysToProcess.length === 0 && fontSizes?.default ) { + keysToProcess.push( 'default' ); + } + + keysToProcess.forEach( ( key ) => { + normalizedFontSizes[ key ] = fontSizes[ key ].map( + ( fontSizeObject ) => { + fontSizeObject.sizePx = getPxFromCssUnit( fontSizeObject.size, { + width: dimensions.width, + height: dimensions.height, + fontSize: DEFAULT_FONT_SIZE, + } ); + return fontSizeObject; + } + ); } ); return normalizedFontSizes; diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 33fa3d26e6c0ad..6f4c1ee783e198 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -15,6 +15,7 @@ For each user feature we should also add a importance categorization label to i - [*] Fix crash when blockType wrapperProps are not defined [#56846] - [*] Guard against an Image block styles crash due to null block values [#56903] - [**] Fix crash when sharing unsupported media types on Android [#56791] +- [**] Fix regressions with wrapper props and font size customization [#56985] ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] diff --git a/test/native/integration-test-helpers/get-global-styles.js b/test/native/integration-test-helpers/get-global-styles.js index 1156018358c3ca..405b3d93c5d90f 100644 --- a/test/native/integration-test-helpers/get-global-styles.js +++ b/test/native/integration-test-helpers/get-global-styles.js @@ -41,6 +41,133 @@ const GLOBAL_STYLES_RAW_FEATURES = { ], }, }, + typography: { + fontSizes: { + default: [ + { + name: 'Small', + size: '13px', + slug: 'small', + }, + { + name: 'Medium', + size: '20px', + slug: 'medium', + }, + { + name: 'Large', + size: '36px', + slug: 'large', + }, + { + name: 'Extra Large', + size: '42px', + slug: 'x-large', + }, + ], + theme: [ + { + fluid: false, + name: 'Small', + size: '0.9rem', + slug: 'small', + }, + { + fluid: false, + name: 'Medium', + size: '1.05rem', + slug: 'medium', + }, + { + fluid: { + max: '1.85rem', + min: '1.39rem', + }, + name: 'Large', + size: '1.85rem', + slug: 'large', + }, + { + fluid: { + max: '2.5rem', + min: '1.85rem', + }, + name: 'Extra Large', + size: '2.5rem', + slug: 'x-large', + }, + { + fluid: { + max: '3.27rem', + min: '2.5rem', + }, + name: 'Extra Extra Large', + size: '3.27rem', + slug: 'xx-large', + }, + ], + }, + }, +}; + +const GLOBAL_STYLES_RAW_STYLES = { + color: { + background: 'var(--wp--preset--color--foreground)', + text: 'var(--wp--preset--color--tertiary)', + }, + elements: { + h1: { + typography: { + fontSize: 'var(--wp--preset--font-size--xx-large)', + lineHeight: '1.15', + }, + }, + h2: { + typography: { + fontSize: 'var(--wp--preset--font-size--x-large)', + }, + }, + h3: { + typography: { + fontSize: 'var(--wp--preset--font-size--large)', + }, + }, + h4: { + typography: { + fontSize: + 'clamp(1.1rem, 1.1rem + ((1vw - 0.2rem) * 0.767), 1.5rem)', + }, + }, + h5: { + typography: { + fontSize: 'var(--wp--preset--font-size--medium)', + }, + }, + h6: { + typography: { + fontSize: 'var(--wp--preset--font-size--small)', + }, + }, + heading: { + color: { + text: 'var(--wp--preset--color--tertiary)', + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--heading)', + lineHeight: '1.2', + }, + }, + link: { + color: { + text: 'var(--wp--preset--color--tertiary)', + }, + }, + }, + typography: { + fontFamily: 'var(--wp--preset--font-family--body)', + fontSize: 'var(--wp--preset--font-size--medium)', + lineHeight: '1.55', + }, }; /** @@ -51,5 +178,6 @@ const GLOBAL_STYLES_RAW_FEATURES = { export function getGlobalStyles() { return { rawFeatures: JSON.stringify( GLOBAL_STYLES_RAW_FEATURES ), + rawStyles: JSON.stringify( GLOBAL_STYLES_RAW_STYLES ), }; } From 1370534794c02317dd729c498f38c89645a2d44a Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Wed, 13 Dec 2023 17:49:41 +0000 Subject: [PATCH 165/325] Fix: Template list title font styles. (#57027) --- packages/edit-site/src/components/list/style.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/edit-site/src/components/list/style.scss b/packages/edit-site/src/components/list/style.scss index 4739243b7dc295..9d6e0f0f1d6e24 100644 --- a/packages/edit-site/src/components/list/style.scss +++ b/packages/edit-site/src/components/list/style.scss @@ -188,6 +188,6 @@ } .edit-site-list-title__customized-info { - font-size: 1.3em; - font-weight: 600; + font-size: $default-font-size; + font-weight: 500; } From 4844fe96a41ba29d56ad17673203cef0f1ed7b0e Mon Sep 17 00:00:00 2001 From: Anton Vlasenko <43744263+anton-vlasenko@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:03:05 +0100 Subject: [PATCH 166/325] Fix code style in Gutenberg_HTML_Tag_Processor_6_5. (#57030) --- .../html-api/class-gutenberg-html-tag-processor-6-5.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php index 5594110f0d1c8e..f14bc15adf9999 100644 --- a/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php +++ b/lib/compat/wordpress-6.5/html-api/class-gutenberg-html-tag-processor-6-5.php @@ -1651,7 +1651,7 @@ private function apply_attributes_updates( $shift_this_point = 0 ) { * replacements adjust offsets in the input document. */ foreach ( $this->bookmarks as $bookmark_name => $bookmark ) { - $bookmark_end = $bookmark->start + $bookmark->length; + $bookmark_end = $bookmark->start + $bookmark->length; /* * Each lexical update which appears before the bookmark's endpoints From 37401d5f05f9aded18a6dc3d690f2bd25d2f87b9 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Wed, 13 Dec 2023 13:26:24 -0500 Subject: [PATCH 167/325] Components: replace `TabPanel` with `Tabs` in the Block Inserter (#56918) * implement `Tabs` * update styles * focusable false * replace render function with object * fix inserter tests * pass contents as prop instead of children --- .../src/components/inserter/menu.js | 23 +++++------ .../src/components/inserter/style.scss | 8 ++-- .../src/components/inserter/tabs.js | 38 ++++++++++++++----- packages/e2e-test-utils/src/inserter.js | 5 ++- 4 files changed, 46 insertions(+), 28 deletions(-) diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index 4f028eb69c6662..cd44b902f491a5 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -191,17 +191,13 @@ function InserterMenu( ] ); - const getCurrentTab = useCallback( - ( tab ) => { - if ( tab.name === 'blocks' ) { - return blocksTab; - } else if ( tab.name === 'patterns' ) { - return patternsTab; - } else if ( tab.name === 'media' ) { - return mediaTab; - } - }, - [ blocksTab, patternsTab, mediaTab ] + const inserterTabsContents = useMemo( + () => ( { + blocks: blocksTab, + patterns: patternsTab, + media: mediaTab, + } ), + [ blocksTab, mediaTab, patternsTab ] ); const searchRef = useRef(); @@ -275,9 +271,8 @@ function InserterMenu( showMedia={ showMedia } prioritizePatterns={ prioritizePatterns } onSelect={ handleSetSelectedTab } - > - { getCurrentTab } - </InserterTabs> + tabsContents={ inserterTabsContents } + /> ) } { ! delayedFilterValue && ! showAsTabs && ( <div className="block-editor-inserter__no-tab-container"> diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 755445246e8596..97a3d877b7e72a 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -61,7 +61,7 @@ $block-inserter-tabs-height: 44px; .block-editor-inserter__popover .block-editor-inserter__menu { margin: -$grid-unit-15; - .block-editor-inserter__tabs .components-tab-panel__tabs { + .block-editor-inserter__tabs div[role="tablist"] { top: $grid-unit-10 + $grid-unit-20 + $grid-unit-60 - $grid-unit-15; } @@ -118,10 +118,10 @@ $block-inserter-tabs-height: 44px; flex-direction: column; overflow: hidden; - .components-tab-panel__tabs { + div[role="tablist"] { border-bottom: $border-width solid $gray-300; - .components-tab-panel__tabs-item { + button[role="tab"] { flex-grow: 1; margin-bottom: -$border-width; &[id$="reusable"] { @@ -133,7 +133,7 @@ $block-inserter-tabs-height: 44px; } } - .components-tab-panel__tab-content { + div[role="tabpanel"] { display: flex; flex-grow: 1; flex-direction: column; diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index a75e0029ef4900..72b13425bbbe79 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -2,9 +2,16 @@ * WordPress dependencies */ import { useMemo } from '@wordpress/element'; -import { TabPanel } from '@wordpress/components'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); + const blocksTab = { name: 'blocks', /* translators: Blocks tab title in the block inserter. */ @@ -23,11 +30,11 @@ const mediaTab = { }; function InserterTabs( { - children, showPatterns = false, showMedia = false, onSelect, prioritizePatterns, + tabsContents, } ) { const tabs = useMemo( () => { const tempTabs = []; @@ -45,13 +52,26 @@ function InserterTabs( { }, [ prioritizePatterns, showPatterns, showMedia ] ); return ( - <TabPanel - className="block-editor-inserter__tabs" - tabs={ tabs } - onSelect={ onSelect } - > - { children } - </TabPanel> + <div className="block-editor-inserter__tabs"> + <Tabs onSelect={ onSelect }> + <Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.Tab key={ tab.name } tabId={ tab.name }> + { tab.title } + </Tabs.Tab> + ) ) } + </Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.TabPanel + key={ tab.name } + tabId={ tab.name } + focusable={ false } + > + { tabsContents[ tab.name ] } + </Tabs.TabPanel> + ) ) } + </Tabs> + </div> ); } diff --git a/packages/e2e-test-utils/src/inserter.js b/packages/e2e-test-utils/src/inserter.js index ebbda244d18564..a2c73abe666552 100644 --- a/packages/e2e-test-utils/src/inserter.js +++ b/packages/e2e-test-utils/src/inserter.js @@ -86,7 +86,10 @@ export async function selectGlobalInserterTab( label ) { } const activeTab = await page.waitForSelector( - '.block-editor-inserter__tabs button.is-active' + // Targeting a class name is necessary here, because there are likely + // two implementations of the `Tabs` component visible to this test, and + // we want to confirm that it's waiting for the correct one. + '.block-editor-inserter__tabs [role="tab"][aria-selected="true"]' ); const activeTabLabel = await page.evaluate( From b592ba99881818ed52b7d05ae9614e8ab69aa45e Mon Sep 17 00:00:00 2001 From: Anton Vlasenko <43744263+anton-vlasenko@users.noreply.github.com> Date: Wed, 13 Dec 2023 20:08:27 +0100 Subject: [PATCH 168/325] Remove the WordPress-Docs coding standard and all associated changes. (#56982) Remove the WordPress-Docs coding standard and associated changes. Refer to https://github.com/WordPress/gutenberg/issues/56487 for more details. --- phpcs.xml.dist | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 56cd6734e4f3ee..21f3fcb8baee16 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -9,7 +9,6 @@ </rule> <rule ref="WordPress-Core"/> - <rule ref="WordPress-Docs"/> <rule ref="WordPress.WP.I18n"> <properties> <property name="text_domain" type="array"> @@ -59,45 +58,6 @@ <exclude-pattern>./vendor/*</exclude-pattern> <exclude-pattern>./test/php/gutenberg-coding-standards/*</exclude-pattern> - <!-- Exclude files maintained in WordPress Core and backported to Gutenberg --> - <exclude-pattern>./lib/compat/wordpress-*/html-api/*.php</exclude-pattern> - - <!-- These special comments are markers for the build process --> - <rule ref="Squiz.Commenting.InlineComment.WrongStyle"> - <exclude-pattern>gutenberg.php</exclude-pattern> - </rule> - - <!-- Do not require docblocks for unit tests --> - <rule ref="Squiz.Commenting.FunctionComment.Missing"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.FileComment.Missing"> - <exclude-pattern>phpunit/*</exclude-pattern> - <exclude-pattern>test/gutenberg-test-themes/*</exclude-pattern> - <exclude-pattern>**/*.min.asset.php</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.ClassComment.Missing"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.ClassComment.SpacingAfter"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.FunctionComment.MissingParamTag"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Generic.Commenting.DocComment.Empty"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Generic.Commenting.DocComment.MissingShort"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.VariableComment.Missing"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <rule ref="Squiz.Commenting.FunctionCommentThrowTag.Missing"> - <exclude-pattern>phpunit/*</exclude-pattern> - </rule> - <!-- Ignore filename error since it requires WP core build process change --> <rule ref="WordPress.Files.FileName.InvalidClassFileName"> <exclude-pattern>/phpunit/*</exclude-pattern> From 863bc33b31f52721424f27bf8dfd98fd16b24398 Mon Sep 17 00:00:00 2001 From: Derek Blank <derekpblank@gmail.com> Date: Thu, 14 Dec 2023 05:04:10 +0800 Subject: [PATCH 169/325] [RNMobile] Add withIsConnected higher order component (#56966) * Add withIsConnected HOC * fix: Destructure isConnected value from useIsConnected Hook The previous value was always `undefined`. * docs: Avoid permalink in documentation Permalinks would likely referenced outdated code at some point. Documentation is generally expected to be up-to-date. --------- Co-authored-by: David Calhoun <github@davidcalhoun.me> --- .../higher-order/with-is-connected/README.md | 19 +++++++++++++++++++ .../with-is-connected/index.native.js | 18 ++++++++++++++++++ packages/compose/src/index.native.js | 1 + 3 files changed, 38 insertions(+) create mode 100644 packages/compose/src/higher-order/with-is-connected/README.md create mode 100644 packages/compose/src/higher-order/with-is-connected/index.native.js diff --git a/packages/compose/src/higher-order/with-is-connected/README.md b/packages/compose/src/higher-order/with-is-connected/README.md new file mode 100644 index 00000000000000..d1358357f81427 --- /dev/null +++ b/packages/compose/src/higher-order/with-is-connected/README.md @@ -0,0 +1,19 @@ +# withIsConnected + +`withIsConnected` provides a true/false mobile connectivity status based on the `useIsConnected` hook found in the [bridge](https://github.com/WordPress/gutenberg/blob/trunk/packages/react-native-bridge/index.js). + +## Usage +```jsx +/** + * WordPress dependencies + */ +import { withIsConnected } from '@wordpress/compose'; + +export class MyComponent extends Component { + if ( this.props.isConnected !== true ) { + console.log( 'You are currently offline.' ) + } +} + +export default withIsConnected( MyComponent ) +``` \ No newline at end of file diff --git a/packages/compose/src/higher-order/with-is-connected/index.native.js b/packages/compose/src/higher-order/with-is-connected/index.native.js new file mode 100644 index 00000000000000..a24cdfc9173a19 --- /dev/null +++ b/packages/compose/src/higher-order/with-is-connected/index.native.js @@ -0,0 +1,18 @@ +/** + * WordPress dependencies + */ +import { useIsConnected } from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import { createHigherOrderComponent } from '../../utils/create-higher-order-component'; + +const withIsConnected = createHigherOrderComponent( ( WrappedComponent ) => { + return ( props ) => { + const { isConnected } = useIsConnected(); + return <WrappedComponent { ...props } isConnected={ isConnected } />; + }; +}, 'withIsConnected' ); + +export default withIsConnected; diff --git a/packages/compose/src/index.native.js b/packages/compose/src/index.native.js index a3f959e644f32a..00e0a66a360340 100644 --- a/packages/compose/src/index.native.js +++ b/packages/compose/src/index.native.js @@ -17,6 +17,7 @@ export { default as withInstanceId } from './higher-order/with-instance-id'; export { default as withSafeTimeout } from './higher-order/with-safe-timeout'; export { default as withState } from './higher-order/with-state'; export { default as withPreferredColorScheme } from './higher-order/with-preferred-color-scheme'; +export { default as withIsConnected } from './higher-order/with-is-connected'; // Hooks. export { default as useConstrainedTabbing } from './hooks/use-constrained-tabbing'; From fdea164fa3408348e7967bf011fda1eeb6feeef8 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:30:13 +0100 Subject: [PATCH 170/325] Blocks: simplify/optimise isUnmodifiedBlock (#56919) --- packages/blocks/src/api/utils.js | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 60a94117b36e23..0d17836faea7eb 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -18,7 +18,6 @@ import { RichTextData } from '@wordpress/rich-text'; */ import { BLOCK_ICON_DEFAULT } from './constants'; import { getBlockType, getDefaultBlockName } from './registration'; -import { createBlock } from './factory'; extend( [ namesPlugin, a11yPlugin ] ); @@ -39,21 +38,25 @@ const ICON_COLORS = [ '#191e23', '#f8f9f9' ]; * @return {boolean} Whether the block is an unmodified block. */ export function isUnmodifiedBlock( block ) { - // Cache a created default block if no cache exists or the default block - // name changed. - if ( ! isUnmodifiedBlock[ block.name ] ) { - isUnmodifiedBlock[ block.name ] = createBlock( block.name ); - } + return Object.entries( getBlockType( block.name )?.attributes ?? {} ).every( + ( [ key, definition ] ) => { + const value = block.attributes[ key ]; - const newBlock = isUnmodifiedBlock[ block.name ]; - const blockType = getBlockType( block.name ); + // Every attribute that has a default must match the default. + if ( definition.hasOwnProperty( 'default' ) ) { + return value === definition.default; + } - function isEqual( a, b ) { - return ( a?.valueOf() ?? a ) === ( b?.valueOf() ?? b ); - } + // The rich text type is a bit different from the rest because it + // has an implicit default value of an empty RichTextData instance, + // so check the length of the value. + if ( definition.type === 'rich-text' ) { + return ! value?.length; + } - return Object.keys( blockType?.attributes ?? {} ).every( ( key ) => - isEqual( newBlock.attributes[ key ], block.attributes[ key ] ) + // Every attribute that doesn't have a default should be undefined. + return value === undefined; + } ); } From 80281d107c3345cabbbcb864a75da76c4c9216e0 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:47:43 +0100 Subject: [PATCH 171/325] Block: combine store subscriptions (#56994) --- .../components/block-editing-mode/index.js | 6 +- .../src/components/block-list/block.js | 432 +++++++++++++----- ...ck-context.js => private-block-context.js} | 2 +- .../block-list/use-block-props/index.js | 150 ++---- 4 files changed, 348 insertions(+), 242 deletions(-) rename packages/block-editor/src/components/block-list/{block-list-block-context.js => private-block-context.js} (59%) diff --git a/packages/block-editor/src/components/block-editing-mode/index.js b/packages/block-editor/src/components/block-editing-mode/index.js index 5d916d9816e606..20f93f6b36f908 100644 --- a/packages/block-editor/src/components/block-editing-mode/index.js +++ b/packages/block-editor/src/components/block-editing-mode/index.js @@ -2,13 +2,13 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useContext, useEffect } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies */ import { store as blockEditorStore } from '../../store'; -import { BlockListBlockContext } from '../block-list/block-list-block-context'; +import { useBlockEditContext } from '../block-edit/context'; /** * @typedef {'disabled'|'contentOnly'|'default'} BlockEditingMode @@ -45,7 +45,7 @@ import { BlockListBlockContext } from '../block-list/block-list-block-context'; * @return {BlockEditingMode} The current editing mode. */ export function useBlockEditingMode( mode ) { - const { clientId = '' } = useContext( BlockListBlockContext ) ?? {}; + const { clientId = '' } = useBlockEditContext(); const blockEditingMode = useSelect( ( select ) => select( blockEditorStore ).getBlockEditingMode( clientId ), diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index b38dcf3ef1f2ec..0bd5d0b7e199f8 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useMemo, useCallback, RawHTML } from '@wordpress/element'; +import { useCallback, RawHTML, useContext } from '@wordpress/element'; import { getBlockType, getSaveContent, @@ -15,16 +15,13 @@ import { switchToBlockType, getDefaultBlockName, isUnmodifiedBlock, + isReusableBlock, + getBlockDefaultClassName, store as blocksStore, } from '@wordpress/blocks'; import { withFilters } from '@wordpress/components'; -import { - withDispatch, - withSelect, - useDispatch, - useSelect, -} from '@wordpress/data'; -import { compose, pure, ifCondition } from '@wordpress/compose'; +import { withDispatch, useDispatch, useSelect } from '@wordpress/data'; +import { compose, pure } from '@wordpress/compose'; import { safeHTML } from '@wordpress/dom'; /** @@ -38,7 +35,15 @@ import BlockHtml from './block-html'; import { useBlockProps } from './use-block-props'; import { store as blockEditorStore } from '../../store'; import { useLayout } from './layout'; -import { BlockListBlockContext } from './block-list-block-context'; +import { PrivateBlockContext } from './private-block-context'; + +import { unlock } from '../../lock-unlock'; + +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; /** * Merges wrapper props with special handling for classNames and styles. @@ -101,44 +106,11 @@ function BlockListBlock( { toggleSelection, } ) { const { - themeSupportsLayout, - isTemporarilyEditingAsBlocks, - blockEditingMode, mayDisplayControls, mayDisplayParentControls, - } = useSelect( - ( select ) => { - const { - getSettings, - __unstableGetTemporarilyEditingAsBlocks, - getBlockEditingMode, - getBlockName, - isFirstMultiSelectedBlock, - getMultiSelectedBlockClientIds, - hasSelectedInnerBlock, - } = select( blockEditorStore ); - const { hasBlockSupport } = select( blocksStore ); - return { - themeSupportsLayout: getSettings().supportsLayout, - isTemporarilyEditingAsBlocks: - __unstableGetTemporarilyEditingAsBlocks() === clientId, - blockEditingMode: getBlockEditingMode( clientId ), - mayDisplayControls: - isSelected || - ( isFirstMultiSelectedBlock( clientId ) && - getMultiSelectedBlockClientIds().every( - ( id ) => getBlockName( id ) === name - ) ), - mayDisplayParentControls: - hasBlockSupport( - getBlockName( clientId ), - '__experimentalExposeControlsToChildren', - false - ) && hasSelectedInnerBlock( clientId ), - }; - }, - [ clientId, isSelected, name ] - ); + themeSupportsLayout, + ...context + } = useContext( PrivateBlockContext ); const { removeBlock } = useDispatch( blockEditorStore ); const onRemove = useCallback( () => removeBlock( clientId ), [ clientId ] ); @@ -172,12 +144,6 @@ function BlockListBlock( { const blockType = getBlockType( name ); - if ( blockEditingMode === 'disabled' ) { - wrapperProps = { - ...wrapperProps, - tabIndex: -1, - }; - } // Determine whether the block has props to apply to the wrapper. if ( blockType?.getEditWrapperProps ) { wrapperProps = mergeWrapperProps( @@ -241,30 +207,28 @@ function BlockListBlock( { } else if ( blockType?.apiVersion > 1 ) { block = blockEdit; } else { - block = <Block { ...wrapperProps }>{ blockEdit }</Block>; + block = <Block>{ blockEdit }</Block>; } const { 'data-align': dataAlign, ...restWrapperProps } = wrapperProps ?? {}; - const value = { - clientId, - className: classnames( - { - 'is-editing-disabled': blockEditingMode === 'disabled', - 'is-content-locked-temporarily-editing-as-blocks': - isTemporarilyEditingAsBlocks, - }, - dataAlign && themeSupportsLayout && `align${ dataAlign }`, - ! ( dataAlign && isSticky ) && className - ), - wrapperProps: restWrapperProps, - isAligned, - }; - - const memoizedValue = useMemo( () => value, Object.values( value ) ); + restWrapperProps.className = classnames( + restWrapperProps.className, + dataAlign && themeSupportsLayout && `align${ dataAlign }`, + ! ( dataAlign && isSticky ) && className + ); + // We set a new context with the adjusted and filtered wrapperProps (through + // `editor.BlockListBlock`), which the `BlockListBlockProvider` did not have + // access to. return ( - <BlockListBlockContext.Provider value={ memoizedValue }> + <PrivateBlockContext.Provider + value={ { + wrapperProps: restWrapperProps, + isAligned, + ...context, + } } + > <BlockCrashBoundary fallback={ <Block className="has-warning"> @@ -274,52 +238,10 @@ function BlockListBlock( { > { block } </BlockCrashBoundary> - </BlockListBlockContext.Provider> + </PrivateBlockContext.Provider> ); } -const applyWithSelect = withSelect( ( select, { clientId, rootClientId } ) => { - const { - isBlockSelected, - getBlockMode, - isSelectionEnabled, - getTemplateLock, - __unstableGetBlockWithoutInnerBlocks, - canRemoveBlock, - canMoveBlock, - } = select( blockEditorStore ); - const block = __unstableGetBlockWithoutInnerBlocks( clientId ); - const isSelected = isBlockSelected( clientId ); - const templateLock = getTemplateLock( rootClientId ); - const canRemove = canRemoveBlock( clientId, rootClientId ); - const canMove = canMoveBlock( clientId, rootClientId ); - - // The fallback to `{}` is a temporary fix. - // This function should never be called when a block is not present in - // the state. It happens now because the order in withSelect rendering - // is not correct. - const { name, attributes, isValid } = block || {}; - - // Do not add new properties here, use `useSelect` instead to avoid - // leaking new props to the public API (editor.BlockListBlock filter). - return { - mode: getBlockMode( clientId ), - isSelectionEnabled: isSelectionEnabled(), - isLocked: !! templateLock, - canRemove, - canMove, - // Users of the editor.BlockListBlock filter used to be able to - // access the block prop. - // Ideally these blocks would rely on the clientId prop only. - // This is kept for backward compatibility reasons. - block, - name, - attributes, - isValid, - isSelected, - }; -} ); - const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { const { updateBlockAttributes, @@ -558,13 +480,285 @@ const applyWithDispatch = withDispatch( ( dispatch, ownProps, registry ) => { }; } ); -export default compose( - pure, - applyWithSelect, +// This component is used by the BlockListBlockProvider component below. It will +// add the props necessary for the `editor.BlockListBlock` filters. +BlockListBlock = compose( applyWithDispatch, - // Block is sometimes not mounted at the right time, causing it be undefined - // see issue for more info - // https://github.com/WordPress/gutenberg/issues/17013 - ifCondition( ( { block } ) => !! block ), withFilters( 'editor.BlockListBlock' ) )( BlockListBlock ); + +// This component provides all the information we need through a single store +// subscription (useSelect mapping). Only the necesssary props are passed down +// to the BlockListBlock component, which is a filtered component, so these +// props are public API. To avoid adding to the public API, we use a private +// context to pass the rest of the information to the filtered BlockListBlock +// component, and useBlockProps. +function BlockListBlockProvider( props ) { + const { clientId, rootClientId } = props; + const selectedProps = useSelect( + ( select ) => { + const { + isBlockSelected, + getBlockMode, + isSelectionEnabled, + getTemplateLock, + __unstableGetBlockWithoutInnerBlocks, + canRemoveBlock, + canMoveBlock, + + getSettings, + __unstableGetTemporarilyEditingAsBlocks, + getBlockEditingMode, + getBlockName, + isFirstMultiSelectedBlock, + getMultiSelectedBlockClientIds, + hasSelectedInnerBlock, + + getBlockIndex, + isTyping, + getGlobalBlockCount, + isBlockMultiSelected, + isAncestorMultiSelected, + isBlockSubtreeDisabled, + isBlockHighlighted, + __unstableIsFullySelected, + __unstableSelectionHasUnmergeableBlock, + isBlockBeingDragged, + hasBlockMovingClientId, + canInsertBlockType, + getBlockRootClientId, + __unstableHasActiveBlockOverlayActive, + __unstableGetEditorMode, + getSelectedBlocksInitialCaretPosition, + } = unlock( select( blockEditorStore ) ); + const block = __unstableGetBlockWithoutInnerBlocks( clientId ); + + // This is a temporary fix. + // This function should never be called when a block is not + // present in the state. It happens now because the order in + // withSelect rendering is not correct. + if ( ! block ) { + return; + } + + const { + hasBlockSupport: _hasBlockSupport, + getActiveBlockVariation, + } = select( blocksStore ); + const _isSelected = isBlockSelected( clientId ); + const templateLock = getTemplateLock( rootClientId ); + const canRemove = canRemoveBlock( clientId, rootClientId ); + const canMove = canMoveBlock( clientId, rootClientId ); + const { name: blockName, attributes, isValid } = block; + const isPartOfMultiSelection = + isBlockMultiSelected( clientId ) || + isAncestorMultiSelected( clientId ); + const blockType = getBlockType( blockName ); + const match = getActiveBlockVariation( blockName, attributes ); + const { outlineMode, supportsLayout } = getSettings(); + const isMultiSelected = isBlockMultiSelected( clientId ); + const checkDeep = true; + const isAncestorOfSelectedBlock = hasSelectedInnerBlock( + clientId, + checkDeep + ); + const typing = isTyping(); + const hasLightBlockWrapper = blockType?.apiVersion > 1; + const movingClientId = hasBlockMovingClientId(); + + return { + mode: getBlockMode( clientId ), + isSelectionEnabled: isSelectionEnabled(), + isLocked: !! templateLock, + canRemove, + canMove, + // Users of the editor.BlockListBlock filter used to be able to + // access the block prop. + // Ideally these blocks would rely on the clientId prop only. + // This is kept for backward compatibility reasons. + block, + name: blockName, + attributes, + isValid, + isSelected: _isSelected, + themeSupportsLayout: supportsLayout, + isTemporarilyEditingAsBlocks: + __unstableGetTemporarilyEditingAsBlocks() === clientId, + blockEditingMode: getBlockEditingMode( clientId ), + mayDisplayControls: + _isSelected || + ( isFirstMultiSelectedBlock( clientId ) && + getMultiSelectedBlockClientIds().every( + ( id ) => getBlockName( id ) === blockName + ) ), + mayDisplayParentControls: + _hasBlockSupport( + getBlockName( clientId ), + '__experimentalExposeControlsToChildren', + false + ) && hasSelectedInnerBlock( clientId ), + index: getBlockIndex( clientId ), + blockApiVersion: blockType?.apiVersion || 1, + blockTitle: match?.title || blockType?.title, + isPartOfSelection: _isSelected || isPartOfMultiSelection, + adjustScrolling: + _isSelected || isFirstMultiSelectedBlock( clientId ), + enableAnimation: + ! typing && + getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, + isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), + isOutlineEnabled: outlineMode, + hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), + initialPosition: + _isSelected && __unstableGetEditorMode() === 'edit' + ? getSelectedBlocksInitialCaretPosition() + : undefined, + isHighlighted: isBlockHighlighted( clientId ), + isMultiSelected, + isPartiallySelected: + isMultiSelected && + ! __unstableIsFullySelected() && + ! __unstableSelectionHasUnmergeableBlock(), + isReusable: isReusableBlock( blockType ), + isDragging: isBlockBeingDragged( clientId ), + hasChildSelected: isAncestorOfSelectedBlock, + removeOutline: _isSelected && outlineMode && typing, + isBlockMovingMode: !! movingClientId, + canInsertMovingBlock: + movingClientId && + canInsertBlockType( + getBlockName( movingClientId ), + getBlockRootClientId( clientId ) + ), + isEditingDisabled: + getBlockEditingMode( clientId ) === 'disabled', + className: hasLightBlockWrapper + ? attributes.className + : undefined, + defaultClassName: hasLightBlockWrapper + ? getBlockDefaultClassName( blockName ) + : undefined, + }; + }, + [ clientId, rootClientId ] + ); + + const { + mode, + isSelectionEnabled, + isLocked, + canRemove, + canMove, + block, + name, + attributes, + isValid, + isSelected, + themeSupportsLayout, + isTemporarilyEditingAsBlocks, + blockEditingMode, + mayDisplayControls, + mayDisplayParentControls, + index, + blockApiVersion, + blockTitle, + isPartOfSelection, + adjustScrolling, + enableAnimation, + isSubtreeDisabled, + isOutlineEnabled, + hasOverlay, + initialPosition, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + className, + defaultClassName, + } = selectedProps; + + // Block is sometimes not mounted at the right time, causing it be + // undefined see issue for more info + // https://github.com/WordPress/gutenberg/issues/17013 + if ( ! selectedProps ) { + return null; + } + + const privateContext = { + clientId, + className, + index, + mode, + name, + blockApiVersion, + blockTitle, + isSelected, + isPartOfSelection, + adjustScrolling, + enableAnimation, + isSubtreeDisabled, + isOutlineEnabled, + hasOverlay, + initialPosition, + blockEditingMode, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + isTemporarilyEditingAsBlocks, + defaultClassName, + mayDisplayControls, + mayDisplayParentControls, + themeSupportsLayout, + }; + + // Here we separate between the props passed to BlockListBlock and any other + // information we selected for internal use. BlockListBlock is a filtered + // component and thus ALL the props are PUBLIC API. + + // Note that the context value doesn't have to be memoized in this case + // because when it changes, this component will be re-rendered anyway, and + // none of the consumers (BlockListBlock and useBlockProps) are memoized or + // "pure". This is different from the public BlockEditContext, where + // consumers might be memoized or "pure". + return ( + <PrivateBlockContext.Provider value={ privateContext }> + <BlockListBlock + { ...props } + // WARNING: all the following props are public API (through the + // editor.BlockListBlock filter) and normally nothing new should + // be added to it. + { ...{ + mode, + isSelectionEnabled, + isLocked, + canRemove, + canMove, + // Users of the editor.BlockListBlock filter used to be able + // to access the block prop. Ideally these blocks would rely + // on the clientId prop only. This is kept for backward + // compatibility reasons. + block, + name, + attributes, + isValid, + isSelected, + } } + /> + </PrivateBlockContext.Provider> + ); +} + +export default pure( BlockListBlockProvider ); diff --git a/packages/block-editor/src/components/block-list/block-list-block-context.js b/packages/block-editor/src/components/block-list/private-block-context.js similarity index 59% rename from packages/block-editor/src/components/block-list/block-list-block-context.js rename to packages/block-editor/src/components/block-list/private-block-context.js index 6fa09c6969ec59..14981058965019 100644 --- a/packages/block-editor/src/components/block-list/block-list-block-context.js +++ b/packages/block-editor/src/components/block-list/private-block-context.js @@ -3,4 +3,4 @@ */ import { createContext } from '@wordpress/element'; -export const BlockListBlockContext = createContext( null ); +export const PrivateBlockContext = createContext( null ); diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index 593beafa06d83f..fea20506c28a1f 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -8,22 +8,15 @@ import classnames from 'classnames'; */ import { useContext } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; -import { - __unstableGetBlockProps as getBlockProps, - getBlockType, - isReusableBlock, - getBlockDefaultClassName, - store as blocksStore, -} from '@wordpress/blocks'; +import { __unstableGetBlockProps as getBlockProps } from '@wordpress/blocks'; import { useMergeRefs, useDisabled } from '@wordpress/compose'; -import { useSelect } from '@wordpress/data'; import warning from '@wordpress/warning'; /** * Internal dependencies */ import useMovingAnimation from '../../use-moving-animation'; -import { BlockListBlockContext } from '../block-list-block-context'; +import { PrivateBlockContext } from '../private-block-context'; import { useFocusFirstElement } from './use-focus-first-element'; import { useIsHovered } from './use-is-hovered'; import { useBlockEditContext } from '../../block-edit/context'; @@ -32,14 +25,6 @@ import { useEventHandlers } from './use-selected-block-event-handlers'; import { useNavModeExit } from './use-nav-mode-exit'; import { useBlockRefProvider } from './use-block-refs'; import { useIntersectionObserver } from './use-intersection-observer'; -import { store as blockEditorStore } from '../../../store'; -import { unlock } from '../../../lock-unlock'; - -/** - * If the block count exceeds the threshold, we disable the reordering animation - * to avoid laginess. - */ -const BLOCK_ANIMATION_THRESHOLD = 200; /** * This hook is used to lightly mark an element as a block element. The element @@ -89,8 +74,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { className, wrapperProps = {}, isAligned, - } = useContext( BlockListBlockContext ); - const { index, mode, name, @@ -104,104 +87,20 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { isOutlineEnabled, hasOverlay, initialPosition, - classNames, - } = useSelect( - ( select ) => { - const { - getBlockAttributes, - getBlockIndex, - getBlockMode, - getBlockName, - isTyping, - getGlobalBlockCount, - isBlockSelected, - isBlockMultiSelected, - isAncestorMultiSelected, - isFirstMultiSelectedBlock, - isBlockSubtreeDisabled, - getSettings, - isBlockHighlighted, - __unstableIsFullySelected, - __unstableSelectionHasUnmergeableBlock, - isBlockBeingDragged, - hasSelectedInnerBlock, - hasBlockMovingClientId, - canInsertBlockType, - getBlockRootClientId, - __unstableHasActiveBlockOverlayActive, - __unstableGetEditorMode, - getSelectedBlocksInitialCaretPosition, - } = unlock( select( blockEditorStore ) ); - const { getActiveBlockVariation } = select( blocksStore ); - const _isSelected = isBlockSelected( clientId ); - const isPartOfMultiSelection = - isBlockMultiSelected( clientId ) || - isAncestorMultiSelected( clientId ); - const blockName = getBlockName( clientId ); - const blockType = getBlockType( blockName ); - const attributes = getBlockAttributes( clientId ); - const match = getActiveBlockVariation( blockName, attributes ); - const { outlineMode } = getSettings(); - const isMultiSelected = isBlockMultiSelected( clientId ); - const checkDeep = true; - const isAncestorOfSelectedBlock = hasSelectedInnerBlock( - clientId, - checkDeep - ); - const typing = isTyping(); - const hasLightBlockWrapper = blockType?.apiVersion > 1; - const movingClientId = hasBlockMovingClientId(); - - return { - index: getBlockIndex( clientId ), - mode: getBlockMode( clientId ), - name: blockName, - blockApiVersion: blockType?.apiVersion || 1, - blockTitle: match?.title || blockType?.title, - isSelected: _isSelected, - isPartOfSelection: _isSelected || isPartOfMultiSelection, - adjustScrolling: - _isSelected || isFirstMultiSelectedBlock( clientId ), - enableAnimation: - ! typing && - getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, - isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), - isOutlineEnabled: outlineMode, - hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), - initialPosition: - _isSelected && __unstableGetEditorMode() === 'edit' - ? getSelectedBlocksInitialCaretPosition() - : undefined, - classNames: classnames( - { - 'is-selected': _isSelected, - 'is-highlighted': isBlockHighlighted( clientId ), - 'is-multi-selected': isMultiSelected, - 'is-partially-selected': - isMultiSelected && - ! __unstableIsFullySelected() && - ! __unstableSelectionHasUnmergeableBlock(), - 'is-reusable': isReusableBlock( blockType ), - 'is-dragging': isBlockBeingDragged( clientId ), - 'has-child-selected': isAncestorOfSelectedBlock, - 'remove-outline': _isSelected && outlineMode && typing, - 'is-block-moving-mode': !! movingClientId, - 'can-insert-moving-block': - movingClientId && - canInsertBlockType( - getBlockName( movingClientId ), - getBlockRootClientId( clientId ) - ), - }, - hasLightBlockWrapper ? attributes.className : undefined, - hasLightBlockWrapper - ? getBlockDefaultClassName( blockName ) - : undefined - ), - }; - }, - [ clientId ] - ); + blockEditingMode, + isHighlighted, + isMultiSelected, + isPartiallySelected, + isReusable, + isDragging, + hasChildSelected, + removeOutline, + isBlockMovingMode, + canInsertMovingBlock, + isEditingDisabled, + isTemporarilyEditingAsBlocks, + defaultClassName, + } = useContext( PrivateBlockContext ); // translators: %s: Type of block (i.e. Text, Image etc) const blockLabel = sprintf( __( 'Block: %s' ), blockTitle ); @@ -233,7 +132,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { } return { - tabIndex: 0, + tabIndex: blockEditingMode === 'disabled' ? -1 : 0, ...wrapperProps, ...props, ref: mergedRefs, @@ -250,11 +149,24 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { // The wp-block className is important for editor styles. 'wp-block': ! isAligned, 'has-block-overlay': hasOverlay, + 'is-selected': isSelected, + 'is-highlighted': isHighlighted, + 'is-multi-selected': isMultiSelected, + 'is-partially-selected': isPartiallySelected, + 'is-reusable': isReusable, + 'is-dragging': isDragging, + 'has-child-selected': hasChildSelected, + 'remove-outline': removeOutline, + 'is-block-moving-mode': isBlockMovingMode, + 'can-insert-moving-block': canInsertMovingBlock, + 'is-editing-disabled': isEditingDisabled, + 'is-content-locked-temporarily-editing-as-blocks': + isTemporarilyEditingAsBlocks, }, className, props.className, wrapperProps.className, - classNames + defaultClassName ), style: { ...wrapperProps.style, ...props.style }, }; From 57052f36f4bc5370df032b31dba1f87e4047b20d Mon Sep 17 00:00:00 2001 From: Derek Blank <derekpblank@gmail.com> Date: Thu, 14 Dec 2023 05:50:40 +0800 Subject: [PATCH 172/325] [RNMobile] Add OfflineStatus component (#56934) * feat: Frame useNetInfo hook foundation This code is non-functioning currently. * feat: Add iOS connection status bridge utilities This bridge will be required for the planned JavaScript Hook to monitor connection status. * feat: Add `useIsConnected` hook Provides React Hook for monitoring the network connection status via the bridge to the host app. * Revert "feat: Frame useNetInfo hook foundation" This reverts commit a8d3660845457787f6b368fe0276bfcfdbd213a6. * refactor: Align with project Swift syntax Semicolon is unnecessary. Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> * feat: Add Android connection status bridge utilities This bridge enables monitoring the connection status on Android. * feat: Android network connection status request utility Allow the Android platform to request the current network connection status. * fix: Add missing `requestConnectionStatus` bridge method mock The Demo editor fails to build without a mocked bridge method. * Add mobile OfflineStatus component * Add OfflineStatus component to block-list behind __DEV__ flag * Replace offline icon and update OfflineStatus text alignment * Update BEM syntax for OfflineStatus * Update OfflineStatus component colors * test: Import sole native stylesheet to fix test module resolution error The current Jest module resolution configuration will fail when there is only a native-specific file without a web-specific file, e.g. only a `style.native.scss` and no sibling `style.scss` next to it. Explicitly importing the native-specific file avoids the error as Jest does not attempt to seek the non-suffixed file. * test: Mock `useIsConnected` native bridge method The native implementation is not available within the JavaScript testing environment. --------- Co-authored-by: David Calhoun <github@davidcalhoun.me> Co-authored-by: Tanner Stokes <tanner.stokes@automattic.com> --- .../src/components/block-list/index.native.js | 5 ++ .../components/offline-status/index.native.js | 46 +++++++++++++++++++ .../offline-status/style.native.scss | 28 +++++++++++ packages/icons/src/index.js | 1 + packages/icons/src/library/offline.js | 22 +++++++++ test/native/setup.js | 1 + 6 files changed, 103 insertions(+) create mode 100644 packages/block-editor/src/components/offline-status/index.native.js create mode 100644 packages/block-editor/src/components/offline-status/style.native.scss create mode 100644 packages/icons/src/library/offline.js 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 810e23e4c1442a..4c4d7914b18066 100644 --- a/packages/block-editor/src/components/block-list/index.native.js +++ b/packages/block-editor/src/components/block-list/index.native.js @@ -30,6 +30,7 @@ import { import { BlockDraggableWrapper } from '../block-draggable'; import { useEditorWrapperStyles } from '../../hooks/use-editor-wrapper-styles'; import { store as blockEditorStore } from '../../store'; +import OfflineStatus from '../offline-status'; const identity = ( x ) => x; @@ -235,6 +236,10 @@ export default function BlockList( { onLayout={ onLayout } testID="block-list-wrapper" > + { + // eslint-disable-next-line no-undef + __DEV__ && <OfflineStatus /> + } { isRootList ? ( <BlockListProvider value={ { diff --git a/packages/block-editor/src/components/offline-status/index.native.js b/packages/block-editor/src/components/offline-status/index.native.js new file mode 100644 index 00000000000000..ae6007e75103ca --- /dev/null +++ b/packages/block-editor/src/components/offline-status/index.native.js @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import { Text, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { usePreferredColorSchemeStyle } from '@wordpress/compose'; +import { Icon } from '@wordpress/components'; +import { offline as offlineIcon } from '@wordpress/icons'; +import { __ } from '@wordpress/i18n'; +import { useIsConnected } from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import styles from './style.native.scss'; + +const OfflineStatus = () => { + const { isConnected } = useIsConnected(); + + const containerStyle = usePreferredColorSchemeStyle( + styles.offline, + styles.offline__dark + ); + + const textStyle = usePreferredColorSchemeStyle( + styles[ 'offline--text' ], + styles[ 'offline--text__dark' ] + ); + + const iconStyle = usePreferredColorSchemeStyle( + styles[ 'offline--icon' ], + styles[ 'offline--icon__dark' ] + ); + + return ! isConnected ? ( + <View style={ containerStyle }> + <Icon fill={ iconStyle.fill } icon={ offlineIcon } /> + <Text style={ textStyle }>{ __( 'Working Offline' ) }</Text> + </View> + ) : null; +}; + +export default OfflineStatus; diff --git a/packages/block-editor/src/components/offline-status/style.native.scss b/packages/block-editor/src/components/offline-status/style.native.scss new file mode 100644 index 00000000000000..529693516653f6 --- /dev/null +++ b/packages/block-editor/src/components/offline-status/style.native.scss @@ -0,0 +1,28 @@ +.offline { + background-color: $gray-lighten-30; + padding: $grid-unit; + justify-content: center; + align-items: center; + flex-direction: row; +} + +.offline__dark { + background-color: $gray-70; +} + +.offline--text { + color: $black; + padding-left: 3; +} + +.offline--text__dark { + color: $white; +} + +.offline--icon { + fill: $gray-70; +} + +.offline--icon__dark { + fill: $gray-10; +} diff --git a/packages/icons/src/index.js b/packages/icons/src/index.js index 36b29714234429..d743299d35a246 100644 --- a/packages/icons/src/index.js +++ b/packages/icons/src/index.js @@ -190,6 +190,7 @@ export { default as postList } from './library/post-list'; export { default as postTerms } from './library/post-terms'; export { default as previous } from './library/previous'; export { default as next } from './library/next'; +export { default as offline } from './library/offline'; export { default as preformatted } from './library/preformatted'; export { default as pullLeft } from './library/pull-left'; export { default as pullRight } from './library/pull-right'; diff --git a/packages/icons/src/library/offline.js b/packages/icons/src/library/offline.js new file mode 100644 index 00000000000000..f0daa1aaeb79ee --- /dev/null +++ b/packages/icons/src/library/offline.js @@ -0,0 +1,22 @@ +/** + * WordPress dependencies + */ +import { SVG, Path } from '@wordpress/primitives'; + +const offline = ( + <SVG + width="16" + height="16" + viewBox="0 0 16 16" + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <Path + fill-rule="evenodd" + clip-rule="evenodd" + d="M1.36605 2.81332L2.30144 1.87332L13.5592 13.1867L12.6239 14.1267L7.92702 9.40666C6.74618 9.41999 5.57861 9.87999 4.68302 10.78L3.35623 9.44665C4.19874 8.60665 5.2071 8.03999 6.2818 7.75332L4.7958 6.25999C3.78744 6.67332 2.84542 7.29332 2.02944 8.11332L0.702656 6.77999C1.512 5.97332 2.42085 5.33332 3.3894 4.84665L1.36605 2.81332ZM15.2973 6.77999L13.9705 8.11332C12.3054 6.43999 10.1096 5.61332 7.92039 5.62666L6.20883 3.90665C9.41303 3.34665 12.8229 4.29332 15.2973 6.77999ZM10.1759 7.89332C11.0781 8.21332 11.9273 8.72665 12.6438 9.44665L12.1794 9.90665L10.1759 7.89332ZM6.00981 12.1133L8 14.1133L9.99018 12.1133C8.89558 11.0067 7.11105 11.0067 6.00981 12.1133Z" + /> + </SVG> +); + +export default offline; diff --git a/test/native/setup.js b/test/native/setup.js index 3770a4ce3efc6f..0f4c9f9eda20c9 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -107,6 +107,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeShowEditorHelp: jest.fn(), subscribeOnUndoPressed: jest.fn(), subscribeOnRedoPressed: jest.fn(), + useIsConnected: jest.fn( () => ( { isConnected: true } ) ), editorDidMount: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), From 1f89b42fb6876f24794acb5ce9b7b8ab5bdaa2cb Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 13 Dec 2023 23:56:03 +0100 Subject: [PATCH 173/325] Collab editing: ensure block attributes are serialisable (#57025) --- packages/core-data/src/entities.js | 45 ++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/core-data/src/entities.js b/packages/core-data/src/entities.js index a2c60c45aaa032..444d66674d9839 100644 --- a/packages/core-data/src/entities.js +++ b/packages/core-data/src/entities.js @@ -8,6 +8,7 @@ import { capitalCase, pascalCase } from 'change-case'; */ import apiFetch from '@wordpress/api-fetch'; import { __ } from '@wordpress/i18n'; +import { RichTextData } from '@wordpress/rich-text'; /** * Internal dependencies @@ -275,6 +276,29 @@ export const prePersistPostType = ( persistedRecord, edits ) => { return newEdits; }; +const serialisableBlocksCache = new WeakMap(); + +function makeBlockAttributesSerializable( attributes ) { + const newAttributes = { ...attributes }; + for ( const [ key, value ] of Object.entries( attributes ) ) { + if ( value instanceof RichTextData ) { + newAttributes[ key ] = value.valueOf(); + } + } + return newAttributes; +} + +function makeBlocksSerializable( blocks ) { + return blocks.map( ( block ) => { + const { innerBlocks, attributes, ...rest } = block; + return { + ...rest, + attributes: makeBlockAttributesSerializable( attributes ), + innerBlocks: makeBlocksSerializable( innerBlocks ), + }; + } ); +} + /** * Returns the list of post type entities. * @@ -317,12 +341,23 @@ async function loadPostTypeEntities() { }, applyChangesToDoc: ( doc, changes ) => { const document = doc.getMap( 'document' ); + Object.entries( changes ).forEach( ( [ key, value ] ) => { - if ( - document.get( key ) !== value && - typeof value !== 'function' - ) { - document.set( key, value ); + if ( typeof value !== 'function' ) { + if ( key === 'blocks' ) { + if ( ! serialisableBlocksCache.has( value ) ) { + serialisableBlocksCache.set( + value, + makeBlocksSerializable( value ) + ); + } + + value = serialisableBlocksCache.get( value ); + } + + if ( document.get( key ) !== value ) { + document.set( key, value ); + } } } ); }, From 59e89580e4a0e5b9fbf68d07470a339bc8fd7949 Mon Sep 17 00:00:00 2001 From: Brooke <35543432+brookewp@users.noreply.github.com> Date: Wed, 13 Dec 2023 16:16:27 -0800 Subject: [PATCH 174/325] `CustomSelect`: Add `WordPressComponentsProps` (#56998) * CustomSelectControl v2: Add WordPressComponentsProps * Add WordPressComponentProps as button to CustomSelect * Update changelog --- packages/components/CHANGELOG.md | 1 + .../src/custom-select-control-v2/index.tsx | 25 ++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 702aabb14a59d9..fddc4de03d6365 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -29,6 +29,7 @@ ### Experimental - `Tabs`: implement new `tabId` prop ([#56883](https://github.com/WordPress/gutenberg/pull/56883)). +- `CustomSelect`: add `WordPressComponentsProps` for more flexibility ([#56998](https://github.com/WordPress/gutenberg/pull/56998)) ### Experimental diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx index 88231078fa8d56..614f52105513f4 100644 --- a/packages/components/src/custom-select-control-v2/index.tsx +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -18,6 +18,7 @@ import type { CustomSelectItemProps, CustomSelectContext as CustomSelectContextType, } from './types'; +import type { WordPressComponentProps } from '../context'; export const CustomSelectContext = createContext< CustomSelectContextType >( undefined ); @@ -41,17 +42,16 @@ function defaultRenderSelectedValue( value: CustomSelectProps[ 'value' ] ) { return value; } -export function CustomSelect( props: CustomSelectProps ) { - const { - children, - defaultValue, - label, - onChange, - size = 'default', - value, - renderSelectedValue = defaultRenderSelectedValue, - } = props; - +export function CustomSelect( { + children, + defaultValue, + label, + onChange, + size = 'default', + value, + renderSelectedValue = defaultRenderSelectedValue, + ...props +}: WordPressComponentProps< CustomSelectProps, 'button', false > ) { const store = Ariakit.useSelectStore( { setValue: ( nextValue ) => onChange?.( nextValue ), defaultValue, @@ -66,6 +66,7 @@ export function CustomSelect( props: CustomSelectProps ) { { label } </Styled.CustomSelectLabel> <Styled.CustomSelectButton + { ...props } size={ size } hasCustomRenderProp={ !! renderSelectedValue } store={ store } @@ -85,7 +86,7 @@ export function CustomSelect( props: CustomSelectProps ) { export function CustomSelectItem( { children, ...props -}: CustomSelectItemProps ) { +}: WordPressComponentProps< CustomSelectItemProps, 'div', false > ) { const customSelectContext = useContext( CustomSelectContext ); return ( <Styled.CustomSelectItem From e103afb02dc3c814cb95f37a85bc5117cc8458a1 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:29:52 +0900 Subject: [PATCH 175/325] Components: Move kebabCase() function from block-editor package and mark it as private API (#56758) * Components: Move kebabCase() function from block-editor package and mark it as private API * Update changelog * Fix native app test * Try to fix native app test * Try to fix mobile app test * Try importing kebabCase directly from components * Fix another import * Move changelog entry to Internal section * Change the argument type of kebabCase function to unknown * Fix lint error * Merge duplicate Internal sections * Remove type info in the JSDoc comment --------- Co-authored-by: Marin Atanasov <tyxla@abv.bg> --- .../src/components/colors/utils.js | 9 +- .../src/components/colors/with-colors.js | 4 +- .../src/components/font-sizes/utils.js | 8 +- .../global-styles/use-global-styles-output.js | 14 ++- .../block-editor/src/hooks/font-family.js | 4 +- packages/block-editor/src/hooks/layout.js | 5 +- .../src/hooks/use-typography-props.js | 8 +- packages/block-editor/src/private-apis.js | 2 - .../block-editor/src/private-apis.native.js | 2 - packages/block-editor/src/utils/object.js | 35 ------- .../block-editor/src/utils/test/object.js | 97 +------------------ packages/block-library/src/embed/util.js | 4 +- packages/components/CHANGELOG.md | 1 + packages/components/src/index.native.js | 3 + packages/components/src/lock-unlock.js | 10 ++ .../components/src/private-apis.native.js | 13 +++ packages/components/src/private-apis.ts | 9 +- packages/components/src/utils/strings.ts | 32 +++++- packages/components/src/utils/test/strings.js | 97 ++++++++++++++++++- .../collection-font-variant.js | 9 +- .../library-font-variant.js | 9 +- 21 files changed, 214 insertions(+), 161 deletions(-) create mode 100644 packages/components/src/lock-unlock.js create mode 100644 packages/components/src/private-apis.native.js diff --git a/packages/block-editor/src/components/colors/utils.js b/packages/block-editor/src/components/colors/utils.js index 1c1947bfc947cc..d6d51ad0013632 100644 --- a/packages/block-editor/src/components/colors/utils.js +++ b/packages/block-editor/src/components/colors/utils.js @@ -5,10 +5,15 @@ import { colord, extend } from 'colord'; import namesPlugin from 'colord/plugins/names'; import a11yPlugin from 'colord/plugins/a11y'; +/** + * WordPress dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; + /** * Internal dependencies */ -import { kebabCase } from '../../utils/object'; +import { unlock } from '../../lock-unlock'; extend( [ namesPlugin, a11yPlugin ] ); @@ -70,6 +75,8 @@ export function getColorClassName( colorContextName, colorSlug ) { return undefined; } + const { kebabCase } = unlock( componentsPrivateApis ); + return `has-${ kebabCase( colorSlug ) }-${ colorContextName }`; } diff --git a/packages/block-editor/src/components/colors/with-colors.js b/packages/block-editor/src/components/colors/with-colors.js index 5946ca90d8bbde..33079f8b409d6e 100644 --- a/packages/block-editor/src/components/colors/with-colors.js +++ b/packages/block-editor/src/components/colors/with-colors.js @@ -3,6 +3,7 @@ */ import { useMemo, Component } from '@wordpress/element'; import { compose, createHigherOrderComponent } from '@wordpress/compose'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies @@ -14,7 +15,7 @@ import { getMostReadableColor, } from './utils'; import { useSettings } from '../use-settings'; -import { kebabCase } from '../../utils/object'; +import { unlock } from '../../lock-unlock'; /** * Capitalizes the first letter in a string. @@ -79,6 +80,7 @@ const withEditorColorPalette = () => * @return {Component} The component that can be used as a HOC. */ function createColorHOC( colorTypes, withColorPalette ) { + const { kebabCase } = unlock( componentsPrivateApis ); const colorMap = colorTypes.reduce( ( colorObject, colorType ) => { return { ...colorObject, diff --git a/packages/block-editor/src/components/font-sizes/utils.js b/packages/block-editor/src/components/font-sizes/utils.js index 2f874f6665f8f3..dff28c7a770d40 100644 --- a/packages/block-editor/src/components/font-sizes/utils.js +++ b/packages/block-editor/src/components/font-sizes/utils.js @@ -1,7 +1,12 @@ +/** + * WordPress dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; + /** * Internal dependencies */ -import { kebabCase } from '../../utils/object'; +import { unlock } from '../../lock-unlock'; /** * Returns the font size object based on an array of named font sizes and the namedFontSize and customFontSize values. @@ -64,5 +69,6 @@ export function getFontSizeClass( fontSizeSlug ) { return; } + const { kebabCase } = unlock( componentsPrivateApis ); return `has-${ kebabCase( fontSizeSlug ) }-font-size`; } diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index 7e99eca355b52e..1cd63ef4d03f00 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -11,6 +11,7 @@ import { import { useSelect } from '@wordpress/data'; import { useContext, useMemo } from '@wordpress/element'; import { getCSSRules } from '@wordpress/style-engine'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies @@ -32,12 +33,9 @@ import { getDuotoneFilter } from '../duotone/utils'; import { getGapCSSValue } from '../../hooks/gap'; import { store as blockEditorStore } from '../../store'; import { LAYOUT_DEFINITIONS } from '../../layouts/definitions'; -import { - getValueFromObjectPath, - kebabCase, - setImmutably, -} from '../../utils/object'; +import { getValueFromObjectPath, setImmutably } from '../../utils/object'; import BlockContext from '../block-context'; +import { unlock } from '../../lock-unlock'; // List of block support features that can have their related styles // generated under their own feature level selector rather than the block's. @@ -72,6 +70,8 @@ function compileStyleValue( uncompiledValue ) { * @return {Array<Object>} An array of style declarations. */ function getPresetsDeclarations( blockPresets = {}, mergedSettings ) { + const { kebabCase } = unlock( componentsPrivateApis ); + return PRESET_METADATA.reduce( ( declarations, { path, valueKey, valueFunc, cssVarInfix } ) => { const presetByOrigin = getValueFromObjectPath( @@ -116,6 +116,8 @@ function getPresetsDeclarations( blockPresets = {}, mergedSettings ) { * @return {string} CSS declarations for the preset classes. */ function getPresetsClasses( blockSelector = '*', blockPresets = {} ) { + const { kebabCase } = unlock( componentsPrivateApis ); + return PRESET_METADATA.reduce( ( declarations, { path, cssVarInfix, classes } ) => { if ( ! classes ) { @@ -180,6 +182,7 @@ function getPresetsSvgFilters( blockPresets = {} ) { } function flattenTree( input = {}, prefix, token ) { + const { kebabCase } = unlock( componentsPrivateApis ); let result = []; Object.keys( input ).forEach( ( key ) => { const newKey = prefix + kebabCase( key.replace( '/', '-' ) ); @@ -321,6 +324,7 @@ export function getStylesDeclarations( tree = {}, isTemplate = true ) { + const { kebabCase } = unlock( componentsPrivateApis ); const isRoot = ROOT_BLOCK_SELECTOR === selector; const output = Object.entries( STYLE_PROPERTY ).reduce( ( diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index 36266d59adcf2c..ae41b7fa34b1f5 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -4,13 +4,14 @@ import { addFilter } from '@wordpress/hooks'; import { hasBlockSupport } from '@wordpress/blocks'; import TokenList from '@wordpress/token-list'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ import { shouldSkipSerialization } from './utils'; import { TYPOGRAPHY_SUPPORT_KEY } from './typography'; -import { kebabCase } from '../utils/object'; +import { unlock } from '../lock-unlock'; export const FONT_FAMILY_SUPPORT_KEY = 'typography.__experimentalFontFamily'; @@ -67,6 +68,7 @@ function addSaveProps( props, blockType, attributes ) { // Use TokenList to dedupe classes. const classes = new TokenList( props.className ); + const { kebabCase } = unlock( componentsPrivateApis ); classes.add( `has-${ kebabCase( attributes?.fontFamily ) }-font-family` ); const newClassName = classes.value; props.className = newClassName ? newClassName : undefined; diff --git a/packages/block-editor/src/hooks/layout.js b/packages/block-editor/src/hooks/layout.js index 18bb46a87a1b87..54824558cb7036 100644 --- a/packages/block-editor/src/hooks/layout.js +++ b/packages/block-editor/src/hooks/layout.js @@ -15,6 +15,7 @@ import { ButtonGroup, ToggleControl, PanelBody, + privateApis as componentsPrivateApis, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -27,8 +28,8 @@ import { useSettings } from '../components/use-settings'; import { getLayoutType, getLayoutTypes } from '../layouts'; import { useBlockEditingMode } from '../components/block-editing-mode'; import { LAYOUT_DEFINITIONS } from '../layouts/definitions'; -import { kebabCase } from '../utils/object'; import { useBlockSettings, useStyleOverride } from './utils'; +import { unlock } from '../lock-unlock'; const layoutBlockSupportKey = 'layout'; @@ -48,6 +49,7 @@ function hasLayoutBlockSupport( blockName ) { * @return { Array } Array of CSS classname strings. */ export function useLayoutClasses( blockAttributes = {}, blockName = '' ) { + const { kebabCase } = unlock( componentsPrivateApis ); const rootPaddingAlignment = useSelect( ( select ) => { const { getSettings } = select( blockEditorStore ); return getSettings().__experimentalFeatures @@ -348,6 +350,7 @@ function BlockWithLayoutStyles( { block: BlockListBlock, props } ) { : layout || defaultBlockLayout || {}; const layoutClasses = useLayoutClasses( attributes, name ); + const { kebabCase } = unlock( componentsPrivateApis ); const selectorPrefix = `wp-container-${ kebabCase( name ) }-layout-`; // Higher specificity to override defaults from theme.json. const selector = `.${ selectorPrefix }${ id }.${ selectorPrefix }${ id }`; diff --git a/packages/block-editor/src/hooks/use-typography-props.js b/packages/block-editor/src/hooks/use-typography-props.js index 1ed02d4a5835f2..14f5874c1422c6 100644 --- a/packages/block-editor/src/hooks/use-typography-props.js +++ b/packages/block-editor/src/hooks/use-typography-props.js @@ -3,6 +3,11 @@ */ import classnames from 'classnames'; +/** + * WordPress dependencies + */ +import { privateApis as componentsPrivateApis } from '@wordpress/components'; + /** * Internal dependencies */ @@ -12,7 +17,7 @@ import { getTypographyFontSizeValue, getFluidTypographyOptionsFromSettings, } from '../components/global-styles/typography-utils'; -import { kebabCase } from '../utils/object'; +import { unlock } from '../lock-unlock'; /* * This utility is intended to assist where the serialization of the typography @@ -29,6 +34,7 @@ import { kebabCase } from '../utils/object'; * @return {Object} Typography block support derived CSS classes & styles. */ export function getTypographyClassesAndStyles( attributes, settings ) { + const { kebabCase } = unlock( componentsPrivateApis ); let typographyStyles = attributes?.style?.typography || {}; const fluidTypographySettings = getFluidTypographyOptionsFromSettings( settings ); diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 9837c206487bea..ff86f07aa4caa4 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -5,7 +5,6 @@ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; import { lock } from './lock-unlock'; import { getRichTextValues } from './components/rich-text/get-rich-text-values'; -import { kebabCase } from './utils/object'; import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; @@ -36,7 +35,6 @@ lock( privateApis, { ExperimentalBlockEditorProvider, getDuotoneFilter, getRichTextValues, - kebabCase, PrivateInserter, PrivateListView, ResizableBoxPopover, diff --git a/packages/block-editor/src/private-apis.native.js b/packages/block-editor/src/private-apis.native.js index 17676f634b1cae..5555e00477e7b5 100644 --- a/packages/block-editor/src/private-apis.native.js +++ b/packages/block-editor/src/private-apis.native.js @@ -3,7 +3,6 @@ */ import * as globalStyles from './components/global-styles'; import { ExperimentalBlockEditorProvider } from './components/provider'; -import { kebabCase } from './utils/object'; import { lock } from './lock-unlock'; /** @@ -12,6 +11,5 @@ import { lock } from './lock-unlock'; export const privateApis = {}; lock( privateApis, { ...globalStyles, - kebabCase, ExperimentalBlockEditorProvider, } ); diff --git a/packages/block-editor/src/utils/object.js b/packages/block-editor/src/utils/object.js index 52380564264174..8f6c82a9c3991e 100644 --- a/packages/block-editor/src/utils/object.js +++ b/packages/block-editor/src/utils/object.js @@ -1,38 +1,3 @@ -/** - * External dependencies - */ -import { paramCase } from 'change-case'; - -/** - * Converts any string to kebab case. - * Backwards compatible with Lodash's `_.kebabCase()`. - * Backwards compatible with `_wp_to_kebab_case()`. - * - * @see https://lodash.com/docs/4.17.15#kebabCase - * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ - * - * @param {string} str String to convert. - * @return {string} Kebab-cased string - */ -export function kebabCase( str ) { - let input = str; - if ( typeof str !== 'string' ) { - input = str?.toString?.() ?? ''; - } - - // See https://github.com/lodash/lodash/blob/b185fcee26b2133bd071f4aaca14b455c2ed1008/lodash.js#L4970 - input = input.replace( /['\u2019]/, '' ); - - return paramCase( input, { - splitRegexp: [ - /(?!(?:1ST|2ND|3RD|[4-9]TH)(?![a-z]))([a-z0-9])([A-Z])/g, // fooBar => foo-bar, 3Bar => 3-bar - /(?!(?:1st|2nd|3rd|[4-9]th)(?![a-z]))([0-9])([a-z])/g, // 3bar => 3-bar - /([A-Za-z])([0-9])/g, // Foo3 => foo-3, foo3 => foo-3 - /([A-Z])([A-Z][a-z])/g, // FOOBar => foo-bar - ], - } ); -} - /** * Immutably sets a value inside an object. Like `lodash#set`, but returning a * new object. Treats nullish initial values as empty objects. Clones any diff --git a/packages/block-editor/src/utils/test/object.js b/packages/block-editor/src/utils/test/object.js index 87f01375df311d..28f2fc7381cd80 100644 --- a/packages/block-editor/src/utils/test/object.js +++ b/packages/block-editor/src/utils/test/object.js @@ -1,102 +1,7 @@ /** * Internal dependencies */ -import { kebabCase, setImmutably } from '../object'; - -describe( 'kebabCase', () => { - it( 'separates lowercase letters, followed by uppercase letters', () => { - expect( kebabCase( 'fooBar' ) ).toEqual( 'foo-bar' ); - } ); - - it( 'separates numbers, followed by uppercase letters', () => { - expect( kebabCase( '123FOO' ) ).toEqual( '123-foo' ); - } ); - - it( 'separates numbers, followed by lowercase characters', () => { - expect( kebabCase( '123bar' ) ).toEqual( '123-bar' ); - } ); - - it( 'separates uppercase letters, followed by numbers', () => { - expect( kebabCase( 'FOO123' ) ).toEqual( 'foo-123' ); - } ); - - it( 'separates lowercase letters, followed by numbers', () => { - expect( kebabCase( 'foo123' ) ).toEqual( 'foo-123' ); - } ); - - it( 'separates uppercase groups from capitalized groups', () => { - expect( kebabCase( 'FOOBar' ) ).toEqual( 'foo-bar' ); - } ); - - it( 'removes any non-dash special characters', () => { - expect( - kebabCase( 'foo±§!@#$%^&*()-_=+/?.>,<\\|{}[]`~\'";:bar' ) - ).toEqual( 'foo-bar' ); - } ); - - it( 'removes any spacing characters', () => { - expect( kebabCase( ' foo \t \n \r \f \v bar ' ) ).toEqual( 'foo-bar' ); - } ); - - it( 'groups multiple dashes into a single one', () => { - expect( kebabCase( 'foo---bar' ) ).toEqual( 'foo-bar' ); - } ); - - it( 'returns an empty string unchanged', () => { - expect( kebabCase( '' ) ).toEqual( '' ); - } ); - - it( 'returns an existing kebab case string unchanged', () => { - expect( kebabCase( 'foo-123-bar' ) ).toEqual( 'foo-123-bar' ); - } ); - - it( 'returns an empty string if any nullish type is passed', () => { - expect( kebabCase( undefined ) ).toEqual( '' ); - expect( kebabCase( null ) ).toEqual( '' ); - } ); - - it( 'converts any unexpected non-nullish type to a string', () => { - expect( kebabCase( 12345 ) ).toEqual( '12345' ); - expect( kebabCase( [] ) ).toEqual( '' ); - expect( kebabCase( {} ) ).toEqual( 'object-object' ); - } ); - - /** - * Should cover all test cases of `_wp_to_kebab_case()`. - * - * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ - * @see https://github.com/WordPress/wordpress-develop/blob/76376fdbc3dc0b3261de377dffc350677345e7ba/tests/phpunit/tests/functions/wpToKebabCase.php#L35-L62 - */ - it.each( [ - [ 'white', 'white' ], - [ 'white+black', 'white-black' ], - [ 'white:black', 'white-black' ], - [ 'white*black', 'white-black' ], - [ 'white.black', 'white-black' ], - [ 'white black', 'white-black' ], - [ 'white black', 'white-black' ], - [ 'white-to-black', 'white-to-black' ], - [ 'white2white', 'white-2-white' ], - [ 'white2nd', 'white-2nd' ], - [ 'white2ndcolor', 'white-2-ndcolor' ], - [ 'white2ndColor', 'white-2nd-color' ], - [ 'white2nd_color', 'white-2nd-color' ], - [ 'white23color', 'white-23-color' ], - [ 'white23', 'white-23' ], - [ '23color', '23-color' ], - [ 'white4th', 'white-4th' ], - [ 'font2xl', 'font-2-xl' ], - [ 'whiteToWhite', 'white-to-white' ], - [ 'whiteTOwhite', 'white-t-owhite' ], - [ 'WHITEtoWHITE', 'whit-eto-white' ], - [ 42, '42' ], - [ "i've done", 'ive-done' ], - [ '#ffffff', 'ffffff' ], - [ '$ffffff', 'ffffff' ], - ] )( 'converts %s properly to %s', ( input, expected ) => { - expect( kebabCase( input ) ).toEqual( expected ); - } ); -} ); +import { setImmutably } from '../object'; describe( 'setImmutably', () => { describe( 'handling falsy values properly', () => { diff --git a/packages/block-library/src/embed/util.js b/packages/block-library/src/embed/util.js index a7a6ea219f2772..c591c5d19e2d22 100644 --- a/packages/block-library/src/embed/util.js +++ b/packages/block-library/src/embed/util.js @@ -7,7 +7,7 @@ import memoize from 'memize'; /** * WordPress dependencies */ -import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { renderToString } from '@wordpress/element'; import { createBlock, @@ -23,7 +23,6 @@ import { ASPECT_RATIOS, WP_EMBED_TYPE } from './constants'; import { unlock } from '../lock-unlock'; const { name: DEFAULT_EMBED_BLOCK } = metadata; -const { kebabCase } = unlock( blockEditorPrivateApis ); /** @typedef {import('@wordpress/blocks').WPBlockVariation} WPBlockVariation */ @@ -283,6 +282,7 @@ export const getAttributesFromPreview = memoize( // If we got a provider name from the API, use it for the slug, otherwise we use the title, // because not all embed code gives us a provider name. const { html, provider_name: providerName } = preview; + const { kebabCase } = unlock( componentsPrivateApis ); const providerNameSlug = kebabCase( ( providerName || title ).toLowerCase() ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index fddc4de03d6365..acf8112b4111ac 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -25,6 +25,7 @@ ### Internal - `DropdownMenuV2Ariakit`: prevent prefix collapsing if all radios or checkboxes are unselected ([#56720](https://github.com/WordPress/gutenberg/pull/56720)). +- Move `kebabCase()` function from `block-editor` package and mark it as private API ([#56758](https://github.com/WordPress/gutenberg/pull/56758)). ### Experimental diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index dc8a77ad77d1e5..f2c1591dc3ce2c 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -141,3 +141,6 @@ export { useMobileGlobalStylesColors, useEditorColorScheme, } from './mobile/global-styles-context/utils'; + +// Private APIs. +export { privateApis } from './private-apis'; diff --git a/packages/components/src/lock-unlock.js b/packages/components/src/lock-unlock.js new file mode 100644 index 00000000000000..1525ece158072e --- /dev/null +++ b/packages/components/src/lock-unlock.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { __dangerousOptInToUnstableAPIsOnlyForCoreModules } from '@wordpress/private-apis'; + +export const { lock, unlock } = + __dangerousOptInToUnstableAPIsOnlyForCoreModules( + 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', + '@wordpress/components' + ); diff --git a/packages/components/src/private-apis.native.js b/packages/components/src/private-apis.native.js new file mode 100644 index 00000000000000..659de19f39137b --- /dev/null +++ b/packages/components/src/private-apis.native.js @@ -0,0 +1,13 @@ +/** + * Internal dependencies + */ +import { kebabCase } from './utils/strings'; +import { lock } from './lock-unlock'; + +/** + * Private @wordpress/components APIs. + */ +export const privateApis = {}; +lock( privateApis, { + kebabCase, +} ); diff --git a/packages/components/src/private-apis.ts b/packages/components/src/private-apis.ts index fb4679dbc34234..ba0048407574e7 100644 --- a/packages/components/src/private-apis.ts +++ b/packages/components/src/private-apis.ts @@ -42,12 +42,8 @@ import { import { ComponentsContext } from './context/context-system-provider'; import Theme from './theme'; import Tabs from './tabs'; - -export const { lock, unlock } = - __dangerousOptInToUnstableAPIsOnlyForCoreModules( - 'I know using unstable features means my theme or plugin will inevitably break in the next version of WordPress.', - '@wordpress/components' - ); +import { kebabCase } from './utils/strings'; +import { lock } from './lock-unlock'; export const privateApis = {}; lock( privateApis, { @@ -81,4 +77,5 @@ lock( privateApis, { DropdownMenuSeparatorV2Ariakit, DropdownMenuItemLabelV2Ariakit, DropdownMenuItemHelpTextV2Ariakit, + kebabCase, } ); diff --git a/packages/components/src/utils/strings.ts b/packages/components/src/utils/strings.ts index e4d1d8f73bda1e..bb43a5e53a4050 100644 --- a/packages/components/src/utils/strings.ts +++ b/packages/components/src/utils/strings.ts @@ -2,6 +2,7 @@ * External dependencies */ import removeAccents from 'remove-accents'; +import { paramCase } from 'change-case'; const ALL_UNICODE_DASH_CHARACTERS = new RegExp( `[${ [ @@ -71,12 +72,39 @@ export const normalizeTextString = ( value: string ): string => { .replace( ALL_UNICODE_DASH_CHARACTERS, '-' ); }; +/** + * Converts any string to kebab case. + * Backwards compatible with Lodash's `_.kebabCase()`. + * Backwards compatible with `_wp_to_kebab_case()`. + * + * @see https://lodash.com/docs/4.17.15#kebabCase + * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ + * + * @param str String to convert. + * @return Kebab-cased string + */ +export function kebabCase( str: unknown ) { + let input = str?.toString?.() ?? ''; + + // See https://github.com/lodash/lodash/blob/b185fcee26b2133bd071f4aaca14b455c2ed1008/lodash.js#L4970 + input = input.replace( /['\u2019]/, '' ); + + return paramCase( input, { + splitRegexp: [ + /(?!(?:1ST|2ND|3RD|[4-9]TH)(?![a-z]))([a-z0-9])([A-Z])/g, // fooBar => foo-bar, 3Bar => 3-bar + /(?!(?:1st|2nd|3rd|[4-9]th)(?![a-z]))([0-9])([a-z])/g, // 3bar => 3-bar + /([A-Za-z])([0-9])/g, // Foo3 => foo-3, foo3 => foo-3 + /([A-Z])([A-Z][a-z])/g, // FOOBar => foo-bar + ], + } ); +} + /** * Escapes the RegExp special characters. * - * @param {string} string Input string. + * @param string Input string. * - * @return {string} Regex-escaped string. + * @return Regex-escaped string. */ export function escapeRegExp( string: string ): string { return string.replace( /[\\^$.*+?()[\]{}|]/g, '\\$&' ); diff --git a/packages/components/src/utils/test/strings.js b/packages/components/src/utils/test/strings.js index 43682a0e2853f3..2c7d9641260f5f 100644 --- a/packages/components/src/utils/test/strings.js +++ b/packages/components/src/utils/test/strings.js @@ -1,7 +1,102 @@ /** * Internal dependencies */ -import { normalizeTextString } from '../strings'; +import { kebabCase, normalizeTextString } from '../strings'; + +describe( 'kebabCase', () => { + it( 'separates lowercase letters, followed by uppercase letters', () => { + expect( kebabCase( 'fooBar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'separates numbers, followed by uppercase letters', () => { + expect( kebabCase( '123FOO' ) ).toEqual( '123-foo' ); + } ); + + it( 'separates numbers, followed by lowercase characters', () => { + expect( kebabCase( '123bar' ) ).toEqual( '123-bar' ); + } ); + + it( 'separates uppercase letters, followed by numbers', () => { + expect( kebabCase( 'FOO123' ) ).toEqual( 'foo-123' ); + } ); + + it( 'separates lowercase letters, followed by numbers', () => { + expect( kebabCase( 'foo123' ) ).toEqual( 'foo-123' ); + } ); + + it( 'separates uppercase groups from capitalized groups', () => { + expect( kebabCase( 'FOOBar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'removes any non-dash special characters', () => { + expect( + kebabCase( 'foo±§!@#$%^&*()-_=+/?.>,<\\|{}[]`~\'";:bar' ) + ).toEqual( 'foo-bar' ); + } ); + + it( 'removes any spacing characters', () => { + expect( kebabCase( ' foo \t \n \r \f \v bar ' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'groups multiple dashes into a single one', () => { + expect( kebabCase( 'foo---bar' ) ).toEqual( 'foo-bar' ); + } ); + + it( 'returns an empty string unchanged', () => { + expect( kebabCase( '' ) ).toEqual( '' ); + } ); + + it( 'returns an existing kebab case string unchanged', () => { + expect( kebabCase( 'foo-123-bar' ) ).toEqual( 'foo-123-bar' ); + } ); + + it( 'returns an empty string if any nullish type is passed', () => { + expect( kebabCase( undefined ) ).toEqual( '' ); + expect( kebabCase( null ) ).toEqual( '' ); + } ); + + it( 'converts any unexpected non-nullish type to a string', () => { + expect( kebabCase( 12345 ) ).toEqual( '12345' ); + expect( kebabCase( [] ) ).toEqual( '' ); + expect( kebabCase( {} ) ).toEqual( 'object-object' ); + } ); + + /** + * Should cover all test cases of `_wp_to_kebab_case()`. + * + * @see https://developer.wordpress.org/reference/functions/_wp_to_kebab_case/ + * @see https://github.com/WordPress/wordpress-develop/blob/76376fdbc3dc0b3261de377dffc350677345e7ba/tests/phpunit/tests/functions/wpToKebabCase.php#L35-L62 + */ + it.each( [ + [ 'white', 'white' ], + [ 'white+black', 'white-black' ], + [ 'white:black', 'white-black' ], + [ 'white*black', 'white-black' ], + [ 'white.black', 'white-black' ], + [ 'white black', 'white-black' ], + [ 'white black', 'white-black' ], + [ 'white-to-black', 'white-to-black' ], + [ 'white2white', 'white-2-white' ], + [ 'white2nd', 'white-2nd' ], + [ 'white2ndcolor', 'white-2-ndcolor' ], + [ 'white2ndColor', 'white-2nd-color' ], + [ 'white2nd_color', 'white-2nd-color' ], + [ 'white23color', 'white-23-color' ], + [ 'white23', 'white-23' ], + [ '23color', '23-color' ], + [ 'white4th', 'white-4th' ], + [ 'font2xl', 'font-2-xl' ], + [ 'whiteToWhite', 'white-to-white' ], + [ 'whiteTOwhite', 'white-t-owhite' ], + [ 'WHITEtoWHITE', 'whit-eto-white' ], + [ 42, '42' ], + [ "i've done", 'ive-done' ], + [ '#ffffff', 'ffffff' ], + [ '$ffffff', 'ffffff' ], + ] )( 'converts %s properly to %s', ( input, expected ) => { + expect( kebabCase( input ) ).toEqual( expected ); + } ); +} ); describe( 'normalizeTextString', () => { it( 'should normalize hyphen-like characters to hyphens', () => { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js index 8c2b36d3adee9f..6108ca7669c5bb 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/collection-font-variant.js @@ -1,14 +1,18 @@ /** * WordPress dependencies */ -import { CheckboxControl, Flex } from '@wordpress/components'; +import { + CheckboxControl, + Flex, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; /** * Internal dependencies */ import { getFontFaceVariantName } from './utils'; import FontFaceDemo from './font-demo'; -import { kebabCase } from '../../../../../block-editor/src/utils/object'; +import { unlock } from '../../../lock-unlock'; function CollectionFontVariant( { face, @@ -25,6 +29,7 @@ function CollectionFontVariant( { }; const displayName = font.name + ' ' + getFontFaceVariantName( face ); + const { kebabCase } = unlock( componentsPrivateApis ); const checkboxId = kebabCase( `${ font.slug }-${ getFontFaceVariantName( face ) }` ); diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js index 010f3efdbeb91a..d74a5f74f019b7 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/library-font-variant.js @@ -2,7 +2,11 @@ * WordPress dependencies */ import { useContext } from '@wordpress/element'; -import { CheckboxControl, Flex } from '@wordpress/components'; +import { + CheckboxControl, + Flex, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; /** * Internal dependencies @@ -10,7 +14,7 @@ import { CheckboxControl, Flex } from '@wordpress/components'; import { getFontFaceVariantName } from './utils'; import { FontLibraryContext } from './context'; import FontFaceDemo from './font-demo'; -import { kebabCase } from '../../../../../block-editor/src/utils/object'; +import { unlock } from '../../../lock-unlock'; function LibraryFontVariant( { face, font } ) { const { isFontActivated, toggleActivateFont } = @@ -34,6 +38,7 @@ function LibraryFontVariant( { face, font } ) { }; const displayName = font.name + ' ' + getFontFaceVariantName( face ); + const { kebabCase } = unlock( componentsPrivateApis ); const checkboxId = kebabCase( `${ font.slug }-${ getFontFaceVariantName( face ) }` ); From 2fb3c57bcadc675f9c6745a99d957b3924e39b71 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 14 Dec 2023 14:23:05 +1100 Subject: [PATCH 176/325] The revisions button is a permanent member of the global styles sidebar, so it doesn't need to be included via fill. (#57034) This commit moves the button from ui.js to the sidebar component in the hope that it will make both maintenance and working with parallel states, e.g., style book visibility easier later. --- .../src/components/global-styles/ui.js | 56 +------------------ .../global-styles-sidebar.js | 48 +++++++++++++++- 2 files changed, 46 insertions(+), 58 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index c8d72205c3bed8..8f602b1abb3934 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -6,7 +6,6 @@ import { __experimentalNavigatorScreen as NavigatorScreen, __experimentalUseNavigator as useNavigator, createSlotFill, - Button, DropdownMenu, MenuGroup, MenuItem, @@ -19,7 +18,7 @@ import { } from '@wordpress/block-editor'; import { __ } from '@wordpress/i18n'; import { store as preferencesStore } from '@wordpress/preferences'; -import { backup, moreVertical } from '@wordpress/icons'; +import { moreVertical } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; import { useEffect } from '@wordpress/element'; @@ -115,58 +114,6 @@ function GlobalStylesActionMenu() { ); } -function GlobalStylesRevisionsMenu() { - const { setIsListViewOpened } = useDispatch( editSiteStore ); - const { revisionsCount } = useSelect( ( select ) => { - const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = - select( coreStore ); - - const globalStylesId = __experimentalGetCurrentGlobalStylesId(); - const globalStyles = globalStylesId - ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) - : undefined; - - return { - revisionsCount: - globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0, - }; - }, [] ); - const { goTo } = useNavigator(); - const { setEditorCanvasContainerView } = unlock( - useDispatch( editSiteStore ) - ); - const isRevisionsOpened = useSelect( - ( select ) => - 'global-styles-revisions' === - unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), - [] - ); - const loadRevisions = () => { - setIsListViewOpened( false ); - - if ( ! isRevisionsOpened ) { - goTo( '/revisions' ); - setEditorCanvasContainerView( 'global-styles-revisions' ); - } else { - goTo( '/' ); - setEditorCanvasContainerView( undefined ); - } - }; - const hasRevisions = revisionsCount > 0; - - return ( - <GlobalStylesMenuFill> - <Button - label={ __( 'Revisions' ) } - icon={ backup } - onClick={ loadRevisions } - disabled={ ! hasRevisions } - isPressed={ isRevisionsOpened } - /> - </GlobalStylesMenuFill> - ); -} - function GlobalStylesNavigationScreen( { className, ...props } ) { return ( <NavigatorScreen @@ -403,7 +350,6 @@ function GlobalStylesUI() { <GlobalStylesStyleBook /> ) } - <GlobalStylesRevisionsMenu /> <GlobalStylesActionMenu /> <GlobalStylesBlockLink /> <GlobalStylesEditorCanvasContainerLink /> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js index a3be71723e8730..6edb12e9ab053d 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js @@ -1,9 +1,15 @@ /** * WordPress dependencies */ -import { FlexItem, FlexBlock, Flex, Button } from '@wordpress/components'; +import { + FlexItem, + FlexBlock, + Flex, + Button, + __experimentalUseNavigator as useNavigator, +} from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { styles, seen } from '@wordpress/icons'; +import { styles, seen, backup } from '@wordpress/icons'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { store as interfaceStore } from '@wordpress/interface'; @@ -17,17 +23,21 @@ import { GlobalStylesUI } from '../global-styles'; import { store as editSiteStore } from '../../store'; import { GlobalStylesMenuSlot } from '../global-styles/ui'; import { unlock } from '../../lock-unlock'; +import { store as coreStore } from '@wordpress/core-data'; export default function GlobalStylesSidebar() { const { shouldClearCanvasContainerView, isStyleBookOpened, showListViewByDefault, + hasRevisions, + isRevisionsOpened, } = useSelect( ( select ) => { const { getActiveComplementaryArea } = select( interfaceStore ); const { getEditorCanvasContainerView, getCanvasMode } = unlock( select( editSiteStore ) ); + const canvasContainerView = getEditorCanvasContainerView(); const _isVisualEditorMode = 'visual' === select( editSiteStore ).getEditorMode(); const _isEditCanvasMode = 'edit' === getCanvasMode(); @@ -35,15 +45,26 @@ export default function GlobalStylesSidebar() { 'core/edit-site', 'showListViewByDefault' ); + const { getEntityRecord, __experimentalGetCurrentGlobalStylesId } = + select( coreStore ); + + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; return { - isStyleBookOpened: 'style-book' === getEditorCanvasContainerView(), + isStyleBookOpened: 'style-book' === canvasContainerView, shouldClearCanvasContainerView: 'edit-site/global-styles' !== getActiveComplementaryArea( 'core/edit-site' ) || ! _isVisualEditorMode || ! _isEditCanvasMode, showListViewByDefault: _showListViewByDefault, + hasRevisions: + !! globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count, + isRevisionsOpened: + 'global-styles-revisions' === canvasContainerView, }; }, [] ); const { setEditorCanvasContainerView } = unlock( @@ -57,6 +78,18 @@ export default function GlobalStylesSidebar() { }, [ shouldClearCanvasContainerView ] ); const { setIsListViewOpened } = useDispatch( editSiteStore ); + const { goTo } = useNavigator(); + const loadRevisions = () => { + setIsListViewOpened( false ); + + if ( ! isRevisionsOpened ) { + goTo( '/revisions' ); + setEditorCanvasContainerView( 'global-styles-revisions' ); + } else { + goTo( '/' ); + setEditorCanvasContainerView( undefined ); + } + }; return ( <DefaultSidebar @@ -91,6 +124,15 @@ export default function GlobalStylesSidebar() { } } /> </FlexItem> + <FlexItem> + <Button + label={ __( 'Revisions' ) } + icon={ backup } + onClick={ loadRevisions } + disabled={ ! hasRevisions } + isPressed={ isRevisionsOpened } + /> + </FlexItem> <GlobalStylesMenuSlot /> </Flex> } From d9ea8d072626f5ad1e5d50ab4207590cfa170b4a Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:54:31 +1300 Subject: [PATCH 177/325] Site Editor: Fix image upload - Check for a numeric post id before appending to upload media data (#57040) --- packages/editor/src/utils/media-upload/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor/src/utils/media-upload/index.js b/packages/editor/src/utils/media-upload/index.js index 036d925034bb86..3edd4fec51d4be 100644 --- a/packages/editor/src/utils/media-upload/index.js +++ b/packages/editor/src/utils/media-upload/index.js @@ -35,13 +35,13 @@ export default function mediaUpload( { const wpAllowedMimeTypes = getEditorSettings().allowedMimeTypes; maxUploadFileSize = maxUploadFileSize || getEditorSettings().maxUploadFileSize; - + const currentPostId = getCurrentPostId(); uploadMedia( { allowedTypes, filesList, onFileChange, additionalData: { - post: getCurrentPostId(), + ...( ! isNaN( currentPostId ) ? { post: currentPostId } : {} ), ...additionalData, }, maxUploadFileSize, From a99a5b161f210e654ea9e87ccd11ec2f1359e05f Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 14 Dec 2023 10:14:49 +0100 Subject: [PATCH 178/325] Editor: Move the panel visibility state from the edit-post to the editor package (#57012) --- .../data/data-core-edit-post.md | 12 +++ .../reference-guides/data/data-core-editor.md | 71 +++++++++++++++++ .../plugin-document-setting-panel.md | 8 +- .../specs/editor/various/sidebar.test.js | 2 +- packages/edit-post/CHANGELOG.md | 4 + .../meta-boxes/meta-box-visibility.js | 10 +-- .../preferences-modal/options/enable-panel.js | 10 +-- .../sidebar/discussion-panel/index.js | 10 +-- .../sidebar/featured-image/index.js | 15 ++-- .../sidebar/page-attributes/index.js | 15 ++-- .../plugin-document-setting-panel/index.js | 6 +- .../components/sidebar/post-excerpt/index.js | 6 +- .../components/sidebar/post-status/index.js | 6 +- .../sidebar/post-taxonomies/taxonomy-panel.js | 10 +-- packages/edit-post/src/store/actions.js | 71 ++++++----------- packages/edit-post/src/store/reducer.js | 20 ----- packages/edit-post/src/store/selectors.js | 62 +++++++++------ packages/edit-post/src/store/test/actions.js | 77 ------------------ packages/edit-post/src/store/test/reducer.js | 26 ------- .../edit-post/src/store/test/selectors.js | 26 ------- packages/editor/CHANGELOG.md | 4 + packages/editor/src/store/actions.js | 78 +++++++++++++++++++ packages/editor/src/store/reducer.js | 20 +++++ packages/editor/src/store/selectors.js | 58 ++++++++++++++ packages/editor/src/store/test/actions.js | 56 +++++++++++++ packages/editor/src/store/test/reducer.js | 21 +++++ packages/editor/src/store/test/selectors.js | 25 ++++++ 27 files changed, 448 insertions(+), 281 deletions(-) diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md index e09cf0caaec515..24b69c7853750d 100644 --- a/docs/reference-guides/data/data-core-edit-post.md +++ b/docs/reference-guides/data/data-core-edit-post.md @@ -144,6 +144,8 @@ Returns true if the template editing mode is enabled. ### isEditorPanelEnabled +> **Deprecated** + Returns true if the given panel is enabled, or false otherwise. Panels are enabled by default. _Parameters_ @@ -157,6 +159,8 @@ _Returns_ ### isEditorPanelOpened +> **Deprecated** + Returns true if the given panel is open, or false otherwise. Panels are closed by default. _Parameters_ @@ -170,6 +174,8 @@ _Returns_ ### isEditorPanelRemoved +> **Deprecated** + Returns true if the given panel was programmatically removed, or false otherwise. All panels are not removed by default. _Parameters_ @@ -408,6 +414,8 @@ _Returns_ ### removeEditorPanel +> **Deprecated** + Returns an action object used to remove a panel from the editor. _Parameters_ @@ -484,6 +492,8 @@ Action that toggles Distraction free mode. Distraction free mode expects there a ### toggleEditorPanelEnabled +> **Deprecated** + Returns an action object used to enable or disable a panel in the editor. _Parameters_ @@ -496,6 +506,8 @@ _Returns_ ### toggleEditorPanelOpened +> **Deprecated** + Opens a closed panel and closes an open panel. _Parameters_ diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index f6086090f9b541..266fce765fd6da 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -879,6 +879,45 @@ _Returns_ - `boolean`: Whether the post can be saved. +### isEditorPanelEnabled + +Returns true if the given panel is enabled, or false otherwise. Panels are enabled by default. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _panelName_ `string`: A string that identifies the panel. + +_Returns_ + +- `boolean`: Whether or not the panel is enabled. + +### isEditorPanelOpened + +Returns true if the given panel is open, or false otherwise. Panels are closed by default. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _panelName_ `string`: A string that identifies the panel. + +_Returns_ + +- `boolean`: Whether or not the panel is open. + +### isEditorPanelRemoved + +Returns true if the given panel was programmatically removed, or false otherwise. All panels are not removed by default. + +_Parameters_ + +- _state_ `Object`: Global application state. +- _panelName_ `string`: A string that identifies the panel. + +_Returns_ + +- `boolean`: Whether or not the panel is removed. + ### isFirstMultiSelectedBlock _Related_ @@ -1226,6 +1265,18 @@ _Related_ - removeBlocks in core/block-editor store. +### removeEditorPanel + +Returns an action object used to remove a panel from the editor. + +_Parameters_ + +- _panelName_ `string`: A string that identifies the panel to remove. + +_Returns_ + +- `Object`: Action object. + ### replaceBlock _Related_ @@ -1379,6 +1430,26 @@ _Related_ - toggleBlockMode in core/block-editor store. +### toggleEditorPanelEnabled + +Returns an action object used to enable or disable a panel in the editor. + +_Parameters_ + +- _panelName_ `string`: A string that identifies the panel to enable or disable. + +_Returns_ + +- `Object`: Action object. + +### toggleEditorPanelOpened + +Opens a closed panel and closes an open panel. + +_Parameters_ + +- _panelName_ `string`: A string that identifies the panel to open or close. + ### toggleSelection _Related_ diff --git a/docs/reference-guides/slotfills/plugin-document-setting-panel.md b/docs/reference-guides/slotfills/plugin-document-setting-panel.md index 76e077056abd63..c62c56b7e6877e 100644 --- a/docs/reference-guides/slotfills/plugin-document-setting-panel.md +++ b/docs/reference-guides/slotfills/plugin-document-setting-panel.md @@ -49,10 +49,10 @@ To programmatically toggle panels, use the following: ```js import { useDispatch } from '@wordpress/data'; -import { store as editPostStore } from '@wordpress/edit-post'; +import { store as editorStore } from '@wordpress/editor'; const Example = () => { - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); return ( <Button variant="primary" @@ -76,10 +76,10 @@ It is also possible to remove panels from the admin using the `removeEditorPanel ```js import { useDispatch } from '@wordpress/data'; -import { store as editPostStore } from '@wordpress/edit-post'; +import { store as editorStore } from '@wordpress/editor'; const Example = () => { - const { removeEditorPanel } = useDispatch( editPostStore ); + const { removeEditorPanel } = useDispatch( editorStore ); return ( <Button variant="primary" diff --git a/packages/e2e-tests/specs/editor/various/sidebar.test.js b/packages/e2e-tests/specs/editor/various/sidebar.test.js index 0cd39093aabb8c..1137019f239f75 100644 --- a/packages/e2e-tests/specs/editor/various/sidebar.test.js +++ b/packages/e2e-tests/specs/editor/various/sidebar.test.js @@ -135,7 +135,7 @@ describe( 'Sidebar', () => { expect( await findSidebarPanelWithTitle( 'Summary' ) ).toBeDefined(); await page.evaluate( () => { - const { removeEditorPanel } = wp.data.dispatch( 'core/edit-post' ); + const { removeEditorPanel } = wp.data.dispatch( 'core/editor' ); removeEditorPanel( 'taxonomy-panel-category' ); removeEditorPanel( 'taxonomy-panel-post_tag' ); diff --git a/packages/edit-post/CHANGELOG.md b/packages/edit-post/CHANGELOG.md index abf6e5b1c1c081..34ccd0953562f4 100644 --- a/packages/edit-post/CHANGELOG.md +++ b/packages/edit-post/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Deprecations + +- Move the panels visibility actions and selectors to the editor package deprecating `toggleEditorPanelEnabled`, `toggleEditorPanelOpened`, `removeEditorPanel`, `isEditorPanelRemoved`, `isEditorPanelOpened` and `isEditorPanelEnabled`. + ## 7.25.0 (2023-12-13) ## 7.24.0 (2023-11-29) diff --git a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js index 488d73e1a76e03..26b69d37f00210 100644 --- a/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js +++ b/packages/edit-post/src/components/meta-boxes/meta-box-visibility.js @@ -3,11 +3,7 @@ */ import { Component } from '@wordpress/element'; import { withSelect } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../store'; +import { store as editorStore } from '@wordpress/editor'; class MetaBoxVisibility extends Component { componentDidMount() { @@ -41,7 +37,5 @@ class MetaBoxVisibility extends Component { } export default withSelect( ( select, { id } ) => ( { - isVisible: select( editPostStore ).isEditorPanelEnabled( - `meta-box-${ id }` - ), + isVisible: select( editorStore ).isEditorPanelEnabled( `meta-box-${ id }` ), } ) )( MetaBoxVisibility ); diff --git a/packages/edit-post/src/components/preferences-modal/options/enable-panel.js b/packages/edit-post/src/components/preferences-modal/options/enable-panel.js index bbde5c80eedfd4..6c9ea22b7f17dd 100644 --- a/packages/edit-post/src/components/preferences-modal/options/enable-panel.js +++ b/packages/edit-post/src/components/preferences-modal/options/enable-panel.js @@ -4,16 +4,12 @@ import { compose, ifCondition } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import { ___unstablePreferencesModalBaseOption as BaseOption } from '@wordpress/interface'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; +import { store as editorStore } from '@wordpress/editor'; export default compose( withSelect( ( select, { panelName } ) => { const { isEditorPanelEnabled, isEditorPanelRemoved } = - select( editPostStore ); + select( editorStore ); return { isRemoved: isEditorPanelRemoved( panelName ), isChecked: isEditorPanelEnabled( panelName ), @@ -22,6 +18,6 @@ export default compose( ifCondition( ( { isRemoved } ) => ! isRemoved ), withDispatch( ( dispatch, { panelName } ) => ( { onChange: () => - dispatch( editPostStore ).toggleEditorPanelEnabled( panelName ), + dispatch( editorStore ).toggleEditorPanelEnabled( panelName ), } ) ) )( BaseOption ); diff --git a/packages/edit-post/src/components/sidebar/discussion-panel/index.js b/packages/edit-post/src/components/sidebar/discussion-panel/index.js index c8e63f23fac8dd..3ed175ca66e1e6 100644 --- a/packages/edit-post/src/components/sidebar/discussion-panel/index.js +++ b/packages/edit-post/src/components/sidebar/discussion-panel/index.js @@ -7,14 +7,10 @@ import { PostComments, PostPingbacks, PostTypeSupportCheck, + store as editorStore, } from '@wordpress/editor'; import { useDispatch, useSelect } from '@wordpress/data'; -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - /** * Module Constants */ @@ -23,14 +19,14 @@ const PANEL_NAME = 'discussion-panel'; function DiscussionPanel() { const { isEnabled, isOpened } = useSelect( ( select ) => { const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); + select( editorStore ); return { isEnabled: isEditorPanelEnabled( PANEL_NAME ), isOpened: isEditorPanelOpened( PANEL_NAME ), }; }, [] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); if ( ! isEnabled ) { return null; diff --git a/packages/edit-post/src/components/sidebar/featured-image/index.js b/packages/edit-post/src/components/sidebar/featured-image/index.js index 42215c5de93805..b528a7097231d6 100644 --- a/packages/edit-post/src/components/sidebar/featured-image/index.js +++ b/packages/edit-post/src/components/sidebar/featured-image/index.js @@ -12,11 +12,6 @@ import { compose } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - /** * Module Constants */ @@ -43,10 +38,12 @@ function FeaturedImage( { isEnabled, isOpened, postType, onTogglePanel } ) { } const applyWithSelect = withSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); + const { + getEditedPostAttribute, + isEditorPanelEnabled, + isEditorPanelOpened, + } = select( editorStore ); const { getPostType } = select( coreStore ); - const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); return { postType: getPostType( getEditedPostAttribute( 'type' ) ), @@ -56,7 +53,7 @@ const applyWithSelect = withSelect( ( select ) => { } ); const applyWithDispatch = withDispatch( ( dispatch ) => { - const { toggleEditorPanelOpened } = dispatch( editPostStore ); + const { toggleEditorPanelOpened } = dispatch( editorStore ); return { onTogglePanel: ( ...args ) => diff --git a/packages/edit-post/src/components/sidebar/page-attributes/index.js b/packages/edit-post/src/components/sidebar/page-attributes/index.js index e34680e42a7b52..7a5d6222e11fcd 100644 --- a/packages/edit-post/src/components/sidebar/page-attributes/index.js +++ b/packages/edit-post/src/components/sidebar/page-attributes/index.js @@ -12,11 +12,6 @@ import { import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; - /** * Module Constants */ @@ -24,9 +19,11 @@ const PANEL_NAME = 'page-attributes'; export function PageAttributes() { const { isEnabled, isOpened, postType } = useSelect( ( select ) => { - const { getEditedPostAttribute } = select( editorStore ); - const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); + const { + getEditedPostAttribute, + isEditorPanelEnabled, + isEditorPanelOpened, + } = select( editorStore ); const { getPostType } = select( coreStore ); return { isEnabled: isEditorPanelEnabled( PANEL_NAME ), @@ -35,7 +32,7 @@ export function PageAttributes() { }; }, [] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); if ( ! isEnabled || ! postType ) { return null; diff --git a/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js b/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js index 7ee3f96498e53c..e77b1ce71160d2 100644 --- a/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js +++ b/packages/edit-post/src/components/sidebar/plugin-document-setting-panel/index.js @@ -5,12 +5,12 @@ import { createSlotFill, PanelBody } from '@wordpress/components'; import { usePluginContext } from '@wordpress/plugins'; import { useDispatch, useSelect } from '@wordpress/data'; import warning from '@wordpress/warning'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import { EnablePluginDocumentSettingPanelOption } from '../../preferences-modal/options'; -import { store as editPostStore } from '../../../store'; const { Fill, Slot } = createSlotFill( 'PluginDocumentSettingPanel' ); @@ -78,7 +78,7 @@ const PluginDocumentSettingPanel = ( { const { opened, isEnabled } = useSelect( ( select ) => { const { isEditorPanelOpened, isEditorPanelEnabled } = - select( editPostStore ); + select( editorStore ); return { opened: isEditorPanelOpened( panelName ), @@ -87,7 +87,7 @@ const PluginDocumentSettingPanel = ( { }, [ panelName ] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); if ( undefined === name ) { warning( 'PluginDocumentSettingPanel requires a name property.' ); diff --git a/packages/edit-post/src/components/sidebar/post-excerpt/index.js b/packages/edit-post/src/components/sidebar/post-excerpt/index.js index c2b9c0c48370eb..8063ed6017b08c 100644 --- a/packages/edit-post/src/components/sidebar/post-excerpt/index.js +++ b/packages/edit-post/src/components/sidebar/post-excerpt/index.js @@ -6,13 +6,13 @@ import { PanelBody } from '@wordpress/components'; import { PostExcerpt as PostExcerptForm, PostExcerptCheck, + store as editorStore, } from '@wordpress/editor'; import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { store as editPostStore } from '../../../store'; import PluginPostExcerpt from '../plugin-post-excerpt'; /** @@ -23,7 +23,7 @@ const PANEL_NAME = 'post-excerpt'; export default function PostExcerpt() { const { isOpened, isEnabled } = useSelect( ( select ) => { const { isEditorPanelOpened, isEditorPanelEnabled } = - select( editPostStore ); + select( editorStore ); return { isOpened: isEditorPanelOpened( PANEL_NAME ), @@ -31,7 +31,7 @@ export default function PostExcerpt() { }; }, [] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); const toggleExcerptPanel = () => toggleEditorPanelOpened( PANEL_NAME ); if ( ! isEnabled ) { diff --git a/packages/edit-post/src/components/sidebar/post-status/index.js b/packages/edit-post/src/components/sidebar/post-status/index.js index 0d14265b15f820..45d749fee2c658 100644 --- a/packages/edit-post/src/components/sidebar/post-status/index.js +++ b/packages/edit-post/src/components/sidebar/post-status/index.js @@ -14,6 +14,7 @@ import { PostSyncStatus, PostURLPanel, PostTemplatePanel, + store as editorStore, } from '@wordpress/editor'; /** @@ -26,7 +27,6 @@ import PostSlug from '../post-slug'; import PostFormat from '../post-format'; import PostPendingStatus from '../post-pending-status'; import PluginPostStatusInfo from '../plugin-post-status-info'; -import { store as editPostStore } from '../../../store'; /** * Module Constants @@ -38,13 +38,13 @@ export default function PostStatus() { // We use isEditorPanelRemoved to hide the panel if it was programatically removed. We do // not use isEditorPanelEnabled since this panel should not be disabled through the UI. const { isEditorPanelRemoved, isEditorPanelOpened } = - select( editPostStore ); + select( editorStore ); return { isRemoved: isEditorPanelRemoved( PANEL_NAME ), isOpened: isEditorPanelOpened( PANEL_NAME ), }; }, [] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); if ( isRemoved ) { return null; diff --git a/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js b/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js index 88c4366d1cfe8a..4b3bab1fcf3442 100644 --- a/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js +++ b/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js @@ -3,11 +3,7 @@ */ import { PanelBody } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; - -/** - * Internal dependencies - */ -import { store as editPostStore } from '../../../store'; +import { store as editorStore } from '@wordpress/editor'; function TaxonomyPanel( { taxonomy, children } ) { const slug = taxonomy?.slug; @@ -15,7 +11,7 @@ function TaxonomyPanel( { taxonomy, children } ) { const { isEnabled, isOpened } = useSelect( ( select ) => { const { isEditorPanelEnabled, isEditorPanelOpened } = - select( editPostStore ); + select( editorStore ); return { isEnabled: slug ? isEditorPanelEnabled( panelName ) : false, isOpened: slug ? isEditorPanelOpened( panelName ) : false, @@ -23,7 +19,7 @@ function TaxonomyPanel( { taxonomy, children } ) { }, [ panelName, slug ] ); - const { toggleEditorPanelOpened } = useDispatch( editPostStore ); + const { toggleEditorPanelOpened } = useDispatch( editorStore ); if ( ! isEnabled ) { return null; diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index eae1030fad0248..89141397f23ee9 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -122,6 +122,8 @@ export function togglePublishSidebar() { /** * Returns an action object used to enable or disable a panel in the editor. * + * @deprecated + * * @param {string} panelName A string that identifies the panel to enable or disable. * * @return {Object} Action object. @@ -129,73 +131,48 @@ export function togglePublishSidebar() { export const toggleEditorPanelEnabled = ( panelName ) => ( { registry } ) => { - const inactivePanels = - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'inactivePanels' ) ?? []; - - const isPanelInactive = !! inactivePanels?.includes( panelName ); - - // If the panel is inactive, remove it to enable it, else add it to - // make it inactive. - let updatedInactivePanels; - if ( isPanelInactive ) { - updatedInactivePanels = inactivePanels.filter( - ( invactivePanelName ) => invactivePanelName !== panelName - ); - } else { - updatedInactivePanels = [ ...inactivePanels, panelName ]; - } - - registry - .dispatch( preferencesStore ) - .set( 'core/edit-post', 'inactivePanels', updatedInactivePanels ); + deprecated( "dispatch( 'core/edit-post' ).toggleEditorPanelEnabled", { + since: '6.5', + alternative: "dispatch( 'core/editor').toggleEditorPanelEnabled", + } ); + registry.dispatch( editorStore ).toggleEditorPanelEnabled( panelName ); }; /** * Opens a closed panel and closes an open panel. * + * @deprecated + * * @param {string} panelName A string that identifies the panel to open or close. */ export const toggleEditorPanelOpened = ( panelName ) => ( { registry } ) => { - const openPanels = - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'openPanels' ) ?? []; - - const isPanelOpen = !! openPanels?.includes( panelName ); - - // If the panel is open, remove it to close it, else add it to - // make it open. - let updatedOpenPanels; - if ( isPanelOpen ) { - updatedOpenPanels = openPanels.filter( - ( openPanelName ) => openPanelName !== panelName - ); - } else { - updatedOpenPanels = [ ...openPanels, panelName ]; - } - - registry - .dispatch( preferencesStore ) - .set( 'core/edit-post', 'openPanels', updatedOpenPanels ); + deprecated( "dispatch( 'core/edit-post' ).toggleEditorPanelOpened", { + since: '6.5', + alternative: "dispatch( 'core/editor').toggleEditorPanelOpened", + } ); + registry.dispatch( editorStore ).toggleEditorPanelOpened( panelName ); }; /** * Returns an action object used to remove a panel from the editor. * + * @deprecated + * * @param {string} panelName A string that identifies the panel to remove. * * @return {Object} Action object. */ -export function removeEditorPanel( panelName ) { - return { - type: 'REMOVE_PANEL', - panelName, +export const removeEditorPanel = + ( panelName ) => + ( { registry } ) => { + deprecated( "dispatch( 'core/edit-post' ).removeEditorPanel", { + since: '6.5', + alternative: "dispatch( 'core/editor').removeEditorPanel", + } ); + registry.dispatch( editorStore ).removeEditorPanel( panelName ); }; -} /** * Triggers an action used to toggle a feature flag. diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 1072919d388db4..151c9951cc5e2e 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -3,25 +3,6 @@ */ import { combineReducers } from '@wordpress/data'; -/** - * Reducer storing the list of all programmatically removed panels. - * - * @param {Array} state Current state. - * @param {Object} action Action object. - * - * @return {Array} Updated state. - */ -export function removedPanels( state = [], action ) { - switch ( action.type ) { - case 'REMOVE_PANEL': - if ( ! state.includes( action.panelName ) ) { - return [ ...state, action.panelName ]; - } - } - - return state; -} - export function publishSidebarActive( state = false, action ) { switch ( action.type ) { case 'OPEN_PUBLISH_SIDEBAR': @@ -161,7 +142,6 @@ const metaBoxes = combineReducers( { export default combineReducers( { metaBoxes, publishSidebarActive, - removedPanels, blockInserterPanel, listViewPanel, } ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index 115dcd9bcd78e7..ca089e3db9f1aa 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -250,19 +250,29 @@ export function isPublishSidebarOpened( state ) { * Returns true if the given panel was programmatically removed, or false otherwise. * All panels are not removed by default. * + * @deprecated + * * @param {Object} state Global application state. * @param {string} panelName A string that identifies the panel. * * @return {boolean} Whether or not the panel is removed. */ -export function isEditorPanelRemoved( state, panelName ) { - return state.removedPanels.includes( panelName ); -} +export const isEditorPanelRemoved = createRegistrySelector( + ( select ) => ( state, panelName ) => { + deprecated( `select( 'core/edit-post' ).isEditorPanelRemoved`, { + since: '6.5', + alternative: `select( 'core/editor' ).isEditorPanelRemoved`, + } ); + return select( editorStore ).isEditorPanelRemoved( panelName ); + } +); /** * Returns true if the given panel is enabled, or false otherwise. Panels are * enabled by default. * + * @deprecated + * * @param {Object} state Global application state. * @param {string} panelName A string that identifies the panel. * @@ -270,14 +280,11 @@ export function isEditorPanelRemoved( state, panelName ) { */ export const isEditorPanelEnabled = createRegistrySelector( ( select ) => ( state, panelName ) => { - const inactivePanels = select( preferencesStore ).get( - 'core/edit-post', - 'inactivePanels' - ); - return ( - ! isEditorPanelRemoved( state, panelName ) && - ! inactivePanels?.includes( panelName ) - ); + deprecated( `select( 'core/edit-post' ).isEditorPanelEnabled`, { + since: '6.5', + alternative: `select( 'core/editor' ).isEditorPanelEnabled`, + } ); + return select( editorStore ).isEditorPanelEnabled( panelName ); } ); @@ -285,6 +292,8 @@ export const isEditorPanelEnabled = createRegistrySelector( * Returns true if the given panel is open, or false otherwise. Panels are * closed by default. * + * @deprecated + * * @param {Object} state Global application state. * @param {string} panelName A string that identifies the panel. * @@ -292,11 +301,11 @@ export const isEditorPanelEnabled = createRegistrySelector( */ export const isEditorPanelOpened = createRegistrySelector( ( select ) => ( state, panelName ) => { - const openPanels = select( preferencesStore ).get( - 'core/edit-post', - 'openPanels' - ); - return !! openPanels?.includes( panelName ); + deprecated( `select( 'core/edit-post' ).isEditorPanelOpened`, { + since: '6.5', + alternative: `select( 'core/editor' ).isEditorPanelOpened`, + } ); + return select( editorStore ).isEditorPanelOpened( panelName ); } ); @@ -376,14 +385,19 @@ export const getActiveMetaBoxLocations = createSelector( * * @return {boolean} Whether the meta box location is active and visible. */ -export function isMetaBoxLocationVisible( state, location ) { - return ( - isMetaBoxLocationActive( state, location ) && - getMetaBoxesPerLocation( state, location )?.some( ( { id } ) => { - return isEditorPanelEnabled( state, `meta-box-${ id }` ); - } ) - ); -} +export const isMetaBoxLocationVisible = createRegistrySelector( + ( select ) => ( state, location ) => { + return ( + isMetaBoxLocationActive( state, location ) && + getMetaBoxesPerLocation( state, location )?.some( ( { id } ) => { + return select( editorStore ).isEditorPanelEnabled( + state, + `meta-box-${ id }` + ); + } ) + ); + } +); /** * Returns true if there is an active meta box in the given location, or false diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 39b889f2fdcbc0..5ec499551a09b4 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -205,83 +205,6 @@ describe( 'actions', () => { } ); } ); - describe( 'toggleEditorPanelEnabled', () => { - it( 'toggles panels to be enabled and not enabled', () => { - // This will switch it off, since the default is on. - registry - .dispatch( editPostStore ) - .toggleEditorPanelEnabled( 'control-panel' ); - - expect( - registry - .select( editPostStore ) - .isEditorPanelEnabled( 'control-panel' ) - ).toBe( false ); - - // Also check that the `getPreference` selector includes panels. - expect( - registry.select( editPostStore ).getPreference( 'panels' ) - ).toEqual( { - 'control-panel': { - enabled: false, - }, - } ); - - // Switch it on again. - registry - .dispatch( editPostStore ) - .toggleEditorPanelEnabled( 'control-panel' ); - - expect( - registry - .select( editPostStore ) - .isEditorPanelEnabled( 'control-panel' ) - ).toBe( true ); - - expect( - registry.select( editPostStore ).getPreference( 'panels' ) - ).toEqual( {} ); - } ); - } ); - - describe( 'toggleEditorPanelOpened', () => { - it( 'toggles panels open and closed', () => { - // This will open it, since the default is closed. - registry - .dispatch( editPostStore ) - .toggleEditorPanelOpened( 'control-panel' ); - - expect( - registry - .select( editPostStore ) - .isEditorPanelOpened( 'control-panel' ) - ).toBe( true ); - - expect( - registry.select( editPostStore ).getPreference( 'panels' ) - ).toEqual( { - 'control-panel': { - opened: true, - }, - } ); - - // Close it. - registry - .dispatch( editPostStore ) - .toggleEditorPanelOpened( 'control-panel' ); - - expect( - registry - .select( editPostStore ) - .isEditorPanelOpened( 'control-panel' ) - ).toBe( false ); - - expect( - registry.select( editPostStore ).getPreference( 'panels' ) - ).toEqual( {} ); - } ); - } ); - describe( 'updatePreferredStyleVariations', () => { it( 'sets a preferred style variation for a block when a style name is passed', () => { registry diff --git a/packages/edit-post/src/store/test/reducer.js b/packages/edit-post/src/store/test/reducer.js index 2e8d923d022f36..3885fa6b9834be 100644 --- a/packages/edit-post/src/store/test/reducer.js +++ b/packages/edit-post/src/store/test/reducer.js @@ -1,15 +1,9 @@ -/** - * External dependencies - */ -import deepFreeze from 'deep-freeze'; - /** * Internal dependencies */ import { isSavingMetaBoxes, metaBoxLocations, - removedPanels, blockInserterPanel, listViewPanel, } from '../reducer'; @@ -95,26 +89,6 @@ describe( 'state', () => { } ); } ); - describe( 'removedPanels', () => { - it( 'should remove panel', () => { - const original = deepFreeze( [] ); - const state = removedPanels( original, { - type: 'REMOVE_PANEL', - panelName: 'post-status', - } ); - expect( state ).toEqual( [ 'post-status' ] ); - } ); - - it( 'should not remove already removed panel', () => { - const original = deepFreeze( [ 'post-status' ] ); - const state = removedPanels( original, { - type: 'REMOVE_PANEL', - panelName: 'post-status', - } ); - expect( state ).toBe( original ); - } ); - } ); - describe( 'blockInserterPanel()', () => { it( 'should apply default state', () => { expect( blockInserterPanel( undefined, {} ) ).toEqual( false ); diff --git a/packages/edit-post/src/store/test/selectors.js b/packages/edit-post/src/store/test/selectors.js index 34c66ed7cf8e67..988574b9a53d81 100644 --- a/packages/edit-post/src/store/test/selectors.js +++ b/packages/edit-post/src/store/test/selectors.js @@ -1,8 +1,3 @@ -/** - * External dependencies - */ -import deepFreeze from 'deep-freeze'; - /** * Internal dependencies */ @@ -11,32 +6,11 @@ import { isSavingMetaBoxes, getActiveMetaBoxLocations, isMetaBoxLocationActive, - isEditorPanelRemoved, isInserterOpened, isListViewOpened, } from '../selectors'; describe( 'selectors', () => { - describe( 'isEditorPanelRemoved', () => { - it( 'should return false by default', () => { - const state = deepFreeze( { - removedPanels: [], - } ); - - expect( isEditorPanelRemoved( state, 'post-status' ) ).toBe( - false - ); - } ); - - it( 'should return true when panel was removed', () => { - const state = deepFreeze( { - removedPanels: [ 'post-status' ], - } ); - - expect( isEditorPanelRemoved( state, 'post-status' ) ).toBe( true ); - } ); - } ); - describe( 'hasMetaBoxes', () => { it( 'should return true if there are active meta boxes', () => { const state = { diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index e1a7943e79f3e8..dc8fffd379798a 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Features + +- Add the editor panels visibility state to the editor store in addition to the following actions and selectors: `toggleEditorPanelEnabled`, `toggleEditorPanelOpened`, `removeEditorPanel`, `isEditorPanelRemoved`, `isEditorPanelOpened` and `isEditorPanelEnabled`. + ## 13.25.0 (2023-12-13) ## 13.24.0 (2023-11-29) diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index 8fe0822e6a016c..d35907e5ed04d3 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -611,6 +611,84 @@ export function setDeviceType( deviceType ) { }; } +/** + * Returns an action object used to enable or disable a panel in the editor. + * + * @param {string} panelName A string that identifies the panel to enable or disable. + * + * @return {Object} Action object. + */ +export const toggleEditorPanelEnabled = + ( panelName ) => + ( { registry } ) => { + const inactivePanels = + registry + .select( preferencesStore ) + .get( 'core/edit-post', 'inactivePanels' ) ?? []; + + const isPanelInactive = !! inactivePanels?.includes( panelName ); + + // If the panel is inactive, remove it to enable it, else add it to + // make it inactive. + let updatedInactivePanels; + if ( isPanelInactive ) { + updatedInactivePanels = inactivePanels.filter( + ( invactivePanelName ) => invactivePanelName !== panelName + ); + } else { + updatedInactivePanels = [ ...inactivePanels, panelName ]; + } + + registry + .dispatch( preferencesStore ) + .set( 'core/edit-post', 'inactivePanels', updatedInactivePanels ); + }; + +/** + * Opens a closed panel and closes an open panel. + * + * @param {string} panelName A string that identifies the panel to open or close. + */ +export const toggleEditorPanelOpened = + ( panelName ) => + ( { registry } ) => { + const openPanels = + registry + .select( preferencesStore ) + .get( 'core/edit-post', 'openPanels' ) ?? []; + + const isPanelOpen = !! openPanels?.includes( panelName ); + + // If the panel is open, remove it to close it, else add it to + // make it open. + let updatedOpenPanels; + if ( isPanelOpen ) { + updatedOpenPanels = openPanels.filter( + ( openPanelName ) => openPanelName !== panelName + ); + } else { + updatedOpenPanels = [ ...openPanels, panelName ]; + } + + registry + .dispatch( preferencesStore ) + .set( 'core/edit-post', 'openPanels', updatedOpenPanels ); + }; + +/** + * Returns an action object used to remove a panel from the editor. + * + * @param {string} panelName A string that identifies the panel to remove. + * + * @return {Object} Action object. + */ +export function removeEditorPanel( panelName ) { + return { + type: 'REMOVE_PANEL', + panelName, + }; +} + /** * Backward compatibility */ diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index a4323b59679561..48c52d44327c3e 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -292,6 +292,25 @@ export function deviceType( state = 'Desktop', action ) { return state; } +/** + * Reducer storing the list of all programmatically removed panels. + * + * @param {Array} state Current state. + * @param {Object} action Action object. + * + * @return {Array} Updated state. + */ +export function removedPanels( state = [], action ) { + switch ( action.type ) { + case 'REMOVE_PANEL': + if ( ! state.includes( action.panelName ) ) { + return [ ...state, action.panelName ]; + } + } + + return state; +} + export default combineReducers( { postId, postType, @@ -305,4 +324,5 @@ export default combineReducers( { postAutosavingLock, renderingMode, deviceType, + removedPanels, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 3b3f3158124300..1c89d0b7f58009 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1125,6 +1125,64 @@ export const getEditorBlocks = createSelector( ] ); +/** + * Returns true if the given panel was programmatically removed, or false otherwise. + * All panels are not removed by default. + * + * @param {Object} state Global application state. + * @param {string} panelName A string that identifies the panel. + * + * @return {boolean} Whether or not the panel is removed. + */ +export function isEditorPanelRemoved( state, panelName ) { + return state.removedPanels.includes( panelName ); +} + +/** + * Returns true if the given panel is enabled, or false otherwise. Panels are + * enabled by default. + * + * @param {Object} state Global application state. + * @param {string} panelName A string that identifies the panel. + * + * @return {boolean} Whether or not the panel is enabled. + */ +export const isEditorPanelEnabled = createRegistrySelector( + ( select ) => ( state, panelName ) => { + // For backward compatibility, we check edit-post + // even though now this is in "editor" package. + const inactivePanels = select( preferencesStore ).get( + 'core/edit-post', + 'inactivePanels' + ); + return ( + ! isEditorPanelRemoved( state, panelName ) && + ! inactivePanels?.includes( panelName ) + ); + } +); + +/** + * Returns true if the given panel is open, or false otherwise. Panels are + * closed by default. + * + * @param {Object} state Global application state. + * @param {string} panelName A string that identifies the panel. + * + * @return {boolean} Whether or not the panel is open. + */ +export const isEditorPanelOpened = createRegistrySelector( + ( select ) => ( state, panelName ) => { + // For backward compatibility, we check edit-post + // even though now this is in "editor" package. + const openPanels = select( preferencesStore ).get( + 'core/edit-post', + 'openPanels' + ); + return !! openPanels?.includes( panelName ); + } +); + /** * A block selection object. * diff --git a/packages/editor/src/store/test/actions.js b/packages/editor/src/store/test/actions.js index b842450b733b38..b977e92baf8c15 100644 --- a/packages/editor/src/store/test/actions.js +++ b/packages/editor/src/store/test/actions.js @@ -432,4 +432,60 @@ describe( 'Editor actions', () => { ).toBe( false ); } ); } ); + + describe( 'toggleEditorPanelEnabled', () => { + it( 'toggles panels to be enabled and not enabled', () => { + const registry = createRegistryWithStores(); + + // This will switch it off, since the default is on. + registry + .dispatch( editorStore ) + .toggleEditorPanelEnabled( 'control-panel' ); + + expect( + registry + .select( editorStore ) + .isEditorPanelEnabled( 'control-panel' ) + ).toBe( false ); + + // Switch it on again. + registry + .dispatch( editorStore ) + .toggleEditorPanelEnabled( 'control-panel' ); + + expect( + registry + .select( editorStore ) + .isEditorPanelEnabled( 'control-panel' ) + ).toBe( true ); + } ); + } ); + + describe( 'toggleEditorPanelOpened', () => { + it( 'toggles panels open and closed', () => { + const registry = createRegistryWithStores(); + + // This will open it, since the default is closed. + registry + .dispatch( editorStore ) + .toggleEditorPanelOpened( 'control-panel' ); + + expect( + registry + .select( editorStore ) + .isEditorPanelOpened( 'control-panel' ) + ).toBe( true ); + + // Close it. + registry + .dispatch( editorStore ) + .toggleEditorPanelOpened( 'control-panel' ); + + expect( + registry + .select( editorStore ) + .isEditorPanelOpened( 'control-panel' ) + ).toBe( false ); + } ); + } ); } ); diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index 3b0ea9d3340a1e..c1f3fd73750499 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -14,6 +14,7 @@ import { saving, postSavingLock, postAutosavingLock, + removedPanels, } from '../reducer'; describe( 'state', () => { @@ -264,4 +265,24 @@ describe( 'state', () => { expect( state ).toEqual( {} ); } ); } ); + + describe( 'removedPanels', () => { + it( 'should remove panel', () => { + const original = deepFreeze( [] ); + const state = removedPanels( original, { + type: 'REMOVE_PANEL', + panelName: 'post-status', + } ); + expect( state ).toEqual( [ 'post-status' ] ); + } ); + + it( 'should not remove already removed panel', () => { + const original = deepFreeze( [ 'post-status' ] ); + const state = removedPanels( original, { + type: 'REMOVE_PANEL', + panelName: 'post-status', + } ); + expect( state ).toBe( original ); + } ); + } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 211ff717c88bda..285d97cabb288a 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import deepFreeze from 'deep-freeze'; + /** * WordPress dependencies */ @@ -187,6 +192,7 @@ const { __experimentalGetDefaultTemplateTypes, __experimentalGetTemplateInfo, __experimentalGetDefaultTemplatePartAreas, + isEditorPanelRemoved, } = selectors; const defaultTemplateTypes = [ @@ -3010,4 +3016,23 @@ describe( 'selectors', () => { ); } ); } ); + describe( 'isEditorPanelRemoved', () => { + it( 'should return false by default', () => { + const state = deepFreeze( { + removedPanels: [], + } ); + + expect( isEditorPanelRemoved( state, 'post-status' ) ).toBe( + false + ); + } ); + + it( 'should return true when panel was removed', () => { + const state = deepFreeze( { + removedPanels: [ 'post-status' ], + } ); + + expect( isEditorPanelRemoved( state, 'post-status' ) ).toBe( true ); + } ); + } ); } ); From 04bc1ca6bd670a6471e84fdd07a709d5e06508e7 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Thu, 14 Dec 2023 11:24:47 +0200 Subject: [PATCH 179/325] Fix BlockSwitcher checks for showing a Dropdown menu or not (#57047) --- .../src/components/block-switcher/index.js | 108 ++++++++---------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/packages/block-editor/src/components/block-switcher/index.js b/packages/block-editor/src/components/block-switcher/index.js index 0960dc87eaa499..8117c6f539b4f3 100644 --- a/packages/block-editor/src/components/block-switcher/index.js +++ b/packages/block-editor/src/components/block-switcher/index.js @@ -138,11 +138,14 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { const hasPossibleBlockVariationTransformations = !! blockVariationTransformations?.length; const hasPatternTransformation = !! patterns?.length && canRemove; - if ( - ! hasBlockStyles && - ! hasPossibleBlockTransformations && - ! hasPossibleBlockVariationTransformations - ) { + const hasBlockOrBlockVariationTransforms = + hasPossibleBlockTransformations || + hasPossibleBlockVariationTransformations; + const showDropdown = + hasBlockStyles || + hasBlockOrBlockVariationTransforms || + hasPatternTransformation; + if ( ! showDropdown ) { return ( <ToolbarGroup> <ToolbarButton @@ -180,13 +183,6 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { blocks.length ); - const hasBlockOrBlockVariationTransforms = - hasPossibleBlockTransformations || - hasPossibleBlockVariationTransformations; - const showDropDown = - hasBlockStyles || - hasBlockOrBlockVariationTransforms || - hasPatternTransformation; return ( <ToolbarGroup> <ToolbarItem> @@ -218,54 +214,48 @@ export const BlockSwitcherDropdownMenu = ( { clientIds, blocks } ) => { } } menuProps={ { orientation: 'both' } } > - { ( { onClose } ) => - showDropDown && ( - <div className="block-editor-block-switcher__container"> - { hasPatternTransformation && ( - <PatternTransformationsMenu - blocks={ blocks } - patterns={ patterns } - onSelect={ ( + { ( { onClose } ) => ( + <div className="block-editor-block-switcher__container"> + { hasPatternTransformation && ( + <PatternTransformationsMenu + blocks={ blocks } + patterns={ patterns } + onSelect={ ( transformedBlocks ) => { + onPatternTransform( transformedBlocks - ) => { - onPatternTransform( - transformedBlocks - ); - onClose(); - } } - /> - ) } - { hasBlockOrBlockVariationTransforms && ( - <BlockTransformationsMenu - className="block-editor-block-switcher__transforms__menugroup" - possibleBlockTransformations={ - possibleBlockTransformations - } - possibleBlockVariationTransformations={ - blockVariationTransformations - } - blocks={ blocks } - onSelect={ ( name ) => { - onBlockTransform( name ); - onClose(); - } } - onSelectVariation={ ( name ) => { - onBlockVariationTransform( - name - ); - onClose(); - } } - /> - ) } - { hasBlockStyles && ( - <BlockStylesMenu - hoveredBlock={ blocks[ 0 ] } - onSwitch={ onClose } - /> - ) } - </div> - ) - } + ); + onClose(); + } } + /> + ) } + { hasBlockOrBlockVariationTransforms && ( + <BlockTransformationsMenu + className="block-editor-block-switcher__transforms__menugroup" + possibleBlockTransformations={ + possibleBlockTransformations + } + possibleBlockVariationTransformations={ + blockVariationTransformations + } + blocks={ blocks } + onSelect={ ( name ) => { + onBlockTransform( name ); + onClose(); + } } + onSelectVariation={ ( name ) => { + onBlockVariationTransform( name ); + onClose(); + } } + /> + ) } + { hasBlockStyles && ( + <BlockStylesMenu + hoveredBlock={ blocks[ 0 ] } + onSwitch={ onClose } + /> + ) } + </div> + ) } </DropdownMenu> ) } </ToolbarItem> From 702fd1b77504a7999c4f8bc0cd0abecfe99b0358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 14 Dec 2023 10:51:18 +0100 Subject: [PATCH 180/325] DataViews: fix bug on operators count for table layout (#57048) --- packages/dataviews/src/view-table.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 3f5891f076791e..008bd9f02c7022 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -59,7 +59,7 @@ function HeaderMenu( { field, view, onChangeView } ) { const operators = columnOperators.filter( ( operator ) => [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) ); - if ( operators.length >= 0 ) { + if ( operators.length > 0 ) { filter = { field: field.id, operators, From bf37edb5b47687d58ce74bdbb4faedb149ab4fc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:09:07 +0100 Subject: [PATCH 181/325] DataViews: rename `operatorsFromField` to `sanitizeOperators` (#57050) --- packages/dataviews/src/filters.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index 153372379cf8d2..f8b1e59e8c9d15 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -6,7 +6,7 @@ import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; -const operatorsFromField = ( field ) => { +const sanitizeOperators = ( field ) => { let operators = field.filterBy?.operators; if ( ! operators || ! Array.isArray( operators ) ) { operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; @@ -23,7 +23,7 @@ export default function Filters( { fields, view, onChangeView } ) { return; } - const operators = operatorsFromField( field ); + const operators = sanitizeOperators( field ); if ( operators.length === 0 ) { return; } From 8a3550bd65dd189bf5f057895dc42d9b45db88fc Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:10:15 +0100 Subject: [PATCH 182/325] Components: tab panel: don't render hidden content by default (#57046) --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/tabs/tabpanel.tsx | 3 ++- packages/e2e-tests/specs/editor/various/editor-modes.test.js | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index acf8112b4111ac..4114a32f148542 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Experimental + +- `TabPanel`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). + ## 25.14.0 (2023-12-13) ### Enhancements diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index 14c449bf41d135..6762c123cce817 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -32,6 +32,7 @@ export const TabPanel = forwardRef< } const { store, instanceId } = context; const instancedTabId = `${ instanceId }-${ tabId }`; + const selectedId = store.useState( ( state ) => state.selectedId ); return ( <StyledTabPanel @@ -41,7 +42,7 @@ export const TabPanel = forwardRef< focusable={ focusable } { ...otherProps } > - { children } + { selectedId === instancedTabId && children } </StyledTabPanel> ); } ); diff --git a/packages/e2e-tests/specs/editor/various/editor-modes.test.js b/packages/e2e-tests/specs/editor/various/editor-modes.test.js index aea6536f605bb6..017237df9fbede 100644 --- a/packages/e2e-tests/specs/editor/various/editor-modes.test.js +++ b/packages/e2e-tests/specs/editor/various/editor-modes.test.js @@ -120,7 +120,7 @@ describe( 'Editing modes (visual/HTML)', () => { '//button[@role="tab"][contains(text(), "Block")]' ); inactiveBlockInspectorTab.click(); - const noBlocksElement = await page.$( + const noBlocksElement = page.waitForSelector( '.block-editor-block-inspector__no-blocks' ); expect( noBlocksElement ).not.toBeNull(); From 789519e950bd365e2dafb59456567fa82139d48b Mon Sep 17 00:00:00 2001 From: Dave Smith <getdavemail@gmail.com> Date: Thu, 14 Dec 2023 10:16:30 +0000 Subject: [PATCH 183/325] Fix flaky Navigation focus mode test (#57016) * Add missing assertion * Test expect url * Do setup before entering editor --- .../site-editor/navigation-editor.spec.js | 42 +++++++++++-------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/test/e2e/specs/site-editor/navigation-editor.spec.js b/test/e2e/specs/site-editor/navigation-editor.spec.js index 3ac72ac3b78d60..344f776c71e027 100644 --- a/test/e2e/specs/site-editor/navigation-editor.spec.js +++ b/test/e2e/specs/site-editor/navigation-editor.spec.js @@ -20,20 +20,6 @@ test.describe( 'Editing Navigation Menus', () => { editor, } ) => { await test.step( 'Manually browse to focus mode for a Navigation Menu', async () => { - // We could Navigate directly to editing the Navigation Menu but we intentionally do not do this. - // - // Why? To provide coverage for a bug that caused the Navigation Editor behaviours to fail - // only when navigating through the editor screens (rather than going directly to the editor by URL). - // See: https://github.com/WordPress/gutenberg/pull/56856. - // - // Example (what we could do): - // await admin.visitSiteEditor( { - // postId: createdMenu?.id, - // postType: 'wp_navigation', - // } ); - // - await admin.visitSiteEditor(); - // create a Navigation Menu called "Test Menu" using the REST API helpers const createdMenu = await requestUtils.createNavigationMenu( { title: 'Primary Menu', @@ -48,6 +34,20 @@ test.describe( 'Editing Navigation Menus', () => { '<!-- wp:navigation-link {"label":"Another Item","type":"custom","url":"http://www.wordpress.org/","kind":"custom"} /-->', } ); + // We could Navigate directly to editing the Navigation Menu but we intentionally do not do this. + // + // Why? To provide coverage for a bug that caused the Navigation Editor behaviours to fail + // only when navigating through the editor screens (rather than going directly to the editor by URL). + // See: https://github.com/WordPress/gutenberg/pull/56856. + // + // Example (what we could do): + // await admin.visitSiteEditor( { + // postId: createdMenu?.id, + // postType: 'wp_navigation', + // } ); + // + await admin.visitSiteEditor(); + const editorSidebar = page.getByRole( 'region', { name: 'Navigation', } ); @@ -66,6 +66,10 @@ test.describe( 'Editing Navigation Menus', () => { } ) ).toBeVisible(); + await expect( page ).toHaveURL( + `wp-admin/site-editor.php?path=%2Fnavigation` + ); + await editorSidebar .getByRole( 'button', { name: 'Primary Menu', @@ -77,10 +81,12 @@ test.describe( 'Editing Navigation Menus', () => { ); // Wait for list of Navigations to appear. - editorSidebar.getByRole( 'heading', { - name: 'Primary Menu', - level: 1, - } ); + await expect( + editorSidebar.getByRole( 'heading', { + name: 'Primary Menu', + level: 1, + } ) + ).toBeVisible(); // Switch to editing the Navigation Menu await editorSidebar From 27ab284dda31ef828e70252bc00c67cbdc493be3 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 14 Dec 2023 11:16:52 +0100 Subject: [PATCH 184/325] Site Editor: Add Post Taxonomies panel (#57049) --- .../sidebar/post-taxonomies/index.js | 30 -- .../sidebar/settings-sidebar/index.js | 9 +- .../sidebar-edit-mode/page-panels/index.js | 7 +- .../sidebar-edit-mode/template-panel/index.js | 51 ++-- .../template-panel/pattern-categories.js | 279 ------------------ packages/editor/src/components/index.js | 1 + .../src/components/post-taxonomies/panel.js} | 26 +- 7 files changed, 59 insertions(+), 344 deletions(-) delete mode 100644 packages/edit-post/src/components/sidebar/post-taxonomies/index.js delete mode 100644 packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js rename packages/{edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js => editor/src/components/post-taxonomies/panel.js} (66%) diff --git a/packages/edit-post/src/components/sidebar/post-taxonomies/index.js b/packages/edit-post/src/components/sidebar/post-taxonomies/index.js deleted file mode 100644 index d44815ee98b71b..00000000000000 --- a/packages/edit-post/src/components/sidebar/post-taxonomies/index.js +++ /dev/null @@ -1,30 +0,0 @@ -/** - * WordPress dependencies - */ -import { - PostTaxonomies as PostTaxonomiesForm, - PostTaxonomiesCheck, -} from '@wordpress/editor'; - -/** - * Internal dependencies - */ -import TaxonomyPanel from './taxonomy-panel'; - -function PostTaxonomies() { - return ( - <PostTaxonomiesCheck> - <PostTaxonomiesForm - taxonomyWrapper={ ( content, taxonomy ) => { - return ( - <TaxonomyPanel taxonomy={ taxonomy }> - { content } - </TaxonomyPanel> - ); - } } - /> - </PostTaxonomiesCheck> - ); -} - -export default PostTaxonomies; diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 9b413d1858b589..3aac4d59df23b4 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -11,14 +11,17 @@ import { isRTL, __ } from '@wordpress/i18n'; import { drawerLeft, drawerRight } from '@wordpress/icons'; import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { store as editorStore, PostLastRevisionPanel } from '@wordpress/editor'; +import { + store as editorStore, + PostLastRevisionPanel, + PostTaxonomiesPanel, +} from '@wordpress/editor'; /** * Internal dependencies */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import PostTaxonomies from '../post-taxonomies'; import FeaturedImage from '../featured-image'; import PostExcerpt from '../post-excerpt'; import DiscussionPanel from '../discussion-panel'; @@ -79,7 +82,7 @@ const SidebarContent = ( { <PostStatus /> <PluginDocumentSettingPanel.Slot /> <PostLastRevisionPanel /> - <PostTaxonomies /> + <PostTaxonomiesPanel /> <FeaturedImage /> <PostExcerpt /> <DiscussionPanel /> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index d23dc87f42543c..28741f4ed6db7c 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -12,7 +12,11 @@ import { humanTimeDiff } from '@wordpress/date'; import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; -import { PostLastRevisionPanel, store as editorStore } from '@wordpress/editor'; +import { + PostLastRevisionPanel, + PostTaxonomiesPanel, + store as editorStore, +} from '@wordpress/editor'; /** * Internal dependencies @@ -95,6 +99,7 @@ export default function PagePanels() { </PanelBody> ) } <PostLastRevisionPanel /> + <PostTaxonomiesPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 2364053c834d71..97a8ba4db448e8 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -3,7 +3,11 @@ */ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; -import { PostLastRevisionPanel, store as editorStore } from '@wordpress/editor'; +import { + PostTaxonomiesPanel, + PostLastRevisionPanel, + store as editorStore, +} from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { navigation, symbol } from '@wordpress/icons'; @@ -15,8 +19,6 @@ import { store as editSiteStore } from '../../../store'; import TemplateActions from './template-actions'; import TemplateAreas from './template-areas'; import SidebarCard from '../sidebar-card'; -import PatternCategories from './pattern-categories'; -import { PATTERN_TYPES } from '../../../utils/constants'; const CARD_ICONS = { wp_block: symbol, @@ -24,29 +26,24 @@ const CARD_ICONS = { }; export default function TemplatePanel() { - const { title, description, icon, record, postType } = useSelect( - ( select ) => { - const { getEditedPostType, getEditedPostId } = - select( editSiteStore ); - const { getEditedEntityRecord } = select( coreStore ); - const { __experimentalGetTemplateInfo: getTemplateInfo } = - select( editorStore ); + const { title, description, icon, record } = useSelect( ( select ) => { + const { getEditedPostType, getEditedPostId } = select( editSiteStore ); + const { getEditedEntityRecord } = select( coreStore ); + const { __experimentalGetTemplateInfo: getTemplateInfo } = + select( editorStore ); - const type = getEditedPostType(); - const postId = getEditedPostId(); - const _record = getEditedEntityRecord( 'postType', type, postId ); - const info = getTemplateInfo( _record ); + const type = getEditedPostType(); + const postId = getEditedPostId(); + const _record = getEditedEntityRecord( 'postType', type, postId ); + const info = getTemplateInfo( _record ); - return { - title: info.title, - description: info.description, - icon: info.icon, - record: _record, - postType: type, - }; - }, - [] - ); + return { + title: info.title, + description: info.description, + icon: info.icon, + record: _record, + }; + }, [] ); if ( ! title && ! description ) { return null; @@ -66,11 +63,7 @@ export default function TemplatePanel() { </SidebarCard> </PanelBody> <PostLastRevisionPanel /> - <PanelBody> - { postType === PATTERN_TYPES.user && ( - <PatternCategories post={ record } /> - ) } - </PanelBody> + <PostTaxonomiesPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js deleted file mode 100644 index 3740b622361ff5..00000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/pattern-categories.js +++ /dev/null @@ -1,279 +0,0 @@ -/** - * WordPress dependencies - */ -import { __, _x, sprintf } from '@wordpress/i18n'; -import { useEffect, useMemo, useState } from '@wordpress/element'; -import { FormTokenField, FlexBlock, PanelRow } from '@wordpress/components'; -import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; -import { useDebounce } from '@wordpress/compose'; -import { store as noticesStore } from '@wordpress/notices'; -import { decodeEntities } from '@wordpress/html-entities'; - -/** - * Internal dependencies - */ -import { PATTERN_TYPES } from '../../../utils/constants'; - -export const unescapeString = ( arg ) => { - return decodeEntities( arg ); -}; - -/** - * Returns a term object with name unescaped. - * - * @param {Object} term The term object to unescape. - * - * @return {Object} Term object with name property unescaped. - */ -export const unescapeTerm = ( term ) => { - return { - ...term, - name: unescapeString( term.name ), - }; -}; - -/** - * Shared reference to an empty array for cases where it is important to avoid - * returning a new array reference on every invocation. - * - * @type {Array<any>} - */ -const EMPTY_ARRAY = []; - -/** - * Module constants - */ -const MAX_TERMS_SUGGESTIONS = 20; -const DEFAULT_QUERY = { - per_page: MAX_TERMS_SUGGESTIONS, - _fields: 'id,name', - context: 'view', -}; - -const isSameTermName = ( termA, termB ) => - unescapeString( termA ).toLowerCase() === - unescapeString( termB ).toLowerCase(); - -const termNamesToIds = ( names, terms ) => { - return names.map( - ( termName ) => - terms.find( ( term ) => isSameTermName( term.name, termName ) ).id - ); -}; - -export default function PatternCategories( { post } ) { - const slug = 'wp_pattern_category'; - const [ values, setValues ] = useState( [] ); - const [ search, setSearch ] = useState( '' ); - const debouncedSearch = useDebounce( setSearch, 500 ); - - const { - terms, - taxonomy, - hasAssignAction, - hasCreateAction, - hasResolvedTerms, - } = useSelect( - ( select ) => { - const { getEntityRecords, getTaxonomy, hasFinishedResolution } = - select( coreStore ); - const _taxonomy = getTaxonomy( slug ); - const _termIds = - post?.wp_pattern_category?.length > 0 - ? post?.wp_pattern_category - : EMPTY_ARRAY; - const query = { - ...DEFAULT_QUERY, - include: _termIds?.join( ',' ), - per_page: -1, - }; - - return { - hasCreateAction: _taxonomy - ? post._links?.[ - 'wp:action-create-' + _taxonomy.rest_base - ] ?? false - : false, - hasAssignAction: _taxonomy - ? post._links?.[ - 'wp:action-assign-' + _taxonomy.rest_base - ] ?? false - : false, - taxonomy: _taxonomy, - termIds: _termIds, - terms: _termIds?.length - ? getEntityRecords( 'taxonomy', slug, query ) - : EMPTY_ARRAY, - hasResolvedTerms: hasFinishedResolution( 'getEntityRecords', [ - 'taxonomy', - slug, - query, - ] ), - }; - }, - [ slug, post ] - ); - - const { searchResults } = useSelect( - ( select ) => { - const { getEntityRecords } = select( coreStore ); - - return { - searchResults: !! search - ? getEntityRecords( 'taxonomy', slug, { - ...DEFAULT_QUERY, - search, - } ) - : EMPTY_ARRAY, - }; - }, - [ search, slug ] - ); - - // Update terms state only after the selectors are resolved. - // We're using this to avoid terms temporarily disappearing on slow networks - // while core data makes REST API requests. - useEffect( () => { - if ( hasResolvedTerms ) { - const newValues = ( terms ?? [] ).map( ( term ) => - unescapeString( term.name ) - ); - - setValues( newValues ); - } - }, [ terms, hasResolvedTerms ] ); - - const suggestions = useMemo( () => { - return ( searchResults ?? [] ).map( ( term ) => - unescapeString( term.name ) - ); - }, [ searchResults ] ); - - const { saveEntityRecord, editEntityRecord, invalidateResolution } = - useDispatch( coreStore ); - const { createErrorNotice } = useDispatch( noticesStore ); - - if ( ! hasAssignAction ) { - return null; - } - - async function findOrCreateTerm( term ) { - try { - const newTerm = await saveEntityRecord( 'taxonomy', slug, term, { - throwOnError: true, - } ); - invalidateResolution( 'getUserPatternCategories' ); - return unescapeTerm( newTerm ); - } catch ( error ) { - if ( error.code !== 'term_exists' ) { - throw error; - } - - return { - id: error.data.term_id, - name: term.name, - }; - } - } - - function onUpdateTerms( newTermIds ) { - editEntityRecord( 'postType', PATTERN_TYPES.user, post.id, { - wp_pattern_category: newTermIds, - } ); - } - - function onChange( termNames ) { - const availableTerms = [ - ...( terms ?? [] ), - ...( searchResults ?? [] ), - ]; - const uniqueTerms = termNames.reduce( ( acc, name ) => { - if ( - ! acc.some( ( n ) => n.toLowerCase() === name.toLowerCase() ) - ) { - acc.push( name ); - } - return acc; - }, [] ); - - const newTermNames = uniqueTerms.filter( - ( termName ) => - ! availableTerms.find( ( term ) => - isSameTermName( term.name, termName ) - ) - ); - - // Optimistically update term values. - // The selector will always re-fetch terms later. - setValues( uniqueTerms ); - - if ( newTermNames.length === 0 ) { - return onUpdateTerms( - termNamesToIds( uniqueTerms, availableTerms ) - ); - } - - if ( ! hasCreateAction ) { - return; - } - - Promise.all( - newTermNames.map( ( termName ) => - findOrCreateTerm( { name: termName } ) - ) - ) - .then( ( newTerms ) => { - const newAvailableTerms = availableTerms.concat( newTerms ); - return onUpdateTerms( - termNamesToIds( uniqueTerms, newAvailableTerms ) - ); - } ) - .catch( ( error ) => { - createErrorNotice( error.message, { - type: 'snackbar', - } ); - } ); - } - - const singularName = - taxonomy?.labels?.singular_name ?? - ( slug === 'post_tag' ? __( 'Tag' ) : __( 'Term' ) ); - const termAddedLabel = sprintf( - /* translators: %s: term name. */ - _x( '%s added', 'term' ), - singularName - ); - const termRemovedLabel = sprintf( - /* translators: %s: term name. */ - _x( '%s removed', 'term' ), - singularName - ); - const removeTermLabel = sprintf( - /* translators: %s: term name. */ - _x( 'Remove %s', 'term' ), - singularName - ); - - return ( - <PanelRow initialOpen={ true } title={ __( 'Categories' ) }> - <FlexBlock> - <FormTokenField - __next40pxDefaultSize - value={ values } - suggestions={ suggestions } - onChange={ onChange } - onInputChange={ debouncedSearch } - maxSuggestions={ MAX_TERMS_SUGGESTIONS } - label={ __( 'Pattern categories' ) } - messages={ { - added: termAddedLabel, - removed: termRemovedLabel, - remove: removeTermLabel, - } } - tokenizeOnBlur - /> - </FlexBlock> - </PanelRow> - ); -} diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index a16c01c8c166ad..4a0dfac03dd5ac 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -68,6 +68,7 @@ export { default as PostTaxonomies } from './post-taxonomies'; export { FlatTermSelector as PostTaxonomiesFlatTermSelector } from './post-taxonomies/flat-term-selector'; export { HierarchicalTermSelector as PostTaxonomiesHierarchicalTermSelector } from './post-taxonomies/hierarchical-term-selector'; export { default as PostTaxonomiesCheck } from './post-taxonomies/check'; +export { default as PostTaxonomiesPanel } from './post-taxonomies/panel'; export { default as PostTextEditor } from './post-text-editor'; export { default as PostTitle } from './post-title'; export { default as PostTitleRaw } from './post-title/post-title-raw'; diff --git a/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js b/packages/editor/src/components/post-taxonomies/panel.js similarity index 66% rename from packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js rename to packages/editor/src/components/post-taxonomies/panel.js index 4b3bab1fcf3442..a2c2d175246403 100644 --- a/packages/edit-post/src/components/sidebar/post-taxonomies/taxonomy-panel.js +++ b/packages/editor/src/components/post-taxonomies/panel.js @@ -3,7 +3,13 @@ */ import { PanelBody } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { store as editorStore } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import PostTaxonomiesForm from './index'; +import PostTaxonomiesCheck from './check'; function TaxonomyPanel( { taxonomy, children } ) { const slug = taxonomy?.slug; @@ -41,4 +47,20 @@ function TaxonomyPanel( { taxonomy, children } ) { ); } -export default TaxonomyPanel; +function PostTaxonomies() { + return ( + <PostTaxonomiesCheck> + <PostTaxonomiesForm + taxonomyWrapper={ ( content, taxonomy ) => { + return ( + <TaxonomyPanel taxonomy={ taxonomy }> + { content } + </TaxonomyPanel> + ); + } } + /> + </PostTaxonomiesCheck> + ); +} + +export default PostTaxonomies; From 23ad2fa28c9975133d275abe4db130617f084c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:37:56 +0100 Subject: [PATCH 185/325] DataViews: display column header when the field is only filterable (#57051) --- packages/dataviews/src/view-table.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 008bd9f02c7022..71d05e66c6b3c3 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -45,9 +45,6 @@ const sortIcons = { asc: chevronUp, desc: chevronDown }; function HeaderMenu( { field, view, onChangeView } ) { const isSortable = field.enableSorting !== false; const isHidable = field.enableHiding !== false; - if ( ! isSortable && ! isHidable ) { - return field.header; - } const isSorted = view.sort?.field === field.id; let filter, filterInView; const otherFilters = []; @@ -74,6 +71,10 @@ function HeaderMenu( { field, view, onChangeView } ) { } const isFilterable = !! filter; + if ( ! isSortable && ! isHidable && ! isFilterable ) { + return field.header; + } + if ( isFilterable ) { const columnFilters = view.filters; columnFilters.forEach( ( columnFilter ) => { From 65cc5527d1a06f2c9d1a90a5178fe5104b47dc91 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Thu, 14 Dec 2023 11:53:29 +0100 Subject: [PATCH 186/325] Site Editor: Add the featured image panel (#57053) --- .../sidebar/featured-image/index.js | 64 ------------------- .../sidebar/settings-sidebar/index.js | 4 +- .../sidebar-edit-mode/page-panels/index.js | 2 + .../sidebar-edit-mode/template-panel/index.js | 4 +- packages/editor/src/components/index.js | 1 + .../components/post-featured-image/panel.js | 55 ++++++++++++++++ 6 files changed, 63 insertions(+), 67 deletions(-) delete mode 100644 packages/edit-post/src/components/sidebar/featured-image/index.js create mode 100644 packages/editor/src/components/post-featured-image/panel.js diff --git a/packages/edit-post/src/components/sidebar/featured-image/index.js b/packages/edit-post/src/components/sidebar/featured-image/index.js deleted file mode 100644 index b528a7097231d6..00000000000000 --- a/packages/edit-post/src/components/sidebar/featured-image/index.js +++ /dev/null @@ -1,64 +0,0 @@ -/** - * WordPress dependencies - */ -import { __ } from '@wordpress/i18n'; -import { PanelBody } from '@wordpress/components'; -import { - PostFeaturedImage, - PostFeaturedImageCheck, - store as editorStore, -} from '@wordpress/editor'; -import { compose } from '@wordpress/compose'; -import { withSelect, withDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; - -/** - * Module Constants - */ -const PANEL_NAME = 'featured-image'; - -function FeaturedImage( { isEnabled, isOpened, postType, onTogglePanel } ) { - if ( ! isEnabled ) { - return null; - } - - return ( - <PostFeaturedImageCheck> - <PanelBody - title={ - postType?.labels?.featured_image ?? __( 'Featured image' ) - } - opened={ isOpened } - onToggle={ onTogglePanel } - > - <PostFeaturedImage /> - </PanelBody> - </PostFeaturedImageCheck> - ); -} - -const applyWithSelect = withSelect( ( select ) => { - const { - getEditedPostAttribute, - isEditorPanelEnabled, - isEditorPanelOpened, - } = select( editorStore ); - const { getPostType } = select( coreStore ); - - return { - postType: getPostType( getEditedPostAttribute( 'type' ) ), - isEnabled: isEditorPanelEnabled( PANEL_NAME ), - isOpened: isEditorPanelOpened( PANEL_NAME ), - }; -} ); - -const applyWithDispatch = withDispatch( ( dispatch ) => { - const { toggleEditorPanelOpened } = dispatch( editorStore ); - - return { - onTogglePanel: ( ...args ) => - toggleEditorPanelOpened( PANEL_NAME, ...args ), - }; -} ); - -export default compose( applyWithSelect, applyWithDispatch )( FeaturedImage ); diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 3aac4d59df23b4..465088b39bf80f 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -13,6 +13,7 @@ import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editorStore, + PostFeaturedImagePanel, PostLastRevisionPanel, PostTaxonomiesPanel, } from '@wordpress/editor'; @@ -22,7 +23,6 @@ import { */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import FeaturedImage from '../featured-image'; import PostExcerpt from '../post-excerpt'; import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; @@ -83,7 +83,7 @@ const SidebarContent = ( { <PluginDocumentSettingPanel.Slot /> <PostLastRevisionPanel /> <PostTaxonomiesPanel /> - <FeaturedImage /> + <PostFeaturedImagePanel /> <PostExcerpt /> <DiscussionPanel /> <PageAttributes /> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index 28741f4ed6db7c..f6c84d8cfd3adc 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -13,6 +13,7 @@ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { + PostFeaturedImagePanel, PostLastRevisionPanel, PostTaxonomiesPanel, store as editorStore, @@ -100,6 +101,7 @@ export default function PagePanels() { ) } <PostLastRevisionPanel /> <PostTaxonomiesPanel /> + <PostFeaturedImagePanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 97a8ba4db448e8..66b5991872cf96 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -4,8 +4,9 @@ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; import { - PostTaxonomiesPanel, + PostFeaturedImagePanel, PostLastRevisionPanel, + PostTaxonomiesPanel, store as editorStore, } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; @@ -64,6 +65,7 @@ export default function TemplatePanel() { </PanelBody> <PostLastRevisionPanel /> <PostTaxonomiesPanel /> + <PostFeaturedImagePanel /> </> ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 4a0dfac03dd5ac..79a4d90663f669 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -34,6 +34,7 @@ export { default as PostExcerpt } from './post-excerpt'; export { default as PostExcerptCheck } from './post-excerpt/check'; export { default as PostFeaturedImage } from './post-featured-image'; export { default as PostFeaturedImageCheck } from './post-featured-image/check'; +export { default as PostFeaturedImagePanel } from './post-featured-image/panel'; export { default as PostFormat } from './post-format'; export { default as PostFormatCheck } from './post-format/check'; export { default as PostLastRevision } from './post-last-revision'; diff --git a/packages/editor/src/components/post-featured-image/panel.js b/packages/editor/src/components/post-featured-image/panel.js new file mode 100644 index 00000000000000..c53aa153514611 --- /dev/null +++ b/packages/editor/src/components/post-featured-image/panel.js @@ -0,0 +1,55 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-data'; + +/** + * Internal dependencies + */ +import { store as editorStore } from '../../store'; +import PostFeaturedImage from './index'; +import PostFeaturedImageCheck from './check'; + +const PANEL_NAME = 'featured-image'; + +function FeaturedImage() { + const { postType, isEnabled, isOpened } = useSelect( ( select ) => { + const { + getEditedPostAttribute, + isEditorPanelEnabled, + isEditorPanelOpened, + } = select( editorStore ); + const { getPostType } = select( coreStore ); + + return { + postType: getPostType( getEditedPostAttribute( 'type' ) ), + isEnabled: isEditorPanelEnabled( PANEL_NAME ), + isOpened: isEditorPanelOpened( PANEL_NAME ), + }; + }, [] ); + + const { toggleEditorPanelOpened } = useDispatch( editorStore ); + + if ( ! isEnabled ) { + return null; + } + + return ( + <PostFeaturedImageCheck> + <PanelBody + title={ + postType?.labels?.featured_image ?? __( 'Featured image' ) + } + opened={ isOpened } + onToggle={ () => toggleEditorPanelOpened( PANEL_NAME ) } + > + <PostFeaturedImage /> + </PanelBody> + </PostFeaturedImageCheck> + ); +} + +export default FeaturedImage; From bcc5a9bfa4a9fb1cf1233c73dd3e73ab5129533d Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Thu, 14 Dec 2023 11:32:13 +0000 Subject: [PATCH 187/325] DataViews: Use default button variants (#57057) --- packages/dataviews/src/add-filter.js | 1 - packages/dataviews/src/view-actions.js | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index 5403d36703128c..8df218c0603703 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -55,7 +55,6 @@ export default function AddFilter( { filters, view, onChangeView } ) { <Button __experimentalIsFocusable label={ __( 'Filters' ) } - variant="tertiary" size="compact" icon={ funnel } className="dataviews-filters-button" diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 628ded53f169d7..836bef54936768 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -261,7 +261,6 @@ export default function ViewActions( { <DropdownMenu trigger={ <Button - variant="tertiary" size="compact" icon={ VIEW_LAYOUTS.find( ( v ) => v.type === view.type ) From 8c010bd5510745ea07d9373108fd2cb05e2b3b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:34:56 +0100 Subject: [PATCH 188/325] DataViews: centralize control of filter visibility in the Filters component (#57056) --- packages/dataviews/src/filter-summary.js | 6 +----- packages/dataviews/src/filters.js | 11 ++++++++--- packages/dataviews/src/reset-filters.js | 9 --------- 3 files changed, 9 insertions(+), 17 deletions(-) diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index fc0f8848f6a939..3c30c6837103a7 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -13,7 +13,7 @@ import { Children, Fragment } from '@wordpress/element'; /** * Internal dependencies */ -import { OPERATOR_IN, OPERATOR_NOT_IN, LAYOUT_LIST } from './constants'; +import { OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; import { unlock } from './lock-unlock'; const { @@ -73,10 +73,6 @@ function WithSeparators( { children } ) { } export default function FilterSummary( { filter, view, onChangeView } ) { - if ( view.type === LAYOUT_LIST ) { - return null; - } - const filterInView = view.filters.find( ( f ) => f.field === filter.field ); const activeElement = filter.elements.find( ( element ) => element.value === filterInView?.value diff --git a/packages/dataviews/src/filters.js b/packages/dataviews/src/filters.js index f8b1e59e8c9d15..6195eefe8fe471 100644 --- a/packages/dataviews/src/filters.js +++ b/packages/dataviews/src/filters.js @@ -4,7 +4,12 @@ import FilterSummary from './filter-summary'; import AddFilter from './add-filter'; import ResetFilters from './reset-filters'; -import { ENUMERATION_TYPE, OPERATOR_IN, OPERATOR_NOT_IN } from './constants'; +import { + ENUMERATION_TYPE, + OPERATOR_IN, + OPERATOR_NOT_IN, + LAYOUT_LIST, +} from './constants'; const sanitizeOperators = ( field ) => { let operators = field.filterBy?.operators; @@ -57,7 +62,7 @@ export default function Filters( { fields, view, onChangeView } ) { const filterComponents = [ addFilter, ...filters.map( ( filter ) => { - if ( ! filter.isVisible ) { + if ( ! filter.isVisible || view.type === LAYOUT_LIST ) { return null; } @@ -72,7 +77,7 @@ export default function Filters( { fields, view, onChangeView } ) { } ), ]; - if ( filterComponents.length > 1 ) { + if ( filterComponents.length > 1 && view.type !== LAYOUT_LIST ) { filterComponents.push( <ResetFilters key="reset-filters" diff --git a/packages/dataviews/src/reset-filters.js b/packages/dataviews/src/reset-filters.js index 503892b6e07377..d78c06624087a7 100644 --- a/packages/dataviews/src/reset-filters.js +++ b/packages/dataviews/src/reset-filters.js @@ -4,16 +4,7 @@ import { Button } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -/** - * Internal dependencies - */ -import { LAYOUT_LIST } from './constants'; - export default ( { view, onChangeView } ) => { - if ( view.type === LAYOUT_LIST ) { - return null; - } - return ( <Button disabled={ view.search === '' && view.filters?.length === 0 } From d949e446876b89be3bd9d5e3ae2f8f3aefa25014 Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Thu, 14 Dec 2023 12:35:47 +0000 Subject: [PATCH 189/325] DataViews: add hover style to table rows (#57058) --- packages/dataviews/src/style.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index ccb565c0b58673..1282d28581dcee 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -97,6 +97,12 @@ &:last-child { border-bottom: 0; } + + &:hover { + td { + background-color: #f8f8f8; + } + } } thead { tr { From 053a8aae5cf61fe09fddd271c7002da4f1e783ab Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 14 Dec 2023 21:50:53 +0900 Subject: [PATCH 190/325] a11y: Apply focus style to revision items (#57039) --- .../global-styles/screen-revisions/style.scss | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index d1325f84772a62..a127142ac60edb 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -13,8 +13,6 @@ .edit-site-global-styles-screen-revisions__revision-item { position: relative; - padding-left: $grid-unit-20; - overflow: hidden; cursor: pointer; &:hover { @@ -40,9 +38,20 @@ left: $grid-unit-20 + 1; // So the circle is centered on the line. transform: translate(-50%, -50%); z-index: 1; + + // This border serves as a background color in Windows High Contrast mode. + border: 4px solid transparent; } - &.is-selected::before { - background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + &.is-selected { + border-radius: $radius-block-ui; + + // Only visible in Windows High Contrast mode. + outline: 3px solid transparent; + outline-offset: -2px; + + &::before { + background: var(--wp-components-color-accent, var(--wp-admin-theme-color, #007cba)); + } } &::after { @@ -66,12 +75,10 @@ width: 100%; height: auto; display: block; - padding: $grid-unit-15 $grid-unit-15 $grid-unit-10 $grid-unit-30; - &:focus, - &:active { - outline: 0; - box-shadow: none; - } + padding: $grid-unit-15 $grid-unit-15 $grid-unit-10 $grid-unit-50; + z-index: 1; + position: relative; + outline-offset: -2px; } } From fbc1188fe00b159bc461b34f4b35fb0c30c6312c Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Thu, 14 Dec 2023 15:10:38 +0200 Subject: [PATCH 191/325] Components: Fix logic of `has-text` class addition in Button (#56949) * Components: Remove fixed width for `compact` sized Buttons * update changelog * Fix logic of has-text class addition in Button * fix changelog --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/button/index.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 4114a32f148542..020586947a2f25 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,6 +6,10 @@ - `TabPanel`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). +### Bug Fix + +- `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). + ## 25.14.0 (2023-12-13) ### Enhancements diff --git a/packages/components/src/button/index.tsx b/packages/components/src/button/index.tsx index b14e85fa52f7f6..bd91de2ec2e83e 100644 --- a/packages/components/src/button/index.tsx +++ b/packages/components/src/button/index.tsx @@ -156,7 +156,7 @@ export function UnforwardedButton( 'is-busy': isBusy, 'is-link': variant === 'link', 'is-destructive': isDestructive, - 'has-text': !! icon && hasChildren, + 'has-text': !! icon && ( hasChildren || text ), 'has-icon': !! icon, } ); From a5a829c9cacb4032a2e0b5fcd071070082a5597a Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:13:12 +0900 Subject: [PATCH 192/325] PaletteEdit: Consider digits when generating kebab-cased slug (#56713) * PaletteEdit: Consider digits when generating kebab-cased slug * Update changelog --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/palette-edit/index.tsx | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 020586947a2f25..b1c41b9a05f53f 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). + ### Experimental - `TabPanel`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index 40621a407f2173..91303471792ebd 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -2,7 +2,6 @@ * External dependencies */ import classnames from 'classnames'; -import { paramCase as kebabCase } from 'change-case'; /** * WordPress dependencies @@ -49,6 +48,7 @@ import { import { NavigableMenu } from '../navigable-container'; import { DEFAULT_GRADIENT } from '../custom-gradient-picker/constants'; import CustomGradientPicker from '../custom-gradient-picker'; +import { kebabCase } from '../utils/strings'; import type { Color, ColorPickerPopoverProps, From 0576c01a59941a41672331be2c16cf60bbda52a2 Mon Sep 17 00:00:00 2001 From: David Calhoun <github@davidcalhoun.me> Date: Thu, 14 Dec 2023 09:17:17 -0500 Subject: [PATCH 193/325] fix: Prevent unnecessary content changes clearing redo actions (#57028) * fix: Prevent unnecessary content changes clearing redo actions In this context, `this.value` is not a string but a instance of `RichTextData`. Therefore, comparing the two values results in unexpected inequality, triggering an update of the block's `attributes.content` toggling it from a `ReactTextData` instance to a string. This toggle results in the undo manager tracking the change as a new line of editor history, clearing out any pending redo actions. The `RichTextData` type was introduced in #43204. Invoking `toString` may not be the best long-term solution to this problem. Refactoring the rich text implementation to appropriately leverage `RichTextData` and (potentially) treat `value` and `record` values different and storing them separately may be necessary. * fix: Convert `RichTextData` to string before comparing values The value stored in the rich text component may be a string or a `RichTextData`. Until the value is store consistently, it may be necessary to convert each value to a string prior to equality comparisons. * test: Verify change events with equal values do not update attributes Ensure empty string values do not cause unnecessary attribute updates when comparing string values to empty `RichTextData` values, which is the new default value. --- .../rich-text/native/index.native.js | 21 +++++++------- .../rich-text/native/test/index.native.js | 29 +++++++++++++++++-- 2 files changed, 38 insertions(+), 12 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 165316fdbde769..116425a15b53b1 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -267,7 +267,7 @@ export class RichText extends Component { onCreateUndoLevel() { const { __unstableOnCreateUndoLevel: onCreateUndoLevel } = this.props; // If the content is the same, no level needs to be created. - if ( this.lastHistoryValue === this.value ) { + if ( this.lastHistoryValue.toString() === this.value.toString() ) { return; } @@ -320,7 +320,7 @@ export class RichText extends Component { unescapeSpaces( event.nativeEvent.text ) ); // On iOS, onChange can be triggered after selection changes, even though there are no content changes. - if ( contentWithoutRootTag === this.value ) { + if ( contentWithoutRootTag === this.value.toString() ) { return; } this.lastEventCount = event.nativeEvent.eventCount; @@ -336,7 +336,7 @@ export class RichText extends Component { ); this.debounceCreateUndoLevel(); - const refresh = this.value !== contentWithoutRootTag; + const refresh = this.value.toString() !== contentWithoutRootTag; this.value = contentWithoutRootTag; // We don't want to refresh if our goal is just to create a record. @@ -567,7 +567,7 @@ export class RichText extends Component { // Check if value is up to date with latest state of native AztecView. if ( event.nativeEvent.text && - event.nativeEvent.text !== this.props.value + event.nativeEvent.text !== this.props.value.toString() ) { this.onTextUpdate( event ); } @@ -592,7 +592,7 @@ export class RichText extends Component { // this approach is not perfectly reliable. const isManual = this.lastAztecEventType !== 'input' && - this.props.value === this.value; + this.props.value.toString() === this.value.toString(); if ( hasChanged && isManual ) { const value = this.createRecord(); const activeFormats = getActiveFormats( value ); @@ -662,7 +662,7 @@ export class RichText extends Component { unescapeSpaces( event.nativeEvent.text ) ); if ( - contentWithoutRootTag === this.value && + contentWithoutRootTag === this.value.toString() && realStart === this.selectionStart && realEnd === this.selectionEnd ) { @@ -759,7 +759,7 @@ export class RichText extends Component { typeof nextProps.value !== 'undefined' && typeof this.props.value !== 'undefined' && ( ! this.comesFromAztec || ! this.firedAfterTextChanged ) && - nextProps.value !== this.props.value + nextProps.value.toString() !== this.props.value.toString() ) { // Gutenberg seems to try to mirror the caret state even on events that only change the content so, // let's force caret update if state has selection set. @@ -833,7 +833,7 @@ export class RichText extends Component { const { style, tagName } = this.props; const { currentFontSize } = this.state; - if ( this.props.value !== this.value ) { + if ( this.props.value.toString() !== this.value.toString() ) { this.value = this.props.value; } const { __unstableIsSelected: prevIsSelected } = prevProps; @@ -851,7 +851,7 @@ export class RichText extends Component { // Since this is happening when merging blocks, the selection should be at the last character position. // As a fallback the internal selectionEnd value is used. const lastCharacterPosition = - this.value?.length ?? this.selectionEnd; + this.value?.toString().length ?? this.selectionEnd; this._editor.focus(); this.props.onSelectionChange( lastCharacterPosition, @@ -893,7 +893,8 @@ export class RichText extends Component { // On android if content is empty we need to send no content or else the placeholder will not show. if ( ! this.isIOS && - ( value === '' || value === EMPTY_PARAGRAPH_TAGS ) + ( value.toString() === '' || + value.toString() === EMPTY_PARAGRAPH_TAGS ) ) { return ''; } diff --git a/packages/block-editor/src/components/rich-text/native/test/index.native.js b/packages/block-editor/src/components/rich-text/native/test/index.native.js index 64bfb3b183c6b9..6e7dc31fc74e79 100644 --- a/packages/block-editor/src/components/rich-text/native/test/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/test/index.native.js @@ -2,13 +2,18 @@ * External dependencies */ import { Dimensions } from 'react-native'; -import { getEditorHtml, render, initializeEditor } from 'test/helpers'; +import { + fireEvent, + getEditorHtml, + render, + initializeEditor, +} from 'test/helpers'; /** * WordPress dependencies */ import { select } from '@wordpress/data'; -import { store as richTextStore } from '@wordpress/rich-text'; +import { store as richTextStore, RichTextData } from '@wordpress/rich-text'; import { coreBlocks } from '@wordpress/block-library'; import { getBlockTypes, @@ -78,6 +83,26 @@ describe( '<RichText/>', () => { } ); } ); + describe( 'when changes arrive from Aztec', () => { + it( 'should avoid updating attributes when values are equal', async () => { + const handleChange = jest.fn(); + const defaultEmptyValue = new RichTextData(); + const screen = render( + <RichText + onChange={ handleChange } + value={ defaultEmptyValue } + /> + ); + + // Simulate an empty string from Aztec + fireEvent( screen.getByLabelText( 'Text input. Empty' ), 'change', { + nativeEvent: { text: '' }, + } ); + + expect( handleChange ).not.toHaveBeenCalled(); + } ); + } ); + describe( 'Font Size', () => { it( 'should display rich text at the DEFAULT font size.', () => { // Arrange. From 8d13605c1cbcc9509f947282b55c3b922ca09685 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:35:04 +0100 Subject: [PATCH 194/325] Block editor: hooks: manage save props in one place (#57043) --- packages/block-editor/src/hooks/align.js | 6 +-- packages/block-editor/src/hooks/anchor.js | 6 +-- packages/block-editor/src/hooks/aria-label.js | 13 +++-- packages/block-editor/src/hooks/border.js | 7 +-- packages/block-editor/src/hooks/color.js | 7 +-- .../src/hooks/custom-class-name.js | 6 +-- .../src/hooks/custom-class-name.native.js | 13 +++-- .../block-editor/src/hooks/font-family.js | 7 +-- packages/block-editor/src/hooks/font-size.js | 7 +-- packages/block-editor/src/hooks/index.js | 19 ++++++- .../block-editor/src/hooks/index.native.js | 16 +++++- packages/block-editor/src/hooks/style.js | 7 +-- .../block-editor/src/hooks/test/anchor.js | 13 ++--- .../src/hooks/test/custom-class-name.js | 11 ++-- packages/block-editor/src/hooks/test/style.js | 18 ++----- packages/block-editor/src/hooks/utils.js | 50 ++++++++++++++++++- 16 files changed, 114 insertions(+), 92 deletions(-) diff --git a/packages/block-editor/src/hooks/align.js b/packages/block-editor/src/hooks/align.js index 189f82ccf429f8..3e4a49bb385558 100644 --- a/packages/block-editor/src/hooks/align.js +++ b/packages/block-editor/src/hooks/align.js @@ -155,6 +155,7 @@ export default { shareWithChildBlocks: true, edit: BlockEditAlignmentToolbarControlsPure, useBlockProps, + addSaveProps: addAssignedAlign, attributeKeys: [ 'align' ], hasSupport( name ) { return hasBlockSupport( name, 'align', false ); @@ -209,8 +210,3 @@ addFilter( 'core/editor/align/addAttribute', addAttribute ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/editor/align/addAssignedAlign', - addAssignedAlign -); diff --git a/packages/block-editor/src/hooks/anchor.js b/packages/block-editor/src/hooks/anchor.js index 882820678aa870..2e79a9d9db17b2 100644 --- a/packages/block-editor/src/hooks/anchor.js +++ b/packages/block-editor/src/hooks/anchor.js @@ -120,6 +120,7 @@ function BlockEditAnchorControlPure( { } export default { + addSaveProps, edit: BlockEditAnchorControlPure, attributeKeys: [ 'anchor' ], hasSupport( name ) { @@ -147,8 +148,3 @@ export function addSaveProps( extraProps, blockType, attributes ) { } addFilter( 'blocks.registerBlockType', 'core/anchor/attribute', addAttribute ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/editor/anchor/save-props', - addSaveProps -); diff --git a/packages/block-editor/src/hooks/aria-label.js b/packages/block-editor/src/hooks/aria-label.js index c4387daab71137..7f93aa4ff8c8b2 100644 --- a/packages/block-editor/src/hooks/aria-label.js +++ b/packages/block-editor/src/hooks/aria-label.js @@ -55,13 +55,16 @@ export function addSaveProps( extraProps, blockType, attributes ) { return extraProps; } +export default { + addSaveProps, + attributeKeys: [ 'ariaLabel' ], + hasSupport( name ) { + return hasBlockSupport( name, 'ariaLabel' ); + }, +}; + addFilter( 'blocks.registerBlockType', 'core/ariaLabel/attribute', addAttribute ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/ariaLabel/save-props', - addSaveProps -); diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index c6947eeaa18e38..a11fdc4b97e48b 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -348,6 +348,7 @@ function useBlockProps( { name, borderColor, style } ) { export default { useBlockProps, + addSaveProps, attributeKeys: [ 'borderColor', 'style' ], hasSupport( name ) { return hasBorderSupport( name, 'color' ); @@ -359,9 +360,3 @@ addFilter( 'core/border/addAttributes', addAttributes ); - -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/border/addSaveProps', - addSaveProps -); diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index db6c3dc8fd86ce..267bafe1201739 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -399,6 +399,7 @@ function useBlockProps( { export default { useBlockProps, + addSaveProps, attributeKeys: [ 'backgroundColor', 'textColor', 'gradient', 'style' ], hasSupport: hasColorSupport, }; @@ -437,12 +438,6 @@ addFilter( addAttributes ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/color/addSaveProps', - addSaveProps -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/color/addTransforms', diff --git a/packages/block-editor/src/hooks/custom-class-name.js b/packages/block-editor/src/hooks/custom-class-name.js index 331edd9ef214a2..037fafe9ca840f 100644 --- a/packages/block-editor/src/hooks/custom-class-name.js +++ b/packages/block-editor/src/hooks/custom-class-name.js @@ -65,6 +65,7 @@ function CustomClassNameControlsPure( { className, setAttributes } ) { export default { edit: CustomClassNameControlsPure, + addSaveProps, attributeKeys: [ 'className' ], hasSupport( name ) { return hasBlockSupport( name, 'customClassName', true ); @@ -140,11 +141,6 @@ addFilter( 'core/editor/custom-class-name/attribute', addAttribute ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/editor/custom-class-name/save-props', - addSaveProps -); addFilter( 'blocks.switchToBlockType.transformedBlock', diff --git a/packages/block-editor/src/hooks/custom-class-name.native.js b/packages/block-editor/src/hooks/custom-class-name.native.js index 65ba2505053755..8d2b6560332e45 100644 --- a/packages/block-editor/src/hooks/custom-class-name.native.js +++ b/packages/block-editor/src/hooks/custom-class-name.native.js @@ -60,8 +60,11 @@ addFilter( 'core/custom-class-name/attribute', addAttribute ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/custom-class-name/save-props', - addSaveProps -); + +export default { + addSaveProps, + attributeKeys: [ 'className' ], + hasSupport( name ) { + return hasBlockSupport( name, 'customClassName', true ); + }, +}; diff --git a/packages/block-editor/src/hooks/font-family.js b/packages/block-editor/src/hooks/font-family.js index ae41b7fa34b1f5..db6515ef1c2fe0 100644 --- a/packages/block-editor/src/hooks/font-family.js +++ b/packages/block-editor/src/hooks/font-family.js @@ -82,6 +82,7 @@ function useBlockProps( { name, fontFamily } ) { export default { useBlockProps, + addSaveProps, attributeKeys: [ 'fontFamily' ], hasSupport( name ) { return hasBlockSupport( name, FONT_FAMILY_SUPPORT_KEY ); @@ -105,9 +106,3 @@ addFilter( 'core/fontFamily/addAttribute', addAttributes ); - -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/fontFamily/addSaveProps', - addSaveProps -); diff --git a/packages/block-editor/src/hooks/font-size.js b/packages/block-editor/src/hooks/font-size.js index b30fcc82d99463..89491da44edce3 100644 --- a/packages/block-editor/src/hooks/font-size.js +++ b/packages/block-editor/src/hooks/font-size.js @@ -211,6 +211,7 @@ function useBlockProps( { name, fontSize, style } ) { export default { useBlockProps, + addSaveProps, attributeKeys: [ 'fontSize', 'style' ], hasSupport( name ) { return hasBlockSupport( name, FONT_SIZE_SUPPORT_KEY ); @@ -245,12 +246,6 @@ addFilter( addAttributes ); -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/font/addSaveProps', - addSaveProps -); - addFilter( 'blocks.switchToBlockType.transformedBlock', 'core/font-size/addTransforms', diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index ec0dba5efb2b69..26d1d1ad12bc0b 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -1,12 +1,16 @@ /** * Internal dependencies */ -import { createBlockEditFilter, createBlockListBlockFilter } from './utils'; +import { + createBlockEditFilter, + createBlockListBlockFilter, + createBlockSaveFilter, +} from './utils'; import './compat'; import align from './align'; import './lock'; import anchor from './anchor'; -import './aria-label'; +import ariaLabel from './aria-label'; import customClassName from './custom-class-name'; import './generated-class-name'; import style from './style'; @@ -50,6 +54,17 @@ createBlockListBlockFilter( [ position, childLayout, ] ); +createBlockSaveFilter( [ + align, + anchor, + ariaLabel, + customClassName, + border, + color, + style, + fontFamily, + fontSize, +] ); export { useCustomSides } from './dimensions'; export { useLayoutClasses, useLayoutStyles } from './layout'; diff --git a/packages/block-editor/src/hooks/index.native.js b/packages/block-editor/src/hooks/index.native.js index c0530aedb37ca4..55ae7e19df7037 100644 --- a/packages/block-editor/src/hooks/index.native.js +++ b/packages/block-editor/src/hooks/index.native.js @@ -1,11 +1,15 @@ /** * Internal dependencies */ -import { createBlockEditFilter, createBlockListBlockFilter } from './utils'; +import { + createBlockEditFilter, + createBlockListBlockFilter, + createBlockSaveFilter, +} from './utils'; import './compat'; import align from './align'; import anchor from './anchor'; -import './custom-class-name'; +import customClassName from './custom-class-name'; import './generated-class-name'; import style from './style'; import color from './color'; @@ -14,6 +18,14 @@ import './layout'; createBlockEditFilter( [ align, anchor, style ] ); createBlockListBlockFilter( [ align, style, color, fontSize ] ); +createBlockSaveFilter( [ + align, + anchor, + customClassName, + color, + style, + fontSize, +] ); export { getBorderClassesAndStyles, useBorderProps } from './use-border-props'; export { getColorClassesAndStyles, useColorProps } from './use-color-props'; diff --git a/packages/block-editor/src/hooks/style.js b/packages/block-editor/src/hooks/style.js index b6098969bebb5e..7221de63456cd5 100644 --- a/packages/block-editor/src/hooks/style.js +++ b/packages/block-editor/src/hooks/style.js @@ -343,6 +343,7 @@ function BlockStyleControls( { export default { edit: BlockStyleControls, hasSupport: hasStyleSupport, + addSaveProps, attributeKeys: [ 'style' ], useBlockProps, }; @@ -455,9 +456,3 @@ addFilter( 'core/style/addAttribute', addAttribute ); - -addFilter( - 'blocks.getSaveContent.extraProps', - 'core/style/addSaveProps', - addSaveProps -); diff --git a/packages/block-editor/src/hooks/test/anchor.js b/packages/block-editor/src/hooks/test/anchor.js index a919fad575312e..557789b1c088f3 100644 --- a/packages/block-editor/src/hooks/test/anchor.js +++ b/packages/block-editor/src/hooks/test/anchor.js @@ -6,7 +6,7 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import '../anchor'; +import anchor from '../anchor'; const noop = () => {}; @@ -62,14 +62,9 @@ describe( 'anchor', () => { } ); describe( 'addSaveProps', () => { - const getSaveContentExtraProps = applyFilters.bind( - null, - 'blocks.getSaveContent.extraProps' - ); - it( 'should do nothing if the block settings do not define anchor support', () => { const attributes = { anchor: 'foo' }; - const extraProps = getSaveContentExtraProps( + const extraProps = anchor.addSaveProps( {}, blockSettings, attributes @@ -80,7 +75,7 @@ describe( 'anchor', () => { it( 'should inject anchor attribute ID', () => { const attributes = { anchor: 'foo' }; - const extraProps = getSaveContentExtraProps( + const extraProps = anchor.addSaveProps( {}, { ...blockSettings, @@ -96,7 +91,7 @@ describe( 'anchor', () => { it( 'should remove an anchor attribute ID when field is cleared', () => { const attributes = { anchor: '' }; - const extraProps = getSaveContentExtraProps( + const extraProps = anchor.addSaveProps( {}, { ...blockSettings, diff --git a/packages/block-editor/src/hooks/test/custom-class-name.js b/packages/block-editor/src/hooks/test/custom-class-name.js index 5a662a99d59aec..29d5d836bce8f7 100644 --- a/packages/block-editor/src/hooks/test/custom-class-name.js +++ b/packages/block-editor/src/hooks/test/custom-class-name.js @@ -6,7 +6,7 @@ import { applyFilters } from '@wordpress/hooks'; /** * Internal dependencies */ -import '../custom-class-name'; +import customClassName from '../custom-class-name'; describe( 'custom className', () => { const blockSettings = { @@ -40,14 +40,9 @@ describe( 'custom className', () => { } ); describe( 'addSaveProps', () => { - const addSaveProps = applyFilters.bind( - null, - 'blocks.getSaveContent.extraProps' - ); - it( 'should do nothing if the block settings do not define custom className support', () => { const attributes = { className: 'foo' }; - const extraProps = addSaveProps( + const extraProps = customClassName.addSaveProps( {}, { ...blockSettings, @@ -63,7 +58,7 @@ describe( 'custom className', () => { it( 'should inject the custom className', () => { const attributes = { className: 'bar' }; - const extraProps = addSaveProps( + const extraProps = customClassName.addSaveProps( { className: 'foo' }, blockSettings, attributes diff --git a/packages/block-editor/src/hooks/test/style.js b/packages/block-editor/src/hooks/test/style.js index 544361a47f1156..2cfe299b8c8d91 100644 --- a/packages/block-editor/src/hooks/test/style.js +++ b/packages/block-editor/src/hooks/test/style.js @@ -1,12 +1,7 @@ -/** - * WordPress dependencies - */ -import { applyFilters } from '@wordpress/hooks'; - /** * Internal dependencies */ -import { getInlineStyles, omitStyle } from '../style'; +import _style, { getInlineStyles, omitStyle } from '../style'; describe( 'getInlineStyles', () => { it( 'should return an empty object when called with undefined', () => { @@ -120,11 +115,6 @@ describe( 'getInlineStyles', () => { } ); describe( 'addSaveProps', () => { - const addSaveProps = applyFilters.bind( - null, - 'blocks.getSaveContent.extraProps' - ); - const blockSettings = { save: () => <div className="default" />, category: 'text', @@ -166,7 +156,7 @@ describe( 'addSaveProps', () => { }; it( 'should serialize all styles by default', () => { - const extraProps = addSaveProps( {}, blockSettings, attributes ); + const extraProps = _style.addSaveProps( {}, blockSettings, attributes ); expect( extraProps.style ).toEqual( { background: @@ -183,7 +173,7 @@ describe( 'addSaveProps', () => { const settings = applySkipSerialization( { typography: true, } ); - const extraProps = addSaveProps( {}, settings, attributes ); + const extraProps = _style.addSaveProps( {}, settings, attributes ); expect( extraProps.style ).toEqual( { background: @@ -198,7 +188,7 @@ describe( 'addSaveProps', () => { color: [ 'gradient' ], typography: [ 'textDecoration', 'textTransform' ], } ); - const extraProps = addSaveProps( {}, settings, attributes ); + const extraProps = _style.addSaveProps( {}, settings, attributes ); expect( extraProps.style ).toEqual( { color: '#d92828', diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index 49617013dc1153..cd342af00d1a55 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -488,10 +488,10 @@ export function createBlockListBlockFilter( features ) { } if ( - ! hasSupport( props.name ) || // Skip rendering if none of the needed attributes are // set. - ! Object.keys( neededProps ).length + ! Object.keys( neededProps ).length || + ! hasSupport( props.name ) ) { return null; } @@ -543,3 +543,49 @@ export function createBlockListBlockFilter( features ) { withBlockListBlockHooks ); } + +export function createBlockSaveFilter( features ) { + function extraPropsFromHooks( props, name, attributes ) { + return features.reduce( ( accu, feature ) => { + const { hasSupport, attributeKeys = [], addSaveProps } = feature; + + const neededAttributes = {}; + for ( const key of attributeKeys ) { + if ( attributes[ key ] ) { + neededAttributes[ key ] = attributes[ key ]; + } + } + + if ( + // Skip rendering if none of the needed attributes are + // set. + ! Object.keys( neededAttributes ).length || + ! hasSupport( name ) + ) { + return accu; + } + + return addSaveProps( accu, name, neededAttributes ); + }, props ); + } + addFilter( + 'blocks.getSaveContent.extraProps', + 'core/editor/hooks', + extraPropsFromHooks, + 0 + ); + addFilter( + 'blocks.getSaveContent.extraProps', + 'core/editor/hooks', + ( props ) => { + // Previously we had a filter deleting the className if it was an empty + // string. That filter is no longer running, so now we need to delete it + // here. + if ( props.hasOwnProperty( 'className' ) && ! props.className ) { + delete props.className; + } + + return props; + } + ); +} From 9a2ba5693699c96b67881c3df3d5f8770752acc5 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 14 Dec 2023 17:39:40 +0100 Subject: [PATCH 195/325] InnerBlocks: combine store subscriptions (#57032) --- .../src/components/inner-blocks/index.js | 90 +++++++++++++------ .../components/inner-blocks/index.native.js | 26 ++++-- .../use-inner-block-template-sync.js | 12 ++- .../use-nested-settings-update.js | 19 ++-- .../components/use-block-drop-zone/index.js | 30 +------ .../src/components/use-on-block-drop/index.js | 13 ++- .../use-on-block-drop/test/index.js | 16 ++-- 7 files changed, 113 insertions(+), 93 deletions(-) diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index d637a16f363602..cdabb348cca05e 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -33,6 +33,15 @@ import { useSettings } from '../use-settings'; const EMPTY_OBJECT = {}; +function BlockContext( { children, clientId } ) { + const context = useBlockContext( clientId ); + return ( + <BlockContextProvider value={ context }> + { children } + </BlockContextProvider> + ); +} + /** * InnerBlocks is a component which allows a single block to have multiple blocks * as children. The UncontrolledInnerBlocks component is used whenever the inner @@ -60,10 +69,15 @@ function UncontrolledInnerBlocks( props ) { orientation, placeholder, layout, + name, + blockType, + innerBlocks, + parentLock, } = props; useNestedSettingsUpdate( clientId, + parentLock, allowedBlocks, prioritizedInserterBlocks, defaultBlock, @@ -78,19 +92,12 @@ function UncontrolledInnerBlocks( props ) { useInnerBlockTemplateSync( clientId, + innerBlocks, template, templateLock, templateInsertUpdatesSelection ); - const context = useBlockContext( clientId ); - const name = useSelect( - ( select ) => { - return select( blockEditorStore ).getBlock( clientId )?.name; - }, - [ clientId ] - ); - const defaultLayoutBlockSupport = getBlockSupport( name, 'layout' ) || getBlockSupport( name, '__experimentalLayout' ) || @@ -114,20 +121,22 @@ function UncontrolledInnerBlocks( props ) { [ defaultLayout, usedLayout, allowSizingOnChildren ] ); - // This component needs to always be synchronous as it's the one changing - // the async mode depending on the block selection. - return ( - <BlockContextProvider value={ context }> - <BlockListItems - rootClientId={ clientId } - renderAppender={ renderAppender } - __experimentalAppenderTagName={ __experimentalAppenderTagName } - layout={ memoedLayout } - wrapperRef={ wrapperRef } - placeholder={ placeholder } - /> - </BlockContextProvider> + const items = ( + <BlockListItems + rootClientId={ clientId } + renderAppender={ renderAppender } + __experimentalAppenderTagName={ __experimentalAppenderTagName } + layout={ memoedLayout } + wrapperRef={ wrapperRef } + placeholder={ placeholder } + /> ); + + if ( Object.keys( blockType.providesContext ).length === 0 ) { + return items; + } + + return <BlockContext clientId={ clientId }>{ items }</BlockContext>; } /** @@ -180,7 +189,16 @@ export function useInnerBlocksProps( props = {}, options = {} ) { __unstableLayoutClassNames: layoutClassNames = '', } = useBlockEditContext(); const isSmallScreen = useViewportMatch( 'medium', '<' ); - const { __experimentalCaptureToolbars, hasOverlay } = useSelect( + const { + __experimentalCaptureToolbars, + hasOverlay, + name, + blockType, + innerBlocks, + parentLock, + parentClientId, + isDropZoneDisabled, + } = useSelect( ( select ) => { if ( ! clientId ) { return {}; @@ -191,14 +209,21 @@ export function useInnerBlocksProps( props = {}, options = {} ) { isBlockSelected, hasSelectedInnerBlock, __unstableGetEditorMode, + getBlocks, + getTemplateLock, + getBlockRootClientId, + __unstableIsWithinBlockOverlay, + __unstableHasActiveBlockOverlayActive, + getBlockEditingMode, } = select( blockEditorStore ); + const { hasBlockSupport, getBlockType } = select( blocksStore ); const blockName = getBlockName( clientId ); const enableClickThrough = __unstableGetEditorMode() === 'navigation' || isSmallScreen; + const blockEditingMode = getBlockEditingMode( clientId ); + const _parentClientId = getBlockRootClientId( clientId ); return { - __experimentalCaptureToolbars: select( - blocksStore - ).hasBlockSupport( + __experimentalCaptureToolbars: hasBlockSupport( blockName, '__experimentalExposeControlsToChildren', false @@ -208,6 +233,15 @@ export function useInnerBlocksProps( props = {}, options = {} ) { ! isBlockSelected( clientId ) && ! hasSelectedInnerBlock( clientId, true ) && enableClickThrough, + name: blockName, + blockType: getBlockType( blockName ), + innerBlocks: getBlocks( clientId ), + parentLock: getTemplateLock( _parentClientId ), + parentClientId: _parentClientId, + isDropZoneDisabled: + blockEditingMode !== 'default' || + __unstableHasActiveBlockOverlayActive( clientId ) || + __unstableIsWithinBlockOverlay( clientId ), }; }, [ clientId, isSmallScreen ] @@ -216,6 +250,8 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const blockDropZoneRef = useBlockDropZone( { dropZoneElement, rootClientId: clientId, + parentClientId, + isDisabled: isDropZoneDisabled, } ); const ref = useMergeRefs( [ @@ -226,6 +262,10 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const innerBlocksProps = { __experimentalCaptureToolbars, layout, + name, + blockType, + innerBlocks, + parentLock, ...options, }; const InnerBlocks = 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 e254eff6c9ef18..2f04854bce2fca 100644 --- a/packages/block-editor/src/components/inner-blocks/index.native.js +++ b/packages/block-editor/src/components/inner-blocks/index.native.js @@ -105,8 +105,26 @@ function UncontrolledInnerBlocks( props ) { const context = useBlockContext( clientId ); + const { nestingLevel, innerBlocks, parentLock } = useSelect( + ( select ) => { + const { + getBlockParents, + getBlocks, + getTemplateLock, + getBlockRootClientId, + } = select( blockEditorStore ); + return { + nestingLevel: getBlockParents( clientId )?.length, + innerBlocks: getBlocks( clientId ), + parentLock: getTemplateLock( getBlockRootClientId( clientId ) ), + }; + }, + [ clientId ] + ); + useNestedSettingsUpdate( clientId, + parentLock, allowedBlocks, prioritizedInserterBlocks, defaultBlock, @@ -121,18 +139,12 @@ function UncontrolledInnerBlocks( props ) { useInnerBlockTemplateSync( clientId, + innerBlocks, template, templateLock, templateInsertUpdatesSelection ); - const nestingLevel = useSelect( - ( select ) => { - return select( blockEditorStore ).getBlockParents( clientId ) - ?.length; - }, - [ clientId ] - ); if ( nestingLevel >= MAX_NESTING_DEPTH ) { return <WarningMaxDepthExceeded clientId={ clientId } />; } diff --git a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js index 664e696eb44b9c..614ddad921ca4a 100644 --- a/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js +++ b/packages/block-editor/src/components/inner-blocks/use-inner-block-template-sync.js @@ -23,6 +23,7 @@ import { store as blockEditorStore } from '../../store'; * then we replace the inner blocks with the correct value after synchronizing it with the template. * * @param {string} clientId The block client ID. + * @param {Array} innerBlocks * @param {Object} template The template to match. * @param {string} templateLock The template lock state for the inner blocks. For * example, if the template lock is set to "all", @@ -36,10 +37,14 @@ import { store as blockEditorStore } from '../../store'; */ export default function useInnerBlockTemplateSync( clientId, + innerBlocks, template, templateLock, templateInsertUpdatesSelection ) { + // Instead of adding a useSelect mapping here, please add to the useSelect + // mapping in InnerBlocks! Every subscription impacts performance. + const { getBlocks, getSelectedBlocksInitialCaretPosition, @@ -48,13 +53,6 @@ export default function useInnerBlockTemplateSync( const { replaceInnerBlocks, __unstableMarkNextChangeAsNotPersistent } = useDispatch( blockEditorStore ); - const { innerBlocks } = useSelect( - ( select ) => ( { - innerBlocks: select( blockEditorStore ).getBlocks( clientId ), - } ), - [ clientId ] - ); - // Maintain a reference to the previous value so we can do a deep equality check. const existingTemplate = useRef( null ); diff --git a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js index 8eba8a1d2223d4..80acf0e639f9e5 100644 --- a/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js +++ b/packages/block-editor/src/components/inner-blocks/use-nested-settings-update.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useLayoutEffect, useMemo, useState } from '@wordpress/element'; -import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; +import { useDispatch, useRegistry } from '@wordpress/data'; import deprecated from '@wordpress/deprecated'; import isShallowEqual from '@wordpress/is-shallow-equal'; @@ -32,6 +32,7 @@ function useShallowMemo( value ) { * came from props. * * @param {string} clientId The client ID of the block to update. + * @param {string} parentLock * @param {string[]} allowedBlocks An array of block names which are permitted * in inner blocks. * @param {string[]} prioritizedInserterBlocks Block names and/or block variations to be prioritized in the inserter, in the format {blockName}/{variationName}. @@ -53,6 +54,7 @@ function useShallowMemo( value ) { */ export default function useNestedSettingsUpdate( clientId, + parentLock, allowedBlocks, prioritizedInserterBlocks, defaultBlock, @@ -64,21 +66,12 @@ export default function useNestedSettingsUpdate( orientation, layout ) { + // Instead of adding a useSelect mapping here, please add to the useSelect + // mapping in InnerBlocks! Every subscription impacts performance. + const { updateBlockListSettings } = useDispatch( blockEditorStore ); const registry = useRegistry(); - const { parentLock } = useSelect( - ( select ) => { - const rootClientId = - select( blockEditorStore ).getBlockRootClientId( clientId ); - return { - parentLock: - select( blockEditorStore ).getTemplateLock( rootClientId ), - }; - }, - [ clientId ] - ); - // Implementors often pass a new array on every render, // and the contents of the arrays are just strings, so the entire array // can be passed as dependencies but We need to include the length of the array, diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index cb3c3ae6a28a3d..4f8a08db0c610f 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -209,6 +209,8 @@ export default function useBlockDropZone( { // values returned by the `getRootBlockClientId` selector, which also uses // an empty string to represent top-level blocks. rootClientId: targetRootClientId = '', + parentClientId: parentBlockClientId = '', + isDisabled = false, } = {} ) { const registry = useRegistry(); const [ dropTarget, setDropTarget ] = useState( { @@ -216,31 +218,6 @@ export default function useBlockDropZone( { operation: 'insert', } ); - const { isDisabled, parentBlockClientId, rootBlockIndex } = useSelect( - ( select ) => { - const { - __unstableIsWithinBlockOverlay, - __unstableHasActiveBlockOverlayActive, - getBlockIndex, - getBlockParents, - getBlockEditingMode, - } = select( blockEditorStore ); - const blockEditingMode = getBlockEditingMode( targetRootClientId ); - return { - parentBlockClientId: - getBlockParents( targetRootClientId, true )[ 0 ] || '', - rootBlockIndex: getBlockIndex( targetRootClientId ), - isDisabled: - blockEditingMode !== 'default' || - __unstableHasActiveBlockOverlayActive( - targetRootClientId - ) || - __unstableIsWithinBlockOverlay( targetRootClientId ), - }; - }, - [ targetRootClientId ] - ); - const { getBlockListSettings, getBlocks, getBlockIndex } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = @@ -299,7 +276,7 @@ export default function useBlockDropZone( { ? getBlockListSettings( parentBlockClientId ) ?.orientation : undefined, - rootBlockIndex, + rootBlockIndex: getBlockIndex( targetRootClientId ), } ); @@ -330,7 +307,6 @@ export default function useBlockDropZone( { showInsertionPoint, getBlockIndex, parentBlockClientId, - rootBlockIndex, ] ), 200 diff --git a/packages/block-editor/src/components/use-on-block-drop/index.js b/packages/block-editor/src/components/use-on-block-drop/index.js index ab0da8ad99e2ab..c49af2f80fca22 100644 --- a/packages/block-editor/src/components/use-on-block-drop/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/index.js @@ -134,7 +134,7 @@ export function onBlockDrop( * * @param {string} targetRootClientId The root client id where the block(s) will be inserted. * @param {number} targetBlockIndex The index where the block(s) will be inserted. - * @param {boolean} hasUploadPermissions Whether the user has upload permissions. + * @param {Function} getSettings A function that gets the block editor settings. * @param {Function} updateBlockAttributes A function that updates a block's attributes. * @param {Function} canInsertBlockType A function that returns checks whether a block type can be inserted. * @param {Function} insertOrReplaceBlocks A function that inserts or replaces blocks. @@ -144,13 +144,13 @@ export function onBlockDrop( export function onFilesDrop( targetRootClientId, targetBlockIndex, - hasUploadPermissions, + getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks ) { return ( files ) => { - if ( ! hasUploadPermissions ) { + if ( ! getSettings().mediaUpload ) { return; } @@ -211,16 +211,13 @@ export default function useOnBlockDrop( options = {} ) { const { operation = 'insert' } = options; - const hasUploadPermissions = useSelect( - ( select ) => select( blockEditorStore ).getSettings().mediaUpload, - [] - ); const { canInsertBlockType, getBlockIndex, getClientIdsOfDescendants, getBlockOrder, getBlocksByClientId, + getSettings, } = useSelect( blockEditorStore ); const { insertBlocks, @@ -313,7 +310,7 @@ export default function useOnBlockDrop( const _onFilesDrop = onFilesDrop( targetRootClientId, targetBlockIndex, - hasUploadPermissions, + getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks diff --git a/packages/block-editor/src/components/use-on-block-drop/test/index.js b/packages/block-editor/src/components/use-on-block-drop/test/index.js index 1b95cc0085a79e..03c873bf6a8fd7 100644 --- a/packages/block-editor/src/components/use-on-block-drop/test/index.js +++ b/packages/block-editor/src/components/use-on-block-drop/test/index.js @@ -307,12 +307,12 @@ describe( 'onFilesDrop', () => { const insertOrReplaceBlocks = jest.fn(); const targetRootClientId = '1'; const targetBlockIndex = 0; - const uploadPermissions = false; + const getSettings = jest.fn( () => ( {} ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, targetBlockIndex, - uploadPermissions, + getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks @@ -332,12 +332,14 @@ describe( 'onFilesDrop', () => { const canInsertBlockType = noop; const targetRootClientId = '1'; const targetBlockIndex = 0; - const uploadPermissions = true; + const getSettings = jest.fn( () => ( { + mediaUpload: true, + } ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, targetBlockIndex, - uploadPermissions, + getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks @@ -360,12 +362,14 @@ describe( 'onFilesDrop', () => { const insertOrReplaceBlocks = jest.fn(); const targetRootClientId = '1'; const targetBlockIndex = 0; - const uploadPermissions = true; + const getSettings = jest.fn( () => ( { + mediaUpload: true, + } ) ); const onFileDropHandler = onFilesDrop( targetRootClientId, targetBlockIndex, - uploadPermissions, + getSettings, updateBlockAttributes, canInsertBlockType, insertOrReplaceBlocks From aecb60755443f5e8363c32ba1bb77adc27218bef Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Thu, 14 Dec 2023 11:44:51 -0500 Subject: [PATCH 196/325] Tabs: Make sure individual `Tab`s are linked to the correct `TabPanel`s (#57033) * fix bug linking tabs to the correct tabpanels * add regression test * changelog * simplify tab content assertions --- packages/components/CHANGELOG.md | 4 +- packages/components/src/tabs/tabpanel.tsx | 10 ++-- packages/components/src/tabs/test/index.tsx | 56 +++++++++++++++++++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b1c41b9a05f53f..025d342073aac8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -8,7 +8,8 @@ ### Experimental -- `TabPanel`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). +- `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). +- `Tabs`: make sure `Tab`s are associated to the right `TabPanel`s, regardless of the order they're rendered in ([#57033](https://github.com/WordPress/gutenberg/pull/57033)). ### Bug Fix @@ -28,6 +29,7 @@ - `FontSizePicker`: Add opt-in prop for 40px default size ([#56804](https://github.com/WordPress/gutenberg/pull/56804)). ### Bug Fix + - `PaletteEdit`: temporary custom gradient not saving ([#56896](https://github.com/WordPress/gutenberg/pull/56896)). - `ToggleGroupControl`: react correctly to external controlled updates ([#56678](https://github.com/WordPress/gutenberg/pull/56678)). - `ToolsPanel`: fix a performance issue ([#56770](https://github.com/WordPress/gutenberg/pull/56770)). diff --git a/packages/components/src/tabs/tabpanel.tsx b/packages/components/src/tabs/tabpanel.tsx index 6762c123cce817..439671a39ff9b7 100644 --- a/packages/components/src/tabs/tabpanel.tsx +++ b/packages/components/src/tabs/tabpanel.tsx @@ -1,7 +1,3 @@ -/** - * External dependencies - */ - /** * WordPress dependencies */ @@ -38,7 +34,11 @@ export const TabPanel = forwardRef< <StyledTabPanel ref={ ref } store={ store } - id={ instancedTabId } + // For TabPanel, the id passed here is the id attribute of the DOM + // element. + // `tabId` is the id of the tab that controls this panel. + id={ `${ instancedTabId }-view` } + tabId={ instancedTabId } focusable={ focusable } { ...otherProps } > diff --git a/packages/components/src/tabs/test/index.tsx b/packages/components/src/tabs/test/index.tsx index 70ad3c1c18ae54..24b7e75b6e72b7 100644 --- a/packages/components/src/tabs/test/index.tsx +++ b/packages/components/src/tabs/test/index.tsx @@ -1278,4 +1278,60 @@ describe( 'Tabs', () => { } ); } ); } ); + it( 'should associate each `Tab` with the correct `TabPanel`, even if they are not rendered in the same order', async () => { + const TABS_WITH_DELTA_REVERSED = [ ...TABS_WITH_DELTA ].reverse(); + + render( + <Tabs> + <Tabs.TabList> + { TABS_WITH_DELTA.map( ( tabObj ) => ( + <Tabs.Tab + key={ tabObj.tabId } + tabId={ tabObj.tabId } + className={ tabObj.tab.className } + disabled={ tabObj.tab.disabled } + > + { tabObj.title } + </Tabs.Tab> + ) ) } + </Tabs.TabList> + { TABS_WITH_DELTA_REVERSED.map( ( tabObj ) => ( + <Tabs.TabPanel + key={ tabObj.tabId } + tabId={ tabObj.tabId } + focusable={ tabObj.tabpanel?.focusable } + > + { tabObj.content } + </Tabs.TabPanel> + ) ) } + </Tabs> + ); + + // Alpha is the initially selected tab,and should render the correct tabpanel + expect( await getSelectedTab() ).toHaveTextContent( 'Alpha' ); + expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( + 'Selected tab: Alpha' + ); + + // Select Beta, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Beta' } ) ); + expect( await getSelectedTab() ).toHaveTextContent( 'Beta' ); + expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( + 'Selected tab: Beta' + ); + + // Select Gamma, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Gamma' } ) ); + expect( await getSelectedTab() ).toHaveTextContent( 'Gamma' ); + expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( + 'Selected tab: Gamma' + ); + + // Select Delta, make sure the correct tabpanel is rendered + await click( screen.getByRole( 'tab', { name: 'Delta' } ) ); + expect( await getSelectedTab() ).toHaveTextContent( 'Delta' ); + expect( screen.getByRole( 'tabpanel' ) ).toHaveTextContent( + 'Selected tab: Delta' + ); + } ); } ); From ea2ee6011ec6739c39454a5cebc14cf48b794d98 Mon Sep 17 00:00:00 2001 From: Marcelo Serpa <81248+fullofcaffeine@users.noreply.github.com> Date: Thu, 14 Dec 2023 12:21:06 -0600 Subject: [PATCH 197/325] (edit-site)(use-init-edited-entity-from-url) Safely access `toString()` on `siteData`'s `page_on_front` (#57035) --- .../sync-state-with-url/use-init-edited-entity-from-url.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js index 46079cbce8efd5..7b1321fdf4b8ac 100644 --- a/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js +++ b/packages/edit-site/src/components/sync-state-with-url/use-init-edited-entity-from-url.js @@ -54,7 +54,10 @@ function useResolveEditedEntityAndContext( { postId, postType } ) { return { hasLoadedAllDependencies: !! base && !! siteData, homepageId: - siteData?.show_on_front === 'page' + siteData?.show_on_front === 'page' && + [ 'number', 'string' ].includes( + typeof siteData.page_on_front + ) ? siteData.page_on_front.toString() : null, url: base?.home, From 30037b55f07c7df725777c6efdb0dca03a4fb6da Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 14 Dec 2023 20:34:52 +0100 Subject: [PATCH 198/325] List: avoid useSelect in block render (#57077) --- packages/block-library/src/list-item/edit.js | 20 ++- .../src/list-item/hooks/use-enter.js | 125 +++++++++--------- .../src/list-item/hooks/use-enter.native.js | 14 +- .../list-item/hooks/use-indent-list-item.js | 96 ++++++-------- .../src/list-item/hooks/use-merge.js | 2 +- .../list-item/hooks/use-outdent-list-item.js | 119 +++++++---------- .../src/list-item/hooks/use-space.js | 11 +- packages/block-library/src/list/edit.js | 62 ++++----- 8 files changed, 218 insertions(+), 231 deletions(-) diff --git a/packages/block-library/src/list-item/edit.js b/packages/block-library/src/list-item/edit.js index 3f26840ad345f9..7733a762807528 100644 --- a/packages/block-library/src/list-item/edit.js +++ b/packages/block-library/src/list-item/edit.js @@ -6,6 +6,7 @@ import { useBlockProps, useInnerBlocksProps, BlockControls, + store as blockEditorStore, } from '@wordpress/block-editor'; import { isRTL, __ } from '@wordpress/i18n'; import { ToolbarButton } from '@wordpress/components'; @@ -16,6 +17,7 @@ import { formatIndent, } from '@wordpress/icons'; import { useMergeRefs } from '@wordpress/compose'; +import { useSelect } from '@wordpress/data'; /** * Internal dependencies @@ -32,8 +34,22 @@ import { import { convertToListItems } from './utils'; export function IndentUI( { clientId } ) { - const [ canIndent, indentListItem ] = useIndentListItem( clientId ); - const [ canOutdent, outdentListItem ] = useOutdentListItem( clientId ); + const indentListItem = useIndentListItem( clientId ); + const outdentListItem = useOutdentListItem(); + const { canIndent, canOutdent } = useSelect( + ( select ) => { + const { getBlockIndex, getBlockRootClientId, getBlockName } = + select( blockEditorStore ); + return { + canIndent: getBlockIndex( clientId ) > 0, + canOutdent: + getBlockName( + getBlockRootClientId( getBlockRootClientId( clientId ) ) + ) === 'core/list-item', + }; + }, + [ clientId ] + ); return ( <> diff --git a/packages/block-library/src/list-item/hooks/use-enter.js b/packages/block-library/src/list-item/hooks/use-enter.js index 46fdc65cecdd7b..ffe5c55fbbed2e 100644 --- a/packages/block-library/src/list-item/hooks/use-enter.js +++ b/packages/block-library/src/list-item/hooks/use-enter.js @@ -19,71 +19,72 @@ import useOutdentListItem from './use-outdent-list-item'; export default function useEnter( props ) { const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore ); - const { getBlock, getBlockRootClientId, getBlockIndex } = + const { getBlock, getBlockRootClientId, getBlockIndex, getBlockName } = useSelect( blockEditorStore ); const propsRef = useRef( props ); propsRef.current = props; - const [ canOutdent, outdentListItem ] = useOutdentListItem( - propsRef.current.clientId - ); - return useRefEffect( - ( element ) => { - function onKeyDown( event ) { - if ( event.defaultPrevented || event.keyCode !== ENTER ) { - return; - } - const { content, clientId } = propsRef.current; - if ( content.length ) { - return; - } - event.preventDefault(); - if ( canOutdent ) { - outdentListItem(); - return; - } - // Here we are in top level list so we need to split. - const topParentListBlock = getBlock( - getBlockRootClientId( clientId ) - ); - const blockIndex = getBlockIndex( clientId ); - const head = cloneBlock( { - ...topParentListBlock, - innerBlocks: topParentListBlock.innerBlocks.slice( - 0, - blockIndex - ), - } ); - const middle = createBlock( getDefaultBlockName() ); - // Last list item might contain a `list` block innerBlock - // In that case append remaining innerBlocks blocks. - const after = [ - ...( topParentListBlock.innerBlocks[ blockIndex ] - .innerBlocks[ 0 ]?.innerBlocks || [] ), - ...topParentListBlock.innerBlocks.slice( blockIndex + 1 ), - ]; - const tail = after.length - ? [ - cloneBlock( { - ...topParentListBlock, - innerBlocks: after, - } ), - ] - : []; - replaceBlocks( - topParentListBlock.clientId, - [ head, middle, ...tail ], - 1 - ); - // We manually change the selection here because we are replacing - // a different block than the selected one. - selectionChange( middle.clientId ); + const outdentListItem = useOutdentListItem(); + return useRefEffect( ( element ) => { + function onKeyDown( event ) { + if ( event.defaultPrevented || event.keyCode !== ENTER ) { + return; } + const { content, clientId } = propsRef.current; + if ( content.length ) { + return; + } + event.preventDefault(); + const canOutdent = + getBlockName( + getBlockRootClientId( + getBlockRootClientId( propsRef.current.clientId ) + ) + ) === 'core/list-item'; + if ( canOutdent ) { + outdentListItem(); + return; + } + // Here we are in top level list so we need to split. + const topParentListBlock = getBlock( + getBlockRootClientId( clientId ) + ); + const blockIndex = getBlockIndex( clientId ); + const head = cloneBlock( { + ...topParentListBlock, + innerBlocks: topParentListBlock.innerBlocks.slice( + 0, + blockIndex + ), + } ); + const middle = createBlock( getDefaultBlockName() ); + // Last list item might contain a `list` block innerBlock + // In that case append remaining innerBlocks blocks. + const after = [ + ...( topParentListBlock.innerBlocks[ blockIndex ] + .innerBlocks[ 0 ]?.innerBlocks || [] ), + ...topParentListBlock.innerBlocks.slice( blockIndex + 1 ), + ]; + const tail = after.length + ? [ + cloneBlock( { + ...topParentListBlock, + innerBlocks: after, + } ), + ] + : []; + replaceBlocks( + topParentListBlock.clientId, + [ head, middle, ...tail ], + 1 + ); + // We manually change the selection here because we are replacing + // a different block than the selected one. + selectionChange( middle.clientId ); + } - element.addEventListener( 'keydown', onKeyDown ); - return () => { - element.removeEventListener( 'keydown', onKeyDown ); - }; - }, - [ canOutdent ] - ); + element.addEventListener( 'keydown', onKeyDown ); + return () => { + element.removeEventListener( 'keydown', onKeyDown ); + }; + }, [] ); } diff --git a/packages/block-library/src/list-item/hooks/use-enter.native.js b/packages/block-library/src/list-item/hooks/use-enter.native.js index d3be5f1ea0e1bb..596dd469d31326 100644 --- a/packages/block-library/src/list-item/hooks/use-enter.native.js +++ b/packages/block-library/src/list-item/hooks/use-enter.native.js @@ -17,13 +17,11 @@ import useOutdentListItem from './use-outdent-list-item'; export default function useEnter( props, preventDefault ) { const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore ); - const { getBlock, getBlockRootClientId, getBlockIndex } = + const { getBlock, getBlockRootClientId, getBlockIndex, getBlockName } = useSelect( blockEditorStore ); const propsRef = useRef( props ); propsRef.current = props; - const [ canOutdent, outdentListItem ] = useOutdentListItem( - propsRef.current.clientId - ); + const outdentListItem = useOutdentListItem(); return { onEnter() { @@ -32,7 +30,13 @@ export default function useEnter( props, preventDefault ) { return; } preventDefault.current = true; - if ( canOutdent ) { + if ( + getBlockName( + getBlockRootClientId( + getBlockRootClientId( propsRef.current.clientId ) + ) + ) === 'core/list-item' + ) { outdentListItem(); return; } diff --git a/packages/block-library/src/list-item/hooks/use-indent-list-item.js b/packages/block-library/src/list-item/hooks/use-indent-list-item.js index b1fd8394be43ce..6eb5d9d73ba658 100644 --- a/packages/block-library/src/list-item/hooks/use-indent-list-item.js +++ b/packages/block-library/src/list-item/hooks/use-indent-list-item.js @@ -7,10 +7,6 @@ import { store as blockEditorStore } from '@wordpress/block-editor'; import { createBlock, cloneBlock } from '@wordpress/blocks'; export default function useIndentListItem( clientId ) { - const canIndent = useSelect( - ( select ) => select( blockEditorStore ).getBlockIndex( clientId ) > 0, - [ clientId ] - ); const { replaceBlocks, selectionChange, multiSelect } = useDispatch( blockEditorStore ); const { @@ -21,55 +17,49 @@ export default function useIndentListItem( clientId ) { hasMultiSelection, getMultiSelectedBlockClientIds, } = useSelect( blockEditorStore ); - return [ - canIndent, - useCallback( () => { - const _hasMultiSelection = hasMultiSelection(); - const clientIds = _hasMultiSelection - ? getMultiSelectedBlockClientIds() - : [ clientId ]; - const clonedBlocks = clientIds.map( ( _clientId ) => - cloneBlock( getBlock( _clientId ) ) - ); - const previousSiblingId = getPreviousBlockClientId( clientId ); - const newListItem = cloneBlock( getBlock( previousSiblingId ) ); - // If the sibling has no innerBlocks, create a new `list` block. - if ( ! newListItem.innerBlocks?.length ) { - newListItem.innerBlocks = [ createBlock( 'core/list' ) ]; - } - // A list item usually has one `list`, but it's possible to have - // more. So we need to preserve the previous `list` blocks and - // merge the new blocks to the last `list`. - newListItem.innerBlocks[ - newListItem.innerBlocks.length - 1 - ].innerBlocks.push( ...clonedBlocks ); + return useCallback( () => { + const _hasMultiSelection = hasMultiSelection(); + const clientIds = _hasMultiSelection + ? getMultiSelectedBlockClientIds() + : [ clientId ]; + const clonedBlocks = clientIds.map( ( _clientId ) => + cloneBlock( getBlock( _clientId ) ) + ); + const previousSiblingId = getPreviousBlockClientId( clientId ); + const newListItem = cloneBlock( getBlock( previousSiblingId ) ); + // If the sibling has no innerBlocks, create a new `list` block. + if ( ! newListItem.innerBlocks?.length ) { + newListItem.innerBlocks = [ createBlock( 'core/list' ) ]; + } + // A list item usually has one `list`, but it's possible to have + // more. So we need to preserve the previous `list` blocks and + // merge the new blocks to the last `list`. + newListItem.innerBlocks[ + newListItem.innerBlocks.length - 1 + ].innerBlocks.push( ...clonedBlocks ); - // We get the selection start/end here, because when - // we replace blocks, the selection is updated too. - const selectionStart = getSelectionStart(); - const selectionEnd = getSelectionEnd(); - // Replace the previous sibling of the block being indented and the indented blocks, - // with a new block whose attributes are equal to the ones of the previous sibling and - // whose descendants are the children of the previous sibling, followed by the indented blocks. - replaceBlocks( - [ previousSiblingId, ...clientIds ], - [ newListItem ] + // We get the selection start/end here, because when + // we replace blocks, the selection is updated too. + const selectionStart = getSelectionStart(); + const selectionEnd = getSelectionEnd(); + // Replace the previous sibling of the block being indented and the indented blocks, + // with a new block whose attributes are equal to the ones of the previous sibling and + // whose descendants are the children of the previous sibling, followed by the indented blocks. + replaceBlocks( [ previousSiblingId, ...clientIds ], [ newListItem ] ); + if ( ! _hasMultiSelection ) { + selectionChange( + clonedBlocks[ 0 ].clientId, + selectionEnd.attributeKey, + selectionEnd.clientId === selectionStart.clientId + ? selectionStart.offset + : selectionEnd.offset, + selectionEnd.offset + ); + } else { + multiSelect( + clonedBlocks[ 0 ].clientId, + clonedBlocks[ clonedBlocks.length - 1 ].clientId ); - if ( ! _hasMultiSelection ) { - selectionChange( - clonedBlocks[ 0 ].clientId, - selectionEnd.attributeKey, - selectionEnd.clientId === selectionStart.clientId - ? selectionStart.offset - : selectionEnd.offset, - selectionEnd.offset - ); - } else { - multiSelect( - clonedBlocks[ 0 ].clientId, - clonedBlocks[ clonedBlocks.length - 1 ].clientId - ); - } - }, [ clientId ] ), - ]; + } + }, [ clientId ] ); } diff --git a/packages/block-library/src/list-item/hooks/use-merge.js b/packages/block-library/src/list-item/hooks/use-merge.js index cda1f0c02d3a88..2fbee4ba275a12 100644 --- a/packages/block-library/src/list-item/hooks/use-merge.js +++ b/packages/block-library/src/list-item/hooks/use-merge.js @@ -20,7 +20,7 @@ export default function useMerge( clientId, onMerge ) { } = useSelect( blockEditorStore ); const { mergeBlocks, moveBlocksToPosition } = useDispatch( blockEditorStore ); - const [ , outdentListItem ] = useOutdentListItem( clientId ); + const outdentListItem = useOutdentListItem(); function getTrailingId( id ) { const order = getBlockOrder( id ); diff --git a/packages/block-library/src/list-item/hooks/use-outdent-list-item.js b/packages/block-library/src/list-item/hooks/use-outdent-list-item.js index 14598dc7451cfa..85c433fbeffada 100644 --- a/packages/block-library/src/list-item/hooks/use-outdent-list-item.js +++ b/packages/block-library/src/list-item/hooks/use-outdent-list-item.js @@ -6,24 +6,8 @@ import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { cloneBlock } from '@wordpress/blocks'; -export default function useOutdentListItem( clientId ) { +export default function useOutdentListItem() { const registry = useRegistry(); - const { canOutdent } = useSelect( - ( innerSelect ) => { - const { getBlockRootClientId, getBlockName } = - innerSelect( blockEditorStore ); - const grandParentId = getBlockRootClientId( - getBlockRootClientId( clientId ) - ); - const grandParentName = getBlockName( grandParentId ); - const isListItem = grandParentName === 'core/list-item'; - - return { - canOutdent: isListItem, - }; - }, - [ clientId ] - ); const { moveBlocksToPosition, removeBlock, @@ -48,69 +32,66 @@ export default function useOutdentListItem( clientId ) { return parentListItemId; } - return [ - canOutdent, - useCallback( ( clientIds = getSelectedBlockClientIds() ) => { - if ( ! Array.isArray( clientIds ) ) { - clientIds = [ clientIds ]; - } - - if ( ! clientIds.length ) return; + return useCallback( ( clientIds = getSelectedBlockClientIds() ) => { + if ( ! Array.isArray( clientIds ) ) { + clientIds = [ clientIds ]; + } - const firstClientId = clientIds[ 0 ]; + if ( ! clientIds.length ) return; - // Can't outdent if it's not a list item. - if ( getBlockName( firstClientId ) !== 'core/list-item' ) return; + const firstClientId = clientIds[ 0 ]; - const parentListItemId = getParentListItemId( firstClientId ); + // Can't outdent if it's not a list item. + if ( getBlockName( firstClientId ) !== 'core/list-item' ) return; - // Can't outdent if it's at the top level. - if ( ! parentListItemId ) return; + const parentListItemId = getParentListItemId( firstClientId ); - const parentListId = getBlockRootClientId( firstClientId ); - const lastClientId = clientIds[ clientIds.length - 1 ]; - const order = getBlockOrder( parentListId ); - const followingListItems = order.slice( - getBlockIndex( lastClientId ) + 1 - ); + // Can't outdent if it's at the top level. + if ( ! parentListItemId ) return; - registry.batch( () => { - if ( followingListItems.length ) { - let nestedListId = getBlockOrder( firstClientId )[ 0 ]; + const parentListId = getBlockRootClientId( firstClientId ); + const lastClientId = clientIds[ clientIds.length - 1 ]; + const order = getBlockOrder( parentListId ); + const followingListItems = order.slice( + getBlockIndex( lastClientId ) + 1 + ); - if ( ! nestedListId ) { - const nestedListBlock = cloneBlock( - getBlock( parentListId ), - {}, - [] - ); - nestedListId = nestedListBlock.clientId; - insertBlock( nestedListBlock, 0, firstClientId, false ); - // Immediately update the block list settings, otherwise - // blocks can't be moved here due to canInsert checks. - updateBlockListSettings( - nestedListId, - getBlockListSettings( parentListId ) - ); - } + registry.batch( () => { + if ( followingListItems.length ) { + let nestedListId = getBlockOrder( firstClientId )[ 0 ]; - moveBlocksToPosition( - followingListItems, - parentListId, - nestedListId + if ( ! nestedListId ) { + const nestedListBlock = cloneBlock( + getBlock( parentListId ), + {}, + [] + ); + nestedListId = nestedListBlock.clientId; + insertBlock( nestedListBlock, 0, firstClientId, false ); + // Immediately update the block list settings, otherwise + // blocks can't be moved here due to canInsert checks. + updateBlockListSettings( + nestedListId, + getBlockListSettings( parentListId ) ); } + moveBlocksToPosition( - clientIds, + followingListItems, parentListId, - getBlockRootClientId( parentListItemId ), - getBlockIndex( parentListItemId ) + 1 + nestedListId ); - if ( ! getBlockOrder( parentListId ).length ) { - const shouldSelectParent = false; - removeBlock( parentListId, shouldSelectParent ); - } - } ); - }, [] ), - ]; + } + moveBlocksToPosition( + clientIds, + parentListId, + getBlockRootClientId( parentListItemId ), + getBlockIndex( parentListItemId ) + 1 + ); + if ( ! getBlockOrder( parentListId ).length ) { + const shouldSelectParent = false; + removeBlock( parentListId, shouldSelectParent ); + } + } ); + }, [] ); } diff --git a/packages/block-library/src/list-item/hooks/use-space.js b/packages/block-library/src/list-item/hooks/use-space.js index 6079b2c5edb281..deb6313e4b1b0e 100644 --- a/packages/block-library/src/list-item/hooks/use-space.js +++ b/packages/block-library/src/list-item/hooks/use-space.js @@ -12,9 +12,9 @@ import { useSelect } from '@wordpress/data'; import useIndentListItem from './use-indent-list-item'; export default function useSpace( clientId ) { - const { getSelectionStart, getSelectionEnd } = + const { getSelectionStart, getSelectionEnd, getBlockIndex } = useSelect( blockEditorStore ); - const [ canIndent, indentListItem ] = useIndentListItem( clientId ); + const indentListItem = useIndentListItem( clientId ); return useRefEffect( ( element ) => { @@ -23,7 +23,6 @@ export default function useSpace( clientId ) { if ( event.defaultPrevented || - ! canIndent || keyCode !== SPACE || // Only override when no modifiers are pressed. shiftKey || @@ -34,6 +33,10 @@ export default function useSpace( clientId ) { return; } + if ( getBlockIndex( clientId ) === 0 ) { + return; + } + const selectionStart = getSelectionStart(); const selectionEnd = getSelectionEnd(); if ( @@ -50,6 +53,6 @@ export default function useSpace( clientId ) { element.removeEventListener( 'keydown', onKeyDown ); }; }, - [ canIndent, indentListItem ] + [ clientId, indentListItem ] ); } diff --git a/packages/block-library/src/list/edit.js b/packages/block-library/src/list/edit.js index 569e4182b3ea55..e1d29d517a5ffe 100644 --- a/packages/block-library/src/list/edit.js +++ b/packages/block-library/src/list/edit.js @@ -68,48 +68,40 @@ function useMigrateOnLoad( attributes, clientId ) { } function useOutdentList( clientId ) { - const { canOutdent } = useSelect( - ( innerSelect ) => { - const { getBlockRootClientId, getBlock } = - innerSelect( blockEditorStore ); - const parentId = getBlockRootClientId( clientId ); - return { - canOutdent: - !! parentId && - getBlock( parentId ).name === 'core/list-item', - }; - }, - [ clientId ] - ); const { replaceBlocks, selectionChange } = useDispatch( blockEditorStore ); const { getBlockRootClientId, getBlockAttributes, getBlock } = useSelect( blockEditorStore ); - return [ - canOutdent, - useCallback( () => { - const parentBlockId = getBlockRootClientId( clientId ); - const parentBlockAttributes = getBlockAttributes( parentBlockId ); - // Create a new parent block without the inner blocks. - const newParentBlock = createBlock( - 'core/list-item', - parentBlockAttributes - ); - const { innerBlocks } = getBlock( clientId ); - // Replace the parent block with a new parent block without inner blocks, - // and make the inner blocks siblings of the parent. - replaceBlocks( - [ parentBlockId ], - [ newParentBlock, ...innerBlocks ] - ); - // Select the last child of the list being outdent. - selectionChange( innerBlocks[ innerBlocks.length - 1 ].clientId ); - }, [ clientId ] ), - ]; + return useCallback( () => { + const parentBlockId = getBlockRootClientId( clientId ); + const parentBlockAttributes = getBlockAttributes( parentBlockId ); + // Create a new parent block without the inner blocks. + const newParentBlock = createBlock( + 'core/list-item', + parentBlockAttributes + ); + const { innerBlocks } = getBlock( clientId ); + // Replace the parent block with a new parent block without inner blocks, + // and make the inner blocks siblings of the parent. + replaceBlocks( [ parentBlockId ], [ newParentBlock, ...innerBlocks ] ); + // Select the last child of the list being outdent. + selectionChange( innerBlocks[ innerBlocks.length - 1 ].clientId ); + }, [ clientId ] ); } function IndentUI( { clientId } ) { - const [ canOutdent, outdentList ] = useOutdentList( clientId ); + const outdentList = useOutdentList( clientId ); + const canOutdent = useSelect( + ( select ) => { + const { getBlockRootClientId, getBlockName } = + select( blockEditorStore ); + return ( + getBlockName( getBlockRootClientId( clientId ) ) === + 'core/list-item' + ); + }, + [ clientId ] + ); return ( <> <ToolbarButton From 829e69cdcea8d671ea426ac168a0b3466a031c1c Mon Sep 17 00:00:00 2001 From: Siobhan Bamber <siobhan@automattic.com> Date: Thu, 14 Dec 2023 19:46:54 +0000 Subject: [PATCH 199/325] Mobile Release v1.109.3 (#57060) * Release script: Update react-native-editor version to 1.109.0 * Release script: Update CHANGELOG for version 1.109.0 * Release script: Update podfile * Update `react-native-editor` changelog * Update `react-native-editor` changelog * Mobile - Fix issue when backspacing in an empty Paragraph block (#56496) * Bring changes from #55134 to the mobile code * Mobile - RichText - Force focus when the block is selected but the textinput is not, for cases when merging blocks. * Update Buttons integration test due to a change in the logic of the app where deleting the only button available does not remove the block * Mobile - Heading block - Adds integration test for merging a Heading block with an empty Paragraph block * Mobile - Paragraph block - Adds integration test to check that backspacing in an empty Paragraph block merges succesfully with the previous block and keeps the focus on the TextInput * Mobile - RichText - Set selection values to be the last character position when merging and adds some comments to explain what is doing * Mobile - Paragraph block test - Use focusTextInput to check the TextInput is in focused instead of checking for the fomatting toolbar button * Rename shouldFocusTextInputAfterUpdate to shouldFocusTextInputAfterMerge * Update CHANGELOG * Release script: Update react-native-editor version to 1.109.1 * Release script: Update CHANGELOG for version 1.109.1 * Release script: Update podfile * [RNMobile] Fixes a crash on pasting MS Word list markup (#56653) * Add polyfill for Element.prototype.remove * Enable unit tests of `raw-handling` API filter `ms-list-converter` * Update `react-native-editor` changelog * [RNMobile] Fix issue related to receiving undefined ref in text color format (#56686) * Fix issue related to receiving undefined ref in text color format In rare cases, `TextColorEdit` might receive the `RichText` ref as undefined. This ref is used to get the background color of the text and use it in the toolbar button. * Update `react-native-editor` changelog * Add test to cover undefined `contentRef` case * Correct typo in `changelog` * [RNMobile] Fix HTML to blocks conversion when no transformations are available (#56723) * Add native workaround for HTML block in `htmlToBlocks` * Add raw handling tests This file is a clone of the same `blocks-raw-handling.js` file located in `gutenberg/test/integration`. The reason for the separation is that several of the test cases fail in the native version. For now, we are going to skip them, but we'd need to work on them in the future. Once all issues in tests are addressed, we'll remove this file in favor of the original one. * Update blocks raw handling test snapshot with original values * Disable more pasteHandler test cases due to not matching test snapshot * Comment obsolete snapshots of blocks raw handling tests The reason for commenting them instead of removing is that, in the future, we'll restore them once we address the failing test cases. * RichText (native): remove HTML check in getFormatColors (#56684) * Mobile - Global Styles - Fix issue with custom color variables not being parsed (#56752) * [RNMobile] Address `NullPointerException` crash in `AztecHeadingSpan` (#56757) Address rare cases where a null value is passed to a heading block, causing a crash. * Release script: Update react-native-editor version to 1.109.2 * Release script: Update CHANGELOG for version 1.109.2 * Release script: Update podfile * Release script: Update react-native-editor version to 1.109.3 * Release script: Update CHANGELOG for version 1.109.3 * Release script: Update podfile * Mobile - Fix having the default font sizes when there are theme font sizes available * Update CHANGELOG entry --------- Co-authored-by: Carlos Garcia <fluiddot@gmail.com> Co-authored-by: Gerardo Pacheco <gerardo.pacheco@automattic.com> Co-authored-by: Ella <4710635+ellatrix@users.noreply.github.com> --- package-lock.json | 6 +++--- packages/react-native-aztec/package.json | 2 +- packages/react-native-bridge/package.json | 2 +- packages/react-native-editor/CHANGELOG.md | 3 +++ packages/react-native-editor/ios/Podfile.lock | 8 ++++---- packages/react-native-editor/package.json | 2 +- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3d707b51aab83e..c01f19ad332288 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56135,7 +56135,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.109.2", + "version": "1.109.3", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56148,7 +56148,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.109.2", + "version": "1.109.3", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56159,7 +56159,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.109.2", + "version": "1.109.3", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index fbf34269306eea..4b24bdbc707562 100644 --- a/packages/react-native-aztec/package.json +++ b/packages/react-native-aztec/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-aztec", - "version": "1.109.2", + "version": "1.109.3", "description": "Aztec view for react-native.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-bridge/package.json b/packages/react-native-bridge/package.json index 6b9bdb782d66d1..586c5159ef165f 100644 --- a/packages/react-native-bridge/package.json +++ b/packages/react-native-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-bridge", - "version": "1.109.2", + "version": "1.109.3", "description": "Native bridge library used to integrate the block editor into a native App.", "private": true, "author": "The WordPress Contributors", diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 6f4c1ee783e198..a66368115a3206 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -17,6 +17,9 @@ For each user feature we should also add a importance categorization label to i - [**] Fix crash when sharing unsupported media types on Android [#56791] - [**] Fix regressions with wrapper props and font size customization [#56985] +## 1.109.3 +- [**] Fix duplicate/unresponsive options in font size settings. [#56985] + ## 1.109.2 - [**] Fix issue related to text color format and receiving in rare cases an undefined ref from `RichText` component [#56686] - [**] Fixes a crash on pasting MS Word list markup [#56653] diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index 51b5554191ea8d..f4eaa1c15bc1f3 100644 --- a/packages/react-native-editor/ios/Podfile.lock +++ b/packages/react-native-editor/ios/Podfile.lock @@ -13,7 +13,7 @@ PODS: - ReactCommon/turbomodule/core (= 0.71.11) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.109.2): + - Gutenberg (1.109.3): - React-Core (= 0.71.11) - React-CoreModules (= 0.71.11) - React-RCTImage (= 0.71.11) @@ -429,7 +429,7 @@ PODS: - React-RCTImage - RNSVG (13.9.0): - React-Core - - RNTAztecView (1.109.2): + - RNTAztecView (1.109.3): - React-Core - WordPress-Aztec-iOS (= 1.19.9) - SDWebImage (5.11.1): @@ -617,7 +617,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: f07662560742d82a5b73cee116c70b0b49bcc220 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: 04b94705f318337d7ead9e6d17c019bd9b1f6b1b - Gutenberg: 2da422f5cdffef9f66fc57f87ddba4dbda5ceb9d + Gutenberg: 74c7183474e117f4ffaae5eac944cf598a383095 hermes-engine: 34c863b446d0135b85a6536fa5fd89f48196f848 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -662,7 +662,7 @@ SPEC CHECKSUMS: RNReanimated: d4f363f4987ae0ade3e36ff81c94e68261bf4b8d RNScreens: 68fd1060f57dd1023880bf4c05d74784b5392789 RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 - RNTAztecView: dc2635b4d33818f4c113717ff67071c1e367ed8c + RNTAztecView: fd32ea370f13d9edd7f43b65b6270ae499757d69 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d WordPress-Aztec-iOS: fbebd569c61baa252b3f5058c0a2a9a6ada686bb diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index bcc15b44b4ca1b..90f99b36a0b0a8 100644 --- a/packages/react-native-editor/package.json +++ b/packages/react-native-editor/package.json @@ -1,6 +1,6 @@ { "name": "@wordpress/react-native-editor", - "version": "1.109.2", + "version": "1.109.3", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", From 30b5e03b2afc1f5ed68c4f4359cb2bfbfa2e7af5 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Thu, 14 Dec 2023 22:16:55 +0100 Subject: [PATCH 200/325] Mobile: fix getPxFromCssUnit circular dependency (#57045) --- packages/block-editor/README.md | 9 +++------ .../src/components/plain-text/index.native.js | 7 ++++++- .../src/components/rich-text/native/index.native.js | 3 ++- packages/block-editor/src/utils/get-px-from-css-unit.js | 8 ++++++++ packages/block-editor/src/utils/index.js | 2 +- packages/block-library/src/spacer/edit.native.js | 6 ++++-- packages/components/src/font-size-picker/index.native.js | 2 +- packages/components/src/index.native.js | 1 + .../src/mobile/global-styles-context/utils.native.js | 2 +- .../src/mobile/utils/get-px-from-css-unit.native.js} | 0 .../mobile/utils/test/get-px-from-css-unit.native.js} | 2 +- 11 files changed, 28 insertions(+), 14 deletions(-) create mode 100644 packages/block-editor/src/utils/get-px-from-css-unit.js rename packages/{block-editor/src/utils/parse-css-unit-to-px.js => components/src/mobile/utils/get-px-from-css-unit.native.js} (100%) rename packages/{block-editor/src/utils/test/parse-css-unit-to-px.js => components/src/mobile/utils/test/get-px-from-css-unit.native.js} (99%) diff --git a/packages/block-editor/README.md b/packages/block-editor/README.md index 6c39b5dcc44b46..5917ac235505cb 100644 --- a/packages/block-editor/README.md +++ b/packages/block-editor/README.md @@ -539,16 +539,13 @@ _Returns_ ### getPxFromCssUnit -Returns the px value of a cssUnit. The memoized version of getPxFromCssUnit; - -_Parameters_ +> **Deprecated** -- _cssUnit_ `string`: -- _options_ `Object`: +This function was accidentially exposed for mobile/native usage. _Returns_ -- `string`: returns the cssUnit value in a simple px format. +- `string`: Empty string. ### getSpacingPresetCssVar diff --git a/packages/block-editor/src/components/plain-text/index.native.js b/packages/block-editor/src/components/plain-text/index.native.js index dc1af954c6e1cd..d61c3647af0184 100644 --- a/packages/block-editor/src/components/plain-text/index.native.js +++ b/packages/block-editor/src/components/plain-text/index.native.js @@ -7,7 +7,12 @@ import { TextInput, Platform, Dimensions } from 'react-native'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { RichText, getPxFromCssUnit } from '@wordpress/block-editor'; +import { getPxFromCssUnit } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import RichText from '../rich-text'; /** * Internal dependencies diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 116425a15b53b1..83396cbb4319fb 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -15,7 +15,8 @@ import { showUserSuggestions, showXpostSuggestions, } from '@wordpress/react-native-bridge'; -import { BlockFormatControls, getPxFromCssUnit } from '@wordpress/block-editor'; +import { BlockFormatControls } from '@wordpress/block-editor'; +import { getPxFromCssUnit } from '@wordpress/components'; import { Component } from '@wordpress/element'; import { compose, diff --git a/packages/block-editor/src/utils/get-px-from-css-unit.js b/packages/block-editor/src/utils/get-px-from-css-unit.js new file mode 100644 index 00000000000000..f1ee9fbcafb13b --- /dev/null +++ b/packages/block-editor/src/utils/get-px-from-css-unit.js @@ -0,0 +1,8 @@ +/** + * This function was accidentially exposed for mobile/native usage. + * + * @deprecated + * + * @return {string} Empty string. + */ +export default () => ''; diff --git a/packages/block-editor/src/utils/index.js b/packages/block-editor/src/utils/index.js index af45111759699c..ee3b2692b369a8 100644 --- a/packages/block-editor/src/utils/index.js +++ b/packages/block-editor/src/utils/index.js @@ -1,3 +1,3 @@ export { default as transformStyles } from './transform-styles'; export * from './block-variation-transforms'; -export { default as getPxFromCssUnit } from './parse-css-unit-to-px'; +export { default as getPxFromCssUnit } from './get-px-from-css-unit'; diff --git a/packages/block-library/src/spacer/edit.native.js b/packages/block-library/src/spacer/edit.native.js index 614624570a6d95..c6ef3095e26acc 100644 --- a/packages/block-library/src/spacer/edit.native.js +++ b/packages/block-library/src/spacer/edit.native.js @@ -6,14 +6,16 @@ import { View, useWindowDimensions } from 'react-native'; /** * WordPress dependencies */ -import { useConvertUnitToMobile } from '@wordpress/components'; +import { + useConvertUnitToMobile, + getPxFromCssUnit, +} from '@wordpress/components'; import { withPreferredColorScheme } from '@wordpress/compose'; import { InspectorControls, isValueSpacingPreset, useSettings, getCustomValueFromPreset, - getPxFromCssUnit, } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; diff --git a/packages/components/src/font-size-picker/index.native.js b/packages/components/src/font-size-picker/index.native.js index 1d5d25cbc1b734..089c2b39230008 100644 --- a/packages/components/src/font-size-picker/index.native.js +++ b/packages/components/src/font-size-picker/index.native.js @@ -11,11 +11,11 @@ import { useState } from '@wordpress/element'; import { Icon, chevronRight, check } from '@wordpress/icons'; import { __, sprintf } from '@wordpress/i18n'; import { BottomSheet } from '@wordpress/components'; -import { getPxFromCssUnit } from '@wordpress/block-editor'; /** * Internal dependencies */ +import { default as getPxFromCssUnit } from '../mobile/utils/get-px-from-css-unit'; import { default as UnitControl, useCustomUnits } from '../unit-control'; import styles from './style.scss'; diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index f2c1591dc3ce2c..3958f359ca3079 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -121,6 +121,7 @@ export { ALIGNMENT_BREAKPOINTS, alignmentHelpers, } from './mobile/utils/alignments'; +export { default as getPxFromCssUnit } from './mobile/utils/get-px-from-css-unit'; // Hooks. export { diff --git a/packages/components/src/mobile/global-styles-context/utils.native.js b/packages/components/src/mobile/global-styles-context/utils.native.js index ac77945c464cb7..f03a2afa77c406 100644 --- a/packages/components/src/mobile/global-styles-context/utils.native.js +++ b/packages/components/src/mobile/global-styles-context/utils.native.js @@ -9,7 +9,6 @@ import { colord } from 'colord'; * WordPress dependencies */ import { - getPxFromCssUnit, useSettings, useMultipleOriginColorsAndGradients, SETTINGS_DEFAULTS, @@ -19,6 +18,7 @@ import { usePreferredColorSchemeStyle } from '@wordpress/compose'; /** * Internal dependencies */ +import { default as getPxFromCssUnit } from '../utils/get-px-from-css-unit'; import { useGlobalStyles } from './index.native'; export const BLOCK_STYLE_ATTRIBUTES = [ diff --git a/packages/block-editor/src/utils/parse-css-unit-to-px.js b/packages/components/src/mobile/utils/get-px-from-css-unit.native.js similarity index 100% rename from packages/block-editor/src/utils/parse-css-unit-to-px.js rename to packages/components/src/mobile/utils/get-px-from-css-unit.native.js diff --git a/packages/block-editor/src/utils/test/parse-css-unit-to-px.js b/packages/components/src/mobile/utils/test/get-px-from-css-unit.native.js similarity index 99% rename from packages/block-editor/src/utils/test/parse-css-unit-to-px.js rename to packages/components/src/mobile/utils/test/get-px-from-css-unit.native.js index 17fd36e1cddcb6..852fc0b4f9a78a 100644 --- a/packages/block-editor/src/utils/test/parse-css-unit-to-px.js +++ b/packages/components/src/mobile/utils/test/get-px-from-css-unit.native.js @@ -4,7 +4,7 @@ import { default as memoizedGetPxFromCssUnit, getPxFromCssUnit, -} from '../parse-css-unit-to-px'; +} from '../get-px-from-css-unit'; describe( 'getPxFromCssUnit', () => { // Absolute units. From f22603f9ff6d3fdf0250969aa99b16c4ae3b3f75 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Fri, 15 Dec 2023 10:12:55 +1100 Subject: [PATCH 201/325] List View: Allow right-click to open block settings dropdown, add editor setting (#50273) * List View: Allow right-click to open block settings dropdown, add setting * Fix flicker of focus state when initially right clicking * Tidy up useSelect * Fix e2e test (hopefully) * Update help text for preferences items --- .../list-view/block-select-button.js | 4 ++ .../src/components/list-view/block.js | 72 ++++++++++++++++++- .../src/components/preferences-modal/index.js | 9 +++ packages/edit-post/src/editor.js | 6 ++ packages/edit-post/src/index.js | 1 + .../block-editor/use-site-editor-settings.js | 7 ++ .../src/components/preferences-modal/index.js | 7 ++ packages/edit-site/src/index.js | 1 + .../provider/use-block-editor-settings.js | 1 + test/e2e/specs/editor/various/a11y.spec.js | 25 ++++++- 10 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 25de5483f5192e..6b9de943ea0bf2 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -38,6 +38,8 @@ function ListViewBlockSelectButton( className, block: { clientId }, onClick, + onContextMenu, + onMouseDown, onToggleExpanded, tabIndex, onFocus, @@ -237,7 +239,9 @@ function ListViewBlockSelectButton( className ) } onClick={ onClick } + onContextMenu={ onContextMenu } onKeyDown={ onKeyDownHandler } + onMouseDown={ onMouseDown } ref={ ref } tabIndex={ tabIndex } onFocus={ onFocus } diff --git a/packages/block-editor/src/components/list-view/block.js b/packages/block-editor/src/components/list-view/block.js index 4957f79fa0d481..a90bf116e1d085 100644 --- a/packages/block-editor/src/components/list-view/block.js +++ b/packages/block-editor/src/components/list-view/block.js @@ -13,7 +13,13 @@ import { } from '@wordpress/components'; import { useInstanceId } from '@wordpress/compose'; import { moreVertical } from '@wordpress/icons'; -import { useState, useRef, useCallback, memo } from '@wordpress/element'; +import { + useCallback, + useMemo, + useState, + useRef, + memo, +} from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { sprintf, __ } from '@wordpress/i18n'; import { ESCAPE } from '@wordpress/keycodes'; @@ -53,7 +59,9 @@ function ListViewBlock( { } ) { const cellRef = useRef( null ); const rowRef = useRef( null ); + const settingsRef = useRef( null ); const [ isHovered, setIsHovered ] = useState( false ); + const [ settingsAnchorRect, setSettingsAnchorRect ] = useState(); const { isLocked, canEdit } = useBlockLock( clientId ); @@ -82,6 +90,11 @@ function ListViewBlock( { }, [ clientId ] ); + const allowRightClickOverrides = useSelect( + ( select ) => + select( blockEditorStore ).getSettings().allowRightClickOverrides, + [] + ); const showBlockActions = // When a block hides its toolbar it also hides the block settings menu, @@ -190,6 +203,56 @@ function ListViewBlock( { [ clientId, expand, collapse, isExpanded ] ); + // Allow right-clicking an item in the List View to open up the block settings dropdown. + const onContextMenu = useCallback( + ( event ) => { + if ( showBlockActions && allowRightClickOverrides ) { + settingsRef.current?.click(); + // Ensure the position of the settings dropdown is at the cursor. + setSettingsAnchorRect( + new window.DOMRect( event.clientX, event.clientY, 0, 0 ) + ); + event.preventDefault(); + } + }, + [ allowRightClickOverrides, settingsRef, showBlockActions ] + ); + + const onMouseDown = useCallback( + ( event ) => { + // Prevent right-click from focusing the block, + // because focus will be handled when opening the block settings dropdown. + if ( allowRightClickOverrides && event.button === 2 ) { + event.preventDefault(); + } + }, + [ allowRightClickOverrides ] + ); + + const settingsPopoverAnchor = useMemo( () => { + const { ownerDocument } = rowRef?.current || {}; + + // If no custom position is set, the settings dropdown will be anchored to the + // DropdownMenu toggle button. + if ( ! settingsAnchorRect || ! ownerDocument ) { + return undefined; + } + + // Position the settings dropdown at the cursor when right-clicking a block. + return { + ownerDocument, + getBoundingClientRect() { + return settingsAnchorRect; + }, + }; + }, [ settingsAnchorRect ] ); + + const clearSettingsAnchorRect = useCallback( () => { + // Clear the custom position for the settings dropdown so that it is restored back + // to being anchored to the DropdownMenu toggle button. + setSettingsAnchorRect( undefined ); + }, [ setSettingsAnchorRect ] ); + let colSpan; if ( hasRenderedMovers ) { colSpan = 2; @@ -257,6 +320,8 @@ function ListViewBlock( { <ListViewBlockContents block={ block } onClick={ selectEditorBlock } + onContextMenu={ onContextMenu } + onMouseDown={ onMouseDown } onToggleExpanded={ toggleExpanded } isSelected={ isSelected } position={ position } @@ -315,6 +380,7 @@ function ListViewBlock( { <TreeGridCell className={ listViewBlockSettingsClassName } aria-selected={ !! isSelected } + ref={ settingsRef } > { ( { ref, tabIndex, onFocus } ) => ( <BlockSettingsMenu @@ -322,10 +388,14 @@ function ListViewBlock( { block={ block } icon={ moreVertical } label={ settingsAriaLabel } + popoverProps={ { + anchor: settingsPopoverAnchor, // Used to position the settings at the cursor on right-click. + } } toggleProps={ { ref, className: 'block-editor-list-view-block__menu', tabIndex, + onClick: clearSettingsAnchorRect, onFocus, } } disableOpenOnArrowDown diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 833a10fce13c33..2e46572efec0a1 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -116,6 +116,15 @@ export default function EditPostPreferencesModal() { label={ __( 'Show block breadcrumbs' ) } /> ) } + <EnableFeature + featureName="allowRightClickOverrides" + help={ __( + 'Allows contextual list view menus via right-click, overriding browser defaults.' + ) } + label={ __( + 'Allow right-click contextual menus' + ) } + /> </PreferencesModalSection> <PreferencesModalSection title={ __( 'Document settings' ) } diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 0abf3328635a86..5dbc28ea85947b 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -30,6 +30,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { const isLargeViewport = useViewportMatch( 'medium' ); const { + allowRightClickOverrides, hasFixedToolbar, focusMode, isDistractionFree, @@ -70,6 +71,9 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { const isViewable = getPostType( postType )?.viewable ?? false; const canEditTemplate = canUser( 'create', 'templates' ); return { + allowRightClickOverrides: isFeatureActive( + 'allowRightClickOverrides' + ), hasFixedToolbar: isFeatureActive( 'fixedToolbar' ) || ! isLargeViewport, focusMode: isFeatureActive( 'focusMode' ), @@ -106,6 +110,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { focusMode, isDistractionFree, hasInlineToolbar, + allowRightClickOverrides, // This is marked as experimental to give time for the quick inserter to mature. __experimentalSetIsInserterOpened: setIsInserterOpened, @@ -133,6 +138,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { return result; }, [ settings, + allowRightClickOverrides, hasFixedToolbar, hasInlineToolbar, focusMode, diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index 64ddf1d9fb08ba..ce30f3a26c1c2d 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -47,6 +47,7 @@ export function initializeEditor( const root = createRoot( target ); dispatch( preferencesStore ).setDefaults( 'core/edit-post', { + allowRightClickOverrides: true, editorMode: 'visual', fixedToolbar: false, fullscreenMode: true, diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 2deb2d4cb5fa6e..0c4aa8d340ee36 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -94,6 +94,7 @@ export function useSpecificEditorSettings() { const { templateSlug, focusMode, + allowRightClickOverrides, isDistractionFree, hasFixedToolbar, keepCaretInsideBlock, @@ -126,6 +127,10 @@ export function useSpecificEditorSettings() { 'core/edit-site', 'distractionFree' ), + allowRightClickOverrides: !! getPreference( + 'core/edit-site', + 'allowRightClickOverrides' + ), hasFixedToolbar: !! getPreference( 'core/edit-site', 'fixedToolbar' ) || ! isLargeViewport, @@ -149,6 +154,7 @@ export function useSpecificEditorSettings() { supportsTemplateMode: true, __experimentalSetIsInserterOpened: setIsInserterOpened, focusMode: canvasMode === 'view' && focusMode ? false : focusMode, + allowRightClickOverrides, isDistractionFree, hasFixedToolbar, keepCaretInsideBlock, @@ -162,6 +168,7 @@ export function useSpecificEditorSettings() { settings, setIsInserterOpened, focusMode, + allowRightClickOverrides, isDistractionFree, hasFixedToolbar, keepCaretInsideBlock, diff --git a/packages/edit-site/src/components/preferences-modal/index.js b/packages/edit-site/src/components/preferences-modal/index.js index b0d065c0cfa5f2..87341f8bf287b0 100644 --- a/packages/edit-site/src/components/preferences-modal/index.js +++ b/packages/edit-site/src/components/preferences-modal/index.js @@ -65,6 +65,13 @@ export default function EditSitePreferencesModal() { ) } label={ __( 'Display block breadcrumbs' ) } /> + <EnableFeature + featureName="allowRightClickOverrides" + help={ __( + 'Allows contextual list view menus via right-click, overriding browser defaults.' + ) } + label={ __( 'Allow right-click contextual menus' ) } + /> </PreferencesModalSection> ), }, diff --git a/packages/edit-site/src/index.js b/packages/edit-site/src/index.js index 82014ad06eb493..9dcdb2ce99b5bc 100644 --- a/packages/edit-site/src/index.js +++ b/packages/edit-site/src/index.js @@ -52,6 +52,7 @@ export function initializeEditor( id, settings ) { // We dispatch actions and update the store synchronously before rendering // so that we won't trigger unnecessary re-renders with useEffect. dispatch( preferencesStore ).setDefaults( 'core/edit-site', { + allowRightClickOverrides: true, editorMode: 'visual', fixedToolbar: false, focusMode: false, diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index de5d9cf43437d4..34aa472a9921d5 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -29,6 +29,7 @@ const BLOCK_EDITOR_SETTINGS = [ '__unstableGalleryWithImageBlocks', 'alignWide', 'allowedBlockTypes', + 'allowRightClickOverrides', 'blockInspectorTabs', 'allowedMimeTypes', 'bodyPlaceholder', diff --git a/test/e2e/specs/editor/various/a11y.spec.js b/test/e2e/specs/editor/various/a11y.spec.js index 05c4ea3b8e97e3..3ec7318ab89e78 100644 --- a/test/e2e/specs/editor/various/a11y.spec.js +++ b/test/e2e/specs/editor/various/a11y.spec.js @@ -124,6 +124,13 @@ test.describe( 'a11y (@firefox, @webkit)', () => { page, pageUtils, } ) => { + // Note: this test depends on a particular viewport height to determine whether or not + // the modal content is scrollable. If this tests fails and needs to be debugged locally, + // double-check the viewport height when running locally versus in CI. Additionally, + // when adding or removing items from the preference menu, this test may need to be updated + // if the height of panels has changed. It would be good to find a more robust way to test + // this behavior. + // Open the top bar Options menu. await page.click( 'role=region[name="Editor top bar"i] >> role=button[name="Options"i]' @@ -145,6 +152,9 @@ test.describe( 'a11y (@firefox, @webkit)', () => { const generalTab = preferencesModal.locator( 'role=tab[name="General"i]' ); + const accessibilityTab = preferencesModal.locator( + 'role=tab[name="Accessibility"i]' + ); const blocksTab = preferencesModal.locator( 'role=tab[name="Blocks"i]' ); @@ -165,9 +175,20 @@ test.describe( 'a11y (@firefox, @webkit)', () => { await tab.focus(); } - // The General tab panel content is short and not scrollable. - // Check it's not focusable. + // The Accessibility tab is currently short and not scrollable. + // Check that it cannot be focused by tabbing. Note: this test depends + // on a particular viewport height to determine whether or not the + // modal content is scrollable. If additional Accessibility options are + // added, then eventually this test will fail. + // TODO: find a more robust way to test this behavior. await clickAndFocusTab( generalTab ); + // Navigate down to the Accessibility tab. + await pageUtils.pressKeys( 'ArrowDown', { times: 2 } ); + // Check the Accessibility tab panel is visible. + await expect( + preferencesModal.locator( 'role=tabpanel[name="Accessibility"i]' ) + ).toBeVisible(); + await expect( accessibilityTab ).toBeFocused(); await pageUtils.pressKeys( 'Shift+Tab' ); await expect( closeButton ).toBeFocused(); await pageUtils.pressKeys( 'Shift+Tab' ); From b8edcb73e9f6816f1eb600f14219fcf7feb90e5e Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Fri, 15 Dec 2023 00:22:13 +0000 Subject: [PATCH 202/325] Update Changelog for 17.2.2 --- changelog.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/changelog.txt b/changelog.txt index d0d8e111937e08..8a56965c390e7e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,10 @@ == Changelog == += 17.2.2 = + +This patch release fixes a WSOD which could occur in the site editor. See https://github.com/WordPress/gutenberg/pull/57035. + + = 17.3.0-rc.1 = From 6e30f08ffa28579a0a3a21daf1f7935414672c01 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:11:53 +0900 Subject: [PATCH 203/325] Quality: Replace wpKebabCase function with kebabCase function from components package (#57038) --- .../font-library-modal/utils/index.js | 18 ++++-------- .../utils/test/wpKebabCase.spec.js | 28 ------------------- 2 files changed, 5 insertions(+), 41 deletions(-) delete mode 100644 packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index f5723f5814e983..69db09d49a0cea 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -1,12 +1,13 @@ /** - * External dependencies + * WordPress dependencies */ -import { paramCase as kebabCase } from 'change-case'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies */ import { FONT_WEIGHTS, FONT_STYLES } from './constants'; +import { unlock } from '../../../../lock-unlock'; export function setUIValuesNeeded( font, extraValues = {} ) { if ( ! font.name && ( font.fontFamily || font.slug ) ) { @@ -129,20 +130,11 @@ export function getDisplaySrcFromFontFace( input, urlPrefix ) { return src; } -// This function replicates one behavior of _wp_to_kebab_case(). -// Additional context: https://github.com/WordPress/gutenberg/issues/53695 -export function wpKebabCase( str ) { - // If a string contains a digit followed by a number, insert a dash between them. - return kebabCase( str ).replace( - /([a-zA-Z])(\d)|(\d)([a-zA-Z])/g, - '$1$3-$2$4' - ); -} - export function makeFormDataFromFontFamilies( fontFamilies ) { const formData = new FormData(); const newFontFamilies = fontFamilies.map( ( family, familyIndex ) => { - family.slug = wpKebabCase( family.slug ); + const { kebabCase } = unlock( componentsPrivateApis ); + family.slug = kebabCase( family.slug ); if ( family?.fontFace ) { family.fontFace = family.fontFace.map( ( face, faceIndex ) => { if ( face.file ) { diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js deleted file mode 100644 index d296117ff3a49b..00000000000000 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/test/wpKebabCase.spec.js +++ /dev/null @@ -1,28 +0,0 @@ -/** - * Internal dependencies - */ -import { wpKebabCase } from '../index'; - -describe( 'wpKebabCase', () => { - it( 'should insert a dash between a letter and a digit', () => { - const input = 'abc1def'; - const expectedOutput = 'abc-1def'; - expect( wpKebabCase( input ) ).toEqual( expectedOutput ); - - const input2 = 'abc1def2ghi'; - const expectedOutput2 = 'abc-1def-2ghi'; - expect( wpKebabCase( input2 ) ).toEqual( expectedOutput2 ); - } ); - - it( 'should not insert a dash between two letters', () => { - const input = 'abcdef'; - const expectedOutput = 'abcdef'; - expect( wpKebabCase( input ) ).toEqual( expectedOutput ); - } ); - - it( 'should not insert a dash between a digit and a hyphen', () => { - const input = 'abc1-def'; - const expectedOutput = 'abc-1-def'; - expect( wpKebabCase( input ) ).toEqual( expectedOutput ); - } ); -} ); From 2aa0804588b7c3082da489b11eb0b9ec7d00068e Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Fri, 15 Dec 2023 15:28:28 +1300 Subject: [PATCH 204/325] Add e2e tests to check image upload working in site editor (#57086) --- test/e2e/specs/editor/blocks/image.spec.js | 53 ++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/test/e2e/specs/editor/blocks/image.spec.js b/test/e2e/specs/editor/blocks/image.spec.js index f1041fa60061a7..adeabc860c8342 100644 --- a/test/e2e/specs/editor/blocks/image.spec.js +++ b/test/e2e/specs/editor/blocks/image.spec.js @@ -1294,6 +1294,59 @@ test.describe.skip( 'Image - interactivity', () => { } ); } ); +// Added to prevent regressions of https://github.com/WordPress/gutenberg/pull/57040. +test.describe( 'Image - Site editor', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + await requestUtils.activateTheme( 'emptytheme' ); + } ); + + test.beforeEach( async ( { admin, editor } ) => { + await admin.visitSiteEditor( { + postId: 'emptytheme//index', + postType: 'wp_template', + } ); + await editor.canvas.locator( 'body' ).click(); + } ); + + test.afterEach( async ( { requestUtils } ) => { + await requestUtils.deleteAllMedia(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'twentytwentyone' ); + } ); + + test( 'can be inserted via image upload', async ( { + editor, + imageBlockUtils, + } ) => { + await editor.insertBlock( { name: 'core/image' } ); + + const imageBlock = editor.canvas.locator( + 'role=document[name="Block: Image"i]' + ); + await expect( imageBlock ).toBeVisible(); + + const filename = await imageBlockUtils.upload( + imageBlock.locator( 'data-testid=form-file-upload-input' ) + ); + + const image = imageBlock.getByRole( 'img', { + name: 'This image has an empty alt attribute', + } ); + await expect( image ).toBeVisible(); + await expect( image ).toHaveAttribute( 'src', new RegExp( filename ) ); + + const regex = new RegExp( + `<!-- wp:image {"id":(\\d+),"sizeSlug":"full","linkDestination":"none"} --> +<figure class="wp-block-image size-full"><img src="[^"]+\\/${ filename }\\.png" alt="" class="wp-image-\\1"/></figure> +<!-- \\/wp:image -->` + ); + expect( await editor.getEditedPostContent() ).toMatch( regex ); + } ); +} ); + class ImageBlockUtils { constructor( { page } ) { /** @type {Page} */ From 58fd414f20bf6f862a94136e3171de4065f599f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 15 Dec 2023 09:15:52 +0100 Subject: [PATCH 205/325] DataViews: remove class from edit site (#57075) --- packages/dataviews/src/style.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 1282d28581dcee..81c6d1055d3285 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -242,13 +242,9 @@ } } - .edit-site-page-pages__featured-image, .dataviews-list-view__media-placeholder { min-width: $grid-unit-40; height: $grid-unit-40; - } - - .dataviews-list-view__media-placeholder { background-color: $gray-200; } From 633d6ef620f33e04c266d29af1da416a342e8557 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Fri, 15 Dec 2023 08:22:23 +0000 Subject: [PATCH 206/325] [a11y] Fix: Use spans instead of headings on dataviews table view page title. (#56956) --- packages/edit-site/src/components/page-pages/index.js | 9 ++++++--- packages/edit-site/src/components/page-pages/style.scss | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 55eb450f7ac7e3..17736abdfc55c0 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { - __experimentalHeading as Heading, + __experimentalView as View, __experimentalVStack as VStack, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -216,7 +216,10 @@ export default function PagePages() { render: ( { item } ) => { return ( <VStack spacing={ 1 }> - <Heading as="h3" level={ 5 } weight={ 500 }> + <View + as="span" + className="edit-site-page-pages__list-view-title-field" + > { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) ? ( @@ -236,7 +239,7 @@ export default function PagePages() { item.title?.rendered || item.slug ) || __( '(no title)' ) ) } - </Heading> + </View> </VStack> ); }, diff --git a/packages/edit-site/src/components/page-pages/style.scss b/packages/edit-site/src/components/page-pages/style.scss index 933fdadb8d070e..35ac8273dc555a 100644 --- a/packages/edit-site/src/components/page-pages/style.scss +++ b/packages/edit-site/src/components/page-pages/style.scss @@ -3,3 +3,9 @@ width: $grid-unit-40; height: $grid-unit-40; } + + +.edit-site-page-pages__list-view-title-field { + font-size: $default-font-size; + font-weight: 500; +} From 77a9c2667f9d5f2764e2ce55fde851e5a142d7c5 Mon Sep 17 00:00:00 2001 From: David Arenas <david.arenas@automattic.com> Date: Fri, 15 Dec 2023 11:24:41 +0100 Subject: [PATCH 207/325] Interactivity API: fix namespaces in nested interactive regions (#57029) * Add failing test * Turn namespaces into a stack inside `toVdom` * Add changelog entry --- .../interactive-blocks/tovdom-islands/render.php | 12 ++++++++++++ packages/interactivity/CHANGELOG.md | 4 ++++ packages/interactivity/src/vdom.js | 10 +++++++--- test/e2e/specs/interactivity/tovdom-islands.spec.ts | 7 +++++++ 4 files changed, 30 insertions(+), 3 deletions(-) diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php index 7b1bc6513977b8..1f53ca1331a377 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/render.php @@ -68,4 +68,16 @@ </div> </div> </div> + + + + <div data-wp-interactive='{ "namespace": "tovdom-islands" }'> + <div data-wp-interactive='{ "namespace": "something-new" }'></div> + <div data-wp-show-mock="state.falseValue"> + <span data-testid="directive after different namespace"> + The directive above should keep the `tovdom-island` namespace, + so this message should not be visible. + </span> + </div> + </div> </div> diff --git a/packages/interactivity/CHANGELOG.md b/packages/interactivity/CHANGELOG.md index ce90835dda23d3..8c03cfc314efed 100644 --- a/packages/interactivity/CHANGELOG.md +++ b/packages/interactivity/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Bug Fix + +- Fix namespaces when there are nested interactive regions. ([#57029](https://github.com/WordPress/gutenberg/pull/57029)) + ## 3.1.0 (2023-12-13) ## 3.0.0 (2023-11-29) diff --git a/packages/interactivity/src/vdom.js b/packages/interactivity/src/vdom.js index b1342ac271a8e2..860a3149e6ffd6 100644 --- a/packages/interactivity/src/vdom.js +++ b/packages/interactivity/src/vdom.js @@ -10,7 +10,8 @@ import { directivePrefix as p } from './constants'; const ignoreAttr = `data-${ p }-ignore`; const islandAttr = `data-${ p }-interactive`; const fullPrefix = `data-${ p }-`; -let namespace = null; +const namespaces = []; +const currentNamespace = () => namespaces[ namespaces.length - 1 ] ?? null; // Regular expression for directive parsing. const directiveParser = new RegExp( @@ -79,7 +80,7 @@ export function toVdom( root ) { } catch ( e ) {} if ( n === islandAttr ) { island = true; - namespace = value?.namespace ?? null; + namespaces.push( value?.namespace ?? null ); } else { directives.push( [ n, ns, value ] ); } @@ -107,7 +108,7 @@ export function toVdom( root ) { directiveParser.exec( name ); if ( ! obj[ prefix ] ) obj[ prefix ] = []; obj[ prefix ].push( { - namespace: ns ?? namespace, + namespace: ns ?? currentNamespace(), value, suffix, } ); @@ -127,6 +128,9 @@ export function toVdom( root ) { treeWalker.parentNode(); } + // Restore previous namespace. + if ( island ) namespaces.pop(); + return [ h( node.localName, props, children ) ]; } diff --git a/test/e2e/specs/interactivity/tovdom-islands.spec.ts b/test/e2e/specs/interactivity/tovdom-islands.spec.ts index fcc7c6081077a6..257b0a0fc94b8c 100644 --- a/test/e2e/specs/interactivity/tovdom-islands.spec.ts +++ b/test/e2e/specs/interactivity/tovdom-islands.spec.ts @@ -55,4 +55,11 @@ test.describe( 'toVdom - islands', () => { ); await expect( el ).toBeHidden(); } ); + + test( 'islands should recover their namespace if an inner island has changed it', async ( { + page, + } ) => { + const el = page.getByTestId( 'directive after different namespace' ); + await expect( el ).toBeHidden(); + } ); } ); From 125c130818ebb83c50618722858e8315d90e76b8 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Fri, 15 Dec 2023 11:51:48 +0100 Subject: [PATCH 208/325] fixed headings (#57098) --- docs/getting-started/fundamentals/registration-of-a-block.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index 28e1618605a200..7d7b0e5ac31634 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -8,7 +8,7 @@ For example, to allow a block [to be styled via `theme.json`](https://developer. [![Open Block Registration diagram image](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png)](https://developer.wordpress.org/files/2023/11/block-registration-e1700493399839.png "Open Block Registration diagram image") -### Registration of the block with PHP (server-side) +## Registration of the block with PHP (server-side) Block registration on the server usually takes place in the main plugin PHP file with the `register_block_type` function called on the [init hook](https://developer.wordpress.org/reference/hooks/init/). @@ -44,7 +44,7 @@ add_action( 'init', 'minimal_block_ca6eda___register_block' ); ``` _See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/minimal-block-ca6eda) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/minimal-block-ca6eda/index.php)_ -### Registration of the block with JavaScript (client-side) +## Registration of the block with JavaScript (client-side) When the block is registered on the server, you only need to register the client-side settings on the client using the same block’s name. From ab04a95714af347cac9ee0f24a3e58abf66f27ea Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Fri, 15 Dec 2023 06:41:02 -0600 Subject: [PATCH 209/325] Remove unnecessary TOCs (#57087) --- docs/contributors/code/release.md | 31 ------------------- .../platform/custom-block-editor.md | 15 --------- .../components/alignment-control/README.md | 5 --- .../block-alignment-control/README.md | 5 --- .../block-alignment-matrix-control/README.md | 10 ------ .../src/components/block-breadcrumb/README.md | 5 --- .../src/components/block-caption/README.md | 5 --- .../src/components/block-card/README.md | 5 --- .../src/components/block-icon/README.md | 5 --- .../src/components/block-inspector/README.md | 5 --- .../src/components/block-mover/README.md | 5 --- .../block-parent-selector/README.md | 5 --- .../components/block-patterns-list/README.md | 5 --- .../src/components/block-toolbar/README.md | 5 --- .../src/components/block-types-list/README.md | 5 --- .../block-variation-picker/README.md | 5 --- .../block-variation-transforms/README.md | 5 --- .../src/components/caption/README.md | 5 --- .../src/components/contrast-checker/README.md | 4 --- .../src/components/copy-handler/README.md | 10 ------ .../src/components/height-control/README.md | 5 --- .../letter-spacing-control/README.md | 5 --- .../components/line-height-control/README.md | 5 --- .../src/components/list-view/README.md | 5 --- .../multi-selection-inspector/README.md | 5 --- .../text-transform-control/README.md | 4 --- .../src/components/ungroup-button/README.md | 5 --- .../src/components/unit-control/README.md | 4 --- .../components/use-resize-canvas/README.md | 4 --- .../src/components/use-settings/README.md | 4 --- .../components/src/button-group/README.md | 6 ---- packages/components/src/button/README.md | 6 ---- .../components/src/checkbox-control/README.md | 10 +----- .../components/src/combobox-control/README.md | 6 ---- .../src/custom-select-control/README.md | 6 ---- .../components/src/dropdown-menu/README.md | 5 --- packages/components/src/form-toggle/README.md | 6 ---- packages/components/src/menu-group/README.md | 8 ----- .../src/menu-items-choice/README.md | 7 ----- packages/components/src/modal/README.md | 6 ---- packages/components/src/notice/README.md | 6 ---- packages/components/src/panel/README.md | 6 ---- .../components/src/radio-control/README.md | 6 ---- packages/components/src/radio-group/README.md | 6 ---- .../components/src/range-control/README.md | 10 +----- .../components/src/search-control/README.md | 6 ---- .../components/src/select-control/README.md | 6 ---- packages/components/src/snackbar/README.md | 6 ---- packages/components/src/spacer/README.md | 2 -- packages/components/src/tab-panel/README.md | 5 --- .../components/src/text-control/README.md | 6 ---- .../components/src/textarea-control/README.md | 6 ---- .../components/src/toolbar/toolbar/README.md | 6 ---- packages/components/src/tree-grid/README.md | 4 --- packages/create-block/README.md | 12 ------- 55 files changed, 2 insertions(+), 348 deletions(-) diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index d19be240f4870f..05194ecd834170 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -10,37 +10,6 @@ Before you begin, there are some requirements that must be met in order to succe Similar requirements apply to releasing WordPress's [npm packages](https://developer.wordpress.org/block-editor/contributors/code/release/#packages-releases-to-npm-and-wordpress-core-updates). -**Table of contents** - -- **[Gutenberg plugin releases](#gutenberg-plugin-releases)** - - [Release schedule](#release-schedule) - - [Release management](#release-management) - - [Preparing a release](#preparing-a-release) - - [Organizing and labeling milestone PRs](#organizing-and-labeling-milestone-prs) - - [Running the release workflow](#running-the-release-workflow) - - [Publishing the @wordpress packages to NPM](#publishing-the-wordpress-packages-to-npm) - - [Viewing the release draft](#viewing-the-release-draft) - - [Curating the release changelog](#curating-the-release-changelog) - - [Creating release candidate patches (cherry-picking)](#creating-release-candidate-patches-cherry-picking) - - [Automated cherry-picking](#automated-cherry-picking) - - [Manual cherry-picking](#manual-cherry-picking) - - [Publishing the release](#publishing-the-release) - - [Troubleshooting the release](#troubleshooting-the-release) - - [Documenting the release](#documenting-the-release) - - [Selecting the release highlights](#selecting-the-release-highlights) - - [Requesting release assets](#requesting-release-assets) - - [Drafting the release post](#drafting-the-release-post) - - [Publishing the release post](#publishing-the-release-post) - - [Creating minor releases](#creating-minor-releases) - - [Updating the release branch](#updating-the-release-branch) - - [Running the minor release](#running-the-minor-release) - - [Creating a minor release for previous stable releases](#creating-a-minor-release-for-previous-stable-releases) - - [Troubleshooting](#troubleshooting) -- [Packages releases to NPM and WordPress Core updates](#packages-releases-to-npm-and-wordpress-core-updates) - - [Synchronizing the Gutenberg plugin](#synchronizing-the-gutenberg-plugin) - - [WordPress releases](#wordpress-releases) - - [Development releases](#development-releases) - ## Gutenberg plugin releases The first step in releasing a stable version of the Gutenberg plugin is to [create an issue](https://github.com/WordPress/gutenberg/issues/new?assignees=&labels=&projects=&template=New_release.md) in the Gutenberg repository. The issue template is called "Gutenberg Release," and it contains a checklist for the complete release process, from release candidate to changelog curation to cherry-picking, stable release, and release post. The issue for [Gutenberg 15.7](https://github.com/WordPress/gutenberg/issues/50092) is a good example. diff --git a/docs/how-to-guides/platform/custom-block-editor.md b/docs/how-to-guides/platform/custom-block-editor.md index 94f942fd5e05f6..65f8c412c45d33 100644 --- a/docs/how-to-guides/platform/custom-block-editor.md +++ b/docs/how-to-guides/platform/custom-block-editor.md @@ -10,21 +10,6 @@ This flexibility and interoperability makes blocks a powerful tool for building This guide covers the basics of creating your first custom block editor. -## Table of contents - -- [Introduction](#introduction) -- [Code Syntax](#code-syntax) -- [What you're going to be building](#what-youre-going-to-be-building) -- [Plugin setup and organization](#plugin-setup-and-organization) -- [The "Core" of the editor](#the-core-of-the-editor) -- [Creating the custom "Block Editor" page](#creating-the-custom-block-editor-page) -- [Registering and rendering the custom block editor](#registering-and-rendering-the-custom-block-editor) -- [Reviewing the `<Editor>` component](#reviewing-the-editor-component) -- [The custom `<BlockEditor>`](#the-custom-blockeditor) -- [Reviewing the sidebar](#reviewing-the-sidebar) -- [Block persistence](#block-persistence) -- [Wrapping up](#wrapping-up) - ## Introduction With its many packages and components, the Gutenberg codebase can be daunting at first. But at its core, it's all about managing and editing blocks. So if you want to work on the editor, it's essential to understand how block editing works at a fundamental level. diff --git a/packages/block-editor/src/components/alignment-control/README.md b/packages/block-editor/src/components/alignment-control/README.md index 5f52e19de8b979..c51da803c77aef 100644 --- a/packages/block-editor/src/components/alignment-control/README.md +++ b/packages/block-editor/src/components/alignment-control/README.md @@ -6,11 +6,6 @@ This component is mostly used for blocks that display text, such as Heading, Par ![Post Title block alignment options](https://make.wordpress.org/core/files/2020/09/post-title-block-alignment-options.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-alignment-control/README.md b/packages/block-editor/src/components/block-alignment-control/README.md index 14f08d1dda9ce6..22b5df9af9bd42 100644 --- a/packages/block-editor/src/components/block-alignment-control/README.md +++ b/packages/block-editor/src/components/block-alignment-control/README.md @@ -4,11 +4,6 @@ The `BlockAlignmentToolbar` component is used to render block alignment options ![Image block alignment options](https://make.wordpress.org/core/files/2020/09/image-block-alignment-options.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-alignment-matrix-control/README.md b/packages/block-editor/src/components/block-alignment-matrix-control/README.md index 377f9f368dae4d..dfb38e15964124 100644 --- a/packages/block-editor/src/components/block-alignment-matrix-control/README.md +++ b/packages/block-editor/src/components/block-alignment-matrix-control/README.md @@ -4,16 +4,6 @@ The alignment matrix control allows users to quickly adjust inner block alignmen ![Button components](https://i.imgur.com/PxYkgL5.png) -## Table of contents - -- [Alignment Matrix Control](#alignment-matrix-control) - - [Table of contents](#table-of-contents) - - [Design guidelines](#design-guidelines) - - [Usage](#usage) - - [Development guidelines](#development-guidelines) - - [Usage](#usage-1) - - [Props](#props) - ## Design guidelines ### Usage diff --git a/packages/block-editor/src/components/block-breadcrumb/README.md b/packages/block-editor/src/components/block-breadcrumb/README.md index 298ff8fdf5ea61..c1ffe43472b0c1 100644 --- a/packages/block-editor/src/components/block-breadcrumb/README.md +++ b/packages/block-editor/src/components/block-breadcrumb/README.md @@ -6,11 +6,6 @@ The block breadcrumb trail displays the hierarchy of the current block selection ![Image in column block breadcrumb](https://make.wordpress.org/core/files/2020/08/gutenberg-image-in-column-block-breadcrumb.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines #### Props diff --git a/packages/block-editor/src/components/block-caption/README.md b/packages/block-editor/src/components/block-caption/README.md index acb53fd4aa4c96..2d3bce1f6d2d4b 100644 --- a/packages/block-editor/src/components/block-caption/README.md +++ b/packages/block-editor/src/components/block-caption/README.md @@ -4,11 +4,6 @@ The `BlockCaption` component renders block-level UI for adding and editing capti `BlockCaption` is used in several native blocks, including `Video`, `Image`, `Audio`, etc. -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-card/README.md b/packages/block-editor/src/components/block-card/README.md index 8aab2a45f612bd..216cf4e3865a04 100644 --- a/packages/block-editor/src/components/block-card/README.md +++ b/packages/block-editor/src/components/block-card/README.md @@ -6,11 +6,6 @@ In the editor, this component is displayed in two different places: in the block ![Heading block card in the block inspector](https://make.wordpress.org/core/files/2020/09/screenshot-wordpress.org-2020.09.08-14_19_21.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-icon/README.md b/packages/block-editor/src/components/block-icon/README.md index 4d7178032b366f..638d7956831988 100644 --- a/packages/block-editor/src/components/block-icon/README.md +++ b/packages/block-editor/src/components/block-icon/README.md @@ -6,11 +6,6 @@ The rendered an [Icon](https://github.com/WordPress/gutenberg/tree/HEAD/packages ![Image block icons in various places](https://make.wordpress.org/core/files/2020/08/image-block-icons-in-various-places.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-inspector/README.md b/packages/block-editor/src/components/block-inspector/README.md index 1f9d52482eb210..4c2fd2a06a61bf 100644 --- a/packages/block-editor/src/components/block-inspector/README.md +++ b/packages/block-editor/src/components/block-inspector/README.md @@ -4,11 +4,6 @@ The Block inspector is one of the panels that is displayed in the editor; it is ![Paragraph block inspector](https://make.wordpress.org/core/files/2020/08/paragraph-block-inspector.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-mover/README.md b/packages/block-editor/src/components/block-mover/README.md index 3763b7dbada11f..38520072b4ac86 100644 --- a/packages/block-editor/src/components/block-mover/README.md +++ b/packages/block-editor/src/components/block-mover/README.md @@ -4,11 +4,6 @@ Block movers allow moving blocks inside the editor using up and down buttons. ![Block mover screenshot](https://make.wordpress.org/core/files/2020/08/block-mover-screenshot.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-parent-selector/README.md b/packages/block-editor/src/components/block-parent-selector/README.md index 181e03a20800b6..e16af799cb66ad 100644 --- a/packages/block-editor/src/components/block-parent-selector/README.md +++ b/packages/block-editor/src/components/block-parent-selector/README.md @@ -8,11 +8,6 @@ In practice the BlockParentSelector component renders a <ToolbarButton /> compon ![Block parent selector test](https://make.wordpress.org/core/files/2020/09/block-parent-selector-test.gif) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-patterns-list/README.md b/packages/block-editor/src/components/block-patterns-list/README.md index 8b798f93b7190a..f63ea449059572 100644 --- a/packages/block-editor/src/components/block-patterns-list/README.md +++ b/packages/block-editor/src/components/block-patterns-list/README.md @@ -6,11 +6,6 @@ For more infos about blocks patterns, read [this](https://make.wordpress.org/cor ![Block patterns sidebar in WordPress 5.5](https://make.wordpress.org/core/files/2020/09/blocks-patterns-sidebar-in-wordpress-5-5.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-toolbar/README.md b/packages/block-editor/src/components/block-toolbar/README.md index be4c8e15abe097..7a8a0543e179e1 100644 --- a/packages/block-editor/src/components/block-toolbar/README.md +++ b/packages/block-editor/src/components/block-toolbar/README.md @@ -6,11 +6,6 @@ The `BlockToolbar` component is used to render a toolbar that serves as a wrappe ![Image block toolbar](https://make.wordpress.org/core/files/2020/09/image-block-toolbar.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-types-list/README.md b/packages/block-editor/src/components/block-types-list/README.md index 68e89573447db8..8740cdbf2c7741 100644 --- a/packages/block-editor/src/components/block-types-list/README.md +++ b/packages/block-editor/src/components/block-types-list/README.md @@ -10,11 +10,6 @@ This component is present in the block insertion tab, the reusable blocks tab an ![Block types list in the quick inserter modal](https://make.wordpress.org/core/files/2020/09/block-types-list-emplacement-3.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-variation-picker/README.md b/packages/block-editor/src/components/block-variation-picker/README.md index 4816822e535a2a..84aae556160667 100644 --- a/packages/block-editor/src/components/block-variation-picker/README.md +++ b/packages/block-editor/src/components/block-variation-picker/README.md @@ -10,11 +10,6 @@ This component is currently used by "Columns" and "Query Loop" blocks. ![Columns block variations](https://make.wordpress.org/core/files/2020/09/colums-block-variations.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/block-variation-transforms/README.md b/packages/block-editor/src/components/block-variation-transforms/README.md index 0eb016d493207a..c9edc9dd1639fe 100644 --- a/packages/block-editor/src/components/block-variation-transforms/README.md +++ b/packages/block-editor/src/components/block-variation-transforms/README.md @@ -4,11 +4,6 @@ This component allows to display the selected block's variations which have the By selecting such a variation an update to the selected block's attributes happen, based on the variation's attributes. -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/caption/README.md b/packages/block-editor/src/components/caption/README.md index d1f8bcddaaceda..c8cf4ebe8701dd 100644 --- a/packages/block-editor/src/components/caption/README.md +++ b/packages/block-editor/src/components/caption/README.md @@ -4,11 +4,6 @@ The `Caption` component renders the [caption part](https://wordpress.org/documen This component encapsulates the "caption" behaviour and styles over a `<RichText>` so it can be used in other components such as the `BlockCaption` component. -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/contrast-checker/README.md b/packages/block-editor/src/components/contrast-checker/README.md index 6f3b41ecb7d0cf..cb9d18252a04c9 100644 --- a/packages/block-editor/src/components/contrast-checker/README.md +++ b/packages/block-editor/src/components/contrast-checker/README.md @@ -6,10 +6,6 @@ ContrastChecker also accounts for font sizes. A notice will be rendered if the color combination of text and background colors are low. -## Table of contents - -1. [Development guidelines](#development-guidelines) - ## Developer guidelines ### Usage diff --git a/packages/block-editor/src/components/copy-handler/README.md b/packages/block-editor/src/components/copy-handler/README.md index b70c841ce0ea53..1e9883d9a2b3b3 100644 --- a/packages/block-editor/src/components/copy-handler/README.md +++ b/packages/block-editor/src/components/copy-handler/README.md @@ -8,16 +8,6 @@ Concretely, it handles the display of success messages and takes care of copying ![Copy/cut behaviours](https://user-images.githubusercontent.com/150562/81698101-6e341d80-945d-11ea-9bfb-b20781f55033.gif) -## Table of contents - -- [Copy Handler](#copy-handler) - - [Table of contents](#table-of-contents) - - [Development guidelines](#development-guidelines) - - [Usage](#usage) - - [Props](#props) - - [`children`](#children) - - [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/height-control/README.md b/packages/block-editor/src/components/height-control/README.md index 334829ea9cf51f..8853f9ef89321e 100644 --- a/packages/block-editor/src/components/height-control/README.md +++ b/packages/block-editor/src/components/height-control/README.md @@ -4,11 +4,6 @@ The `HeightControl` component adds a linked unit control and slider component fo _Note:_ It is worth noting that the minimum height option is an opt-in feature. Themes need to declare support for it before it'll be available, and a convenient way to do that is via opting in to the [appearanceTools](/docs/how-to-guides/themes/theme-json/#opt-in-into-ui-controls) UI controls. -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/letter-spacing-control/README.md b/packages/block-editor/src/components/letter-spacing-control/README.md index fb4dd4fd23daca..4288f67a2aa99b 100644 --- a/packages/block-editor/src/components/letter-spacing-control/README.md +++ b/packages/block-editor/src/components/letter-spacing-control/README.md @@ -5,11 +5,6 @@ The `LetterSpacingControl` component renders a [`UnitControl`](https://github.co This component is used for blocks that display text, commonly inside a [`ToolsPanelItem`](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/tools-panel/tools-panel-item/README.md). -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/line-height-control/README.md b/packages/block-editor/src/components/line-height-control/README.md index 36f9d17f15bd65..38ab5c3f779a77 100644 --- a/packages/block-editor/src/components/line-height-control/README.md +++ b/packages/block-editor/src/components/line-height-control/README.md @@ -6,11 +6,6 @@ The `LineHeightControl` component adds a lineHeight attribute to the core Paragr _Note:_ It is worth noting that the line height setting option is an opt-in feature. [Themes need to declare support for it](/docs/how-to-guides/themes/theme-support.md#supporting-custom-line-heights) before it'll be available. -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/list-view/README.md b/packages/block-editor/src/components/list-view/README.md index da0a255d1ac97f..0db077c6412494 100644 --- a/packages/block-editor/src/components/list-view/README.md +++ b/packages/block-editor/src/components/list-view/README.md @@ -11,11 +11,6 @@ In addition to presenting the structure of the blocks in the editor, the ListVie ![List view](https://make.wordpress.org/core/files/2020/08/block-navigation.png) ![View of a group list view](https://make.wordpress.org/core/files/2020/08/view-of-group-block-navigation.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/multi-selection-inspector/README.md b/packages/block-editor/src/components/multi-selection-inspector/README.md index e7edc7ef2f43da..726a4d72d6697d 100644 --- a/packages/block-editor/src/components/multi-selection-inspector/README.md +++ b/packages/block-editor/src/components/multi-selection-inspector/README.md @@ -6,11 +6,6 @@ This card contains information on the number of blocks selected, and in the case ![Multi selection inspector card](https://make.wordpress.org/core/files/2020/09/multi-selection-inspector-card.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/text-transform-control/README.md b/packages/block-editor/src/components/text-transform-control/README.md index 511b73b0ec696b..2d40cc16ba86f8 100644 --- a/packages/block-editor/src/components/text-transform-control/README.md +++ b/packages/block-editor/src/components/text-transform-control/README.md @@ -4,10 +4,6 @@ The `TextTransformControl` component is responsible for rendering a control elem ![TextTransformConrol Element in Inspector Control](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/text-transform-component.png?raw=true) -## Table of contents - -1. [Development guidelines](#development-guidelines) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/ungroup-button/README.md b/packages/block-editor/src/components/ungroup-button/README.md index e869e6266f5b85..fe1985535b103e 100644 --- a/packages/block-editor/src/components/ungroup-button/README.md +++ b/packages/block-editor/src/components/ungroup-button/README.md @@ -6,11 +6,6 @@ This only happens in the mobile WordPress apps. ![Ungroup button icon](https://user-images.githubusercontent.com/21242757/65593577-11006000-df91-11e9-8460-1179e9ef46d2.png) -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/unit-control/README.md b/packages/block-editor/src/components/unit-control/README.md index e5126e7bf45242..7cd5269f00d032 100644 --- a/packages/block-editor/src/components/unit-control/README.md +++ b/packages/block-editor/src/components/unit-control/README.md @@ -15,10 +15,6 @@ UnitControl component allows the user to set a value as well as a unit (e.g. `px } ``` -## Table of contents - -1. [Development guidelines](#development-guidelines) - ## Developer guidelines ### Usage diff --git a/packages/block-editor/src/components/use-resize-canvas/README.md b/packages/block-editor/src/components/use-resize-canvas/README.md index ce8f06adea5d82..a34958c56c4542 100644 --- a/packages/block-editor/src/components/use-resize-canvas/README.md +++ b/packages/block-editor/src/components/use-resize-canvas/README.md @@ -6,10 +6,6 @@ On-page CSS media queries are also updated to match the width of the device. Note that this is currently experimental, and is available as `__experimentalUseResizeCanvas`. -## Table of contents - -1. [Development guidelines](#development-guidelines) - ## Development guidelines ### Usage diff --git a/packages/block-editor/src/components/use-settings/README.md b/packages/block-editor/src/components/use-settings/README.md index 83ab802edea83c..68f580aa357bea 100644 --- a/packages/block-editor/src/components/use-settings/README.md +++ b/packages/block-editor/src/components/use-settings/README.md @@ -9,10 +9,6 @@ It does the lookup of the settings in the following order: 3. If that doesn't prove to be successful in getting a value, then it falls back to the settings from the block editor store. 4. If none of the above steps prove to be successful, then it's likely to be a deprecated setting and the deprecated setting is used instead. -## Table of contents - -1. [Development guidelines](#development-guidelines) - ## Development guidelines ### Usage diff --git a/packages/components/src/button-group/README.md b/packages/components/src/button-group/README.md index 2135a027afa5b6..5c0179d6877af9 100644 --- a/packages/components/src/button-group/README.md +++ b/packages/components/src/button-group/README.md @@ -4,12 +4,6 @@ ButtonGroup can be used to group any related buttons together. To emphasize rela ![ButtonGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index 3af46280b1bc9a..cf2748d3846f73 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -4,12 +4,6 @@ Buttons let users take actions and make choices with a single click or tap. ![Button components](https://make.wordpress.org/design/files/2019/03/button.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/checkbox-control/README.md b/packages/components/src/checkbox-control/README.md index 66f3cae2be379e..8044cef0535eaf 100644 --- a/packages/components/src/checkbox-control/README.md +++ b/packages/components/src/checkbox-control/README.md @@ -2,15 +2,7 @@ Checkboxes allow the user to select one or more items from a set. -![](https://make.wordpress.org/design/files/2019/02/CheckboxControl.png) - -Selected and unselected checkboxes - -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) +![Selected and unselected checkboxes](https://make.wordpress.org/design/files/2019/02/CheckboxControl.png) ## Design guidelines diff --git a/packages/components/src/combobox-control/README.md b/packages/components/src/combobox-control/README.md index cc15248678d275..9b64076be32402 100644 --- a/packages/components/src/combobox-control/README.md +++ b/packages/components/src/combobox-control/README.md @@ -2,12 +2,6 @@ `ComboboxControl` is an enhanced version of a [`SelectControl`](/packages/components/src/select-control/README.md), with the addition of being able to search for options using a search input. -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines These are the same as [the ones for `SelectControl`s](/packages/components/src/select-control/README.md#design-guidelines), but this component is better suited for when there are too many items to scroll through or load at once so you need to filter them based on user input. diff --git a/packages/components/src/custom-select-control/README.md b/packages/components/src/custom-select-control/README.md index 696fca338e465c..56f82f6ead84b6 100644 --- a/packages/components/src/custom-select-control/README.md +++ b/packages/components/src/custom-select-control/README.md @@ -2,12 +2,6 @@ `CustomSelectControl` allows users to select an item from a single-option menu just like [`SelectControl`](/packages/components/src/select-control/readme.md), with the addition of being able to provide custom styles for each item in the menu. This means it does not use a native `<select>`, so should only be used if the custom styling is necessary. -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines These are the same as [the ones for `SelectControl`s](/packages/components/src/select-control/readme.md#design-guidelines). diff --git a/packages/components/src/dropdown-menu/README.md b/packages/components/src/dropdown-menu/README.md index dcdb30997038eb..f1b664efc09a29 100644 --- a/packages/components/src/dropdown-menu/README.md +++ b/packages/components/src/dropdown-menu/README.md @@ -4,11 +4,6 @@ The DropdownMenu displays a list of actions (each contained in a MenuItem, MenuI ![An expanded DropdownMenu, containing a list of MenuItems.](https://wordpress.org/gutenberg/files/2019/01/DropdownMenuExample.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) - ## Anatomy ![Anatomy of a DropdownMenu.](https://wordpress.org/gutenberg/files/2019/01/DropdownMenuAnatomy.png) diff --git a/packages/components/src/form-toggle/README.md b/packages/components/src/form-toggle/README.md index 5d385e23f7bdec..ba5fbf3cb16007 100644 --- a/packages/components/src/form-toggle/README.md +++ b/packages/components/src/form-toggle/README.md @@ -4,12 +4,6 @@ FormToggle switches a single setting on or off. ![On and off FormToggles. The top toggle is on, while the bottom toggle is off.](https://wordpress.org/gutenberg/files/2019/01/Toggle.jpg) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/menu-group/README.md b/packages/components/src/menu-group/README.md index 94dc54e0cda8ce..f3e18edec9b1a1 100644 --- a/packages/components/src/menu-group/README.md +++ b/packages/components/src/menu-group/README.md @@ -4,14 +4,6 @@ ![MenuGroup Example](https://wordpress.org/gutenberg/files/2019/03/MenuGroup.png) -1. MenuGroup - -## Table of Contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/menu-items-choice/README.md b/packages/components/src/menu-items-choice/README.md index 7aa12bbbb74ff9..7ee1b6096fb1fd 100644 --- a/packages/components/src/menu-items-choice/README.md +++ b/packages/components/src/menu-items-choice/README.md @@ -4,13 +4,6 @@ ![MenuItemsChoice Example](https://wordpress.org/gutenberg/files/2019/03/MenuItemsChoice.png) -1. MenuItemsChoice - -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) - ## Design guidelines A `MenuItemsChoice` should be housed within in its own distinct `MenuGroup`, so that the set of options are distinct from nearby `MenuItems`. diff --git a/packages/components/src/modal/README.md b/packages/components/src/modal/README.md index a2165afde9d5ca..c7c0c8ec6d6348 100644 --- a/packages/components/src/modal/README.md +++ b/packages/components/src/modal/README.md @@ -4,12 +4,6 @@ Modals give users information and choices related to a task they’re trying to ![An alert modal for trashing a post](https://wordpress.org/gutenberg/files/2019/04/Modal.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/notice/README.md b/packages/components/src/notice/README.md index 0fd961a281bfc7..2efb8276cb7584 100644 --- a/packages/components/src/notice/README.md +++ b/packages/components/src/notice/README.md @@ -4,12 +4,6 @@ Use Notices to communicate prominent messages to the user. ![Notice component](https://make.wordpress.org/design/files/2019/03/Notice-Screenshot-alt.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines A Notice displays a succinct message. It can also offer the user options, like viewing a published post or updating a setting, and requires a user action to be dismissed. diff --git a/packages/components/src/panel/README.md b/packages/components/src/panel/README.md index be4beca3e75278..ae9c209f250f54 100644 --- a/packages/components/src/panel/README.md +++ b/packages/components/src/panel/README.md @@ -4,12 +4,6 @@ Panels expand and collapse multiple sections of content. ![](https://make.wordpress.org/design/files/2019/03/panel.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Anatomy diff --git a/packages/components/src/radio-control/README.md b/packages/components/src/radio-control/README.md index e9be7b3c669da6..a1fd130d033a16 100644 --- a/packages/components/src/radio-control/README.md +++ b/packages/components/src/radio-control/README.md @@ -5,12 +5,6 @@ Use radio buttons when you want users to select one option from a set, and you w ![](https://make.wordpress.org/design/files/2018/11/radio.png) Selected and unselected radio buttons -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/radio-group/README.md b/packages/components/src/radio-group/README.md index c285e1a5d27731..1c24d49f1169d5 100644 --- a/packages/components/src/radio-group/README.md +++ b/packages/components/src/radio-group/README.md @@ -12,12 +12,6 @@ Use a RadioGroup component when you want users to select one option from a small ![RadioGroup component](https://wordpress.org/gutenberg/files/2018/12/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541792995815_ButtonGroup.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/range-control/README.md b/packages/components/src/range-control/README.md index 0dd822200a7bf5..1a0f1e6adef95a 100644 --- a/packages/components/src/range-control/README.md +++ b/packages/components/src/range-control/README.md @@ -2,15 +2,7 @@ RangeControls are used to make selections from a range of incremental values. -![](https://make.wordpress.org/design/files/2018/12/rangecontrol.png) - -A RangeControl for volume - -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) +![A RangeControl for volume](https://make.wordpress.org/design/files/2018/12/rangecontrol.png) ## Design guidelines diff --git a/packages/components/src/search-control/README.md b/packages/components/src/search-control/README.md index 07a18f07130ce0..c52256b951ebba 100644 --- a/packages/components/src/search-control/README.md +++ b/packages/components/src/search-control/README.md @@ -2,12 +2,6 @@ SearchControl components let users display a search control. - -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) - Check out the [Storybook page](https://wordpress.github.io/gutenberg/?path=/docs/components-searchcontrol--docs) for a visual exploration of this component. ## Development guidelines diff --git a/packages/components/src/select-control/README.md b/packages/components/src/select-control/README.md index e113c7cf01620d..34d3134365d9f3 100644 --- a/packages/components/src/select-control/README.md +++ b/packages/components/src/select-control/README.md @@ -4,12 +4,6 @@ SelectControl allow users to select from a single or multiple option menu. It fu ![A “Link To” select with “none” selected.](https://wordpress.org/gutenberg/files/2018/12/select.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/snackbar/README.md b/packages/components/src/snackbar/README.md index bbff9b9efabcb6..7fc734c69ac9b8 100644 --- a/packages/components/src/snackbar/README.md +++ b/packages/components/src/snackbar/README.md @@ -2,12 +2,6 @@ Use Snackbars to communicate low priority, non-interruptive messages to the user. -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines A Snackbar displays a succinct message that is cleared out after a small delay. It can also offer the user options, like viewing a published post but these options should also be available elsewhere in the UI. diff --git a/packages/components/src/spacer/README.md b/packages/components/src/spacer/README.md index 577e306acbf54e..16160d39b61fa0 100644 --- a/packages/components/src/spacer/README.md +++ b/packages/components/src/spacer/README.md @@ -6,8 +6,6 @@ This feature is still experimental. “Experimental” means this is an early im `Spacer` is a primitive layout component that providers inner (`padding`) or outer (`margin`) space in-between components. It can also be used to adaptively provide space within an `HStack` or `VStack`. -## Table of contents - ## Usage `Spacer` comes with a bunch of shorthand props to adjust `margin` and `padding`. The values of these props work as a multiplier to the library's grid system (base of `4px`). diff --git a/packages/components/src/tab-panel/README.md b/packages/components/src/tab-panel/README.md index 67b00c37679eca..73d5d16b46985b 100644 --- a/packages/components/src/tab-panel/README.md +++ b/packages/components/src/tab-panel/README.md @@ -6,11 +6,6 @@ TabPanels organize content across different screens, data sets, and interactions ![The “Document” tab selected in the sidebar TabPanel.](https://wordpress.org/gutenberg/files/2019/01/s_E36D9C9B8FFA15A1A8CE224E422535A12B016F88884089575F9998E52016A49F_1541785098230_TabPanel.png) -## Table of contents - -1. Design guidelines -2. Development guidelines - ## Design guidelines ### Usage diff --git a/packages/components/src/text-control/README.md b/packages/components/src/text-control/README.md index 6dfdb9ab6545da..2a95e4c46864b8 100644 --- a/packages/components/src/text-control/README.md +++ b/packages/components/src/text-control/README.md @@ -4,12 +4,6 @@ TextControl components let users enter and edit text. ![Unfilled and filled TextControl components](https://make.wordpress.org/design/files/2019/03/TextControl.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/textarea-control/README.md b/packages/components/src/textarea-control/README.md index 2b9a5a1f70e7e7..a4d5189c065f4e 100644 --- a/packages/components/src/textarea-control/README.md +++ b/packages/components/src/textarea-control/README.md @@ -4,12 +4,6 @@ TextareaControls are TextControls that allow for multiple lines of text, and wra ![An empty TextareaControl, and a focused TextareaControl with some content entered.](https://wordpress.org/gutenberg/files/2019/01/TextareaControl.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/toolbar/toolbar/README.md b/packages/components/src/toolbar/toolbar/README.md index ee89127ded74f0..4ba7ac7d7db16c 100644 --- a/packages/components/src/toolbar/toolbar/README.md +++ b/packages/components/src/toolbar/toolbar/README.md @@ -4,12 +4,6 @@ Toolbar can be used to group related options. To emphasize groups of related ico ![Toolbar component above an image block](https://wordpress.org/gutenberg/files/2019/01/s_96EC471FE9C9D91A996770229947AAB54A03351BDE98F444FD3C1BF0CED365EA_1541782974545_ButtonGroup.png) -## Table of contents - -1. [Design guidelines](#design-guidelines) -2. [Development guidelines](#development-guidelines) -3. [Related components](#related-components) - ## Design guidelines ### Usage diff --git a/packages/components/src/tree-grid/README.md b/packages/components/src/tree-grid/README.md index f6de1bab2ddcb2..d6e861a7b9b18b 100644 --- a/packages/components/src/tree-grid/README.md +++ b/packages/components/src/tree-grid/README.md @@ -3,10 +3,6 @@ <div class="callout callout-alert"> This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. </div> -## Table of contents - -1. [Development guidelines](#development-guidelines) -2. [Related components](#related-components) ## Development guidelines diff --git a/packages/create-block/README.md b/packages/create-block/README.md index 20bb6c62ccf66c..d78f8973b44ffa 100644 --- a/packages/create-block/README.md +++ b/packages/create-block/README.md @@ -8,18 +8,6 @@ _It is largely inspired by [create-react-app](https://create-react-app.dev/docs/ > _Learn more about the [Block API at the Gutenberg HandBook](https://developer.wordpress.org/block-editor/developers/block-api/block-registration/)._ -## Table of Contents - -- [Quick start](#quick-start) -- [Usage](#usage) - - [Interactive Mode](#interactive-mode) - - [`slug`](#slug) - - [`options`](#options) -- [Available Commands](#available-commands-in-the-scaffolded-project) -- [External Project Templates](#external-project-templates) -- [Contributing to this package](#contributing-to-this-package) - - ## Quick start ```bash From 2ec7f37cae48f8ac4ce9776b3a822213160fffe3 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 15 Dec 2023 15:36:26 +0100 Subject: [PATCH 210/325] DataViews: Mark the new Templates pages as stable (#57109) --- packages/dataviews/src/view-actions.js | 12 +- .../src/components/page-main/index.js | 7 +- .../page-templates/dataviews-templates.js | 407 ---------------- .../src/components/page-templates/index.js | 453 +++++++++++++++--- 4 files changed, 396 insertions(+), 483 deletions(-) delete mode 100644 packages/edit-site/src/components/page-templates/dataviews-templates.js diff --git a/packages/dataviews/src/view-actions.js b/packages/dataviews/src/view-actions.js index 836bef54936768..f9a07e2a1830af 100644 --- a/packages/dataviews/src/view-actions.js +++ b/packages/dataviews/src/view-actions.js @@ -273,11 +273,13 @@ export default function ViewActions( { } > <DropdownMenuGroup> - <ViewTypeMenu - view={ view } - onChangeView={ onChangeView } - supportedLayouts={ supportedLayouts } - /> + { window?.__experimentalAdminViews && ( + <ViewTypeMenu + view={ view } + onChangeView={ onChangeView } + supportedLayouts={ supportedLayouts } + /> + ) } <SortMenu fields={ fields } view={ view } diff --git a/packages/edit-site/src/components/page-main/index.js b/packages/edit-site/src/components/page-main/index.js index e2b39e0ecd151d..10b5b99dc2fbf5 100644 --- a/packages/edit-site/src/components/page-main/index.js +++ b/packages/edit-site/src/components/page-main/index.js @@ -9,7 +9,6 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import PagePatterns from '../page-patterns'; import PageTemplateParts from '../page-template-parts'; import PageTemplates from '../page-templates'; -import DataviewsTemplates from '../page-templates/dataviews-templates'; import PagePages from '../page-pages'; import { unlock } from '../../lock-unlock'; @@ -21,11 +20,7 @@ export default function PageMain() { } = useLocation(); if ( path === '/wp_template/all' ) { - return window?.__experimentalAdminViews ? ( - <DataviewsTemplates /> - ) : ( - <PageTemplates /> - ); + return <PageTemplates />; } else if ( path === '/wp_template_part/all' ) { return <PageTemplateParts />; } else if ( path === '/patterns' ) { diff --git a/packages/edit-site/src/components/page-templates/dataviews-templates.js b/packages/edit-site/src/components/page-templates/dataviews-templates.js deleted file mode 100644 index 1121eeb3daa5d8..00000000000000 --- a/packages/edit-site/src/components/page-templates/dataviews-templates.js +++ /dev/null @@ -1,407 +0,0 @@ -/** - * External dependencies - */ -import removeAccents from 'remove-accents'; - -/** - * WordPress dependencies - */ -import { - Icon, - __experimentalView as View, - __experimentalText as Text, - __experimentalHStack as HStack, - __experimentalVStack as VStack, - VisuallyHidden, -} from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useState, useMemo, useCallback } from '@wordpress/element'; -import { useEntityRecords } from '@wordpress/core-data'; -import { decodeEntities } from '@wordpress/html-entities'; -import { parse } from '@wordpress/blocks'; -import { - BlockPreview, - privateApis as blockEditorPrivateApis, -} from '@wordpress/block-editor'; -import { DataViews } from '@wordpress/dataviews'; - -/** - * Internal dependencies - */ -import Page from '../page'; -import Link from '../routes/link'; -import { useAddedBy, AvatarImage } from '../list/added-by'; -import { - TEMPLATE_POST_TYPE, - ENUMERATION_TYPE, - OPERATOR_IN, - OPERATOR_NOT_IN, - LAYOUT_GRID, - LAYOUT_TABLE, - LAYOUT_LIST, -} from '../../utils/constants'; -import { - useResetTemplateAction, - deleteTemplateAction, - renameTemplateAction, -} from './template-actions'; -import usePatternSettings from '../page-patterns/use-pattern-settings'; -import { unlock } from '../../lock-unlock'; -import PostPreview from '../post-preview'; - -const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( - blockEditorPrivateApis -); - -const EMPTY_ARRAY = []; - -const defaultConfigPerViewType = { - [ LAYOUT_TABLE ]: {}, - [ LAYOUT_GRID ]: { - mediaField: 'preview', - primaryField: 'title', - }, - [ LAYOUT_LIST ]: { - primaryField: 'title', - mediaField: 'preview', - }, -}; - -const DEFAULT_VIEW = { - type: LAYOUT_TABLE, - search: '', - page: 1, - perPage: 20, - // All fields are visible by default, so it's - // better to keep track of the hidden ones. - hiddenFields: [ 'preview' ], - layout: {}, - filters: [], -}; - -function normalizeSearchInput( input = '' ) { - return removeAccents( input.trim().toLowerCase() ); -} - -function TemplateTitle( { item, view } ) { - if ( view.type === LAYOUT_LIST ) { - return ( - <> - { decodeEntities( item.title?.rendered || item.slug ) || - __( '(no title)' ) } - </> - ); - } - - return ( - <VStack spacing={ 1 }> - <View as="span" className="edit-site-list-title__customized-info"> - <Link - params={ { - postId: item.id, - postType: item.type, - canvas: 'edit', - } } - > - { decodeEntities( item.title?.rendered || item.slug ) || - __( '(no title)' ) } - </Link> - </View> - </VStack> - ); -} - -function AuthorField( { item, view } ) { - const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); - const withIcon = view.type !== LAYOUT_LIST; - - return ( - <HStack alignment="left" spacing={ 1 }> - { withIcon && imageUrl && <AvatarImage imageUrl={ imageUrl } /> } - { withIcon && ! imageUrl && ( - <div className="edit-site-list-added-by__icon"> - <Icon icon={ icon } /> - </div> - ) } - <span>{ text }</span> - </HStack> - ); -} - -function TemplatePreview( { content, viewType } ) { - const settings = usePatternSettings(); - const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); - const blocks = useMemo( () => { - return parse( content ); - }, [ content ] ); - if ( ! blocks?.length ) { - return null; - } - // Wrap everything in a block editor provider to ensure 'styles' that are needed - // for the previews are synced between the site editor store and the block editor store. - // Additionally we need to have the `__experimentalBlockPatterns` setting in order to - // render patterns inside the previews. - // TODO: Same approach is used in the patterns list and it becomes obvious that some of - // the block editor settings are needed in context where we don't have the block editor. - // Explore how we can solve this in a better way. - return ( - <ExperimentalBlockEditorProvider settings={ settings }> - <div - className={ `page-templates-preview-field is-viewtype-${ viewType }` } - style={ { backgroundColor } } - > - <BlockPreview blocks={ blocks } /> - </div> - </ExperimentalBlockEditorProvider> - ); -} - -export default function DataviewsTemplates() { - const [ templateId, setTemplateId ] = useState( null ); - const [ view, setView ] = useState( DEFAULT_VIEW ); - const { records: allTemplates, isResolving: isLoadingData } = - useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { - per_page: -1, - } ); - - const onSelectionChange = ( items ) => - setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); - - const authors = useMemo( () => { - if ( ! allTemplates ) { - return EMPTY_ARRAY; - } - const authorsSet = new Set(); - allTemplates.forEach( ( template ) => { - authorsSet.add( template.author_text ); - } ); - return Array.from( authorsSet ).map( ( author ) => ( { - value: author, - label: author, - } ) ); - }, [ allTemplates ] ); - - const fields = useMemo( - () => [ - { - header: __( 'Preview' ), - id: 'preview', - render: ( { item } ) => { - return ( - <TemplatePreview - content={ item.content.raw } - viewType={ view.type } - /> - ); - }, - minWidth: 120, - maxWidth: 120, - enableSorting: false, - }, - { - header: __( 'Template' ), - id: 'title', - getValue: ( { item } ) => item.title?.rendered || item.slug, - render: ( { item } ) => ( - <TemplateTitle item={ item } view={ view } /> - ), - maxWidth: 400, - enableHiding: false, - }, - { - header: __( 'Description' ), - id: 'description', - getValue: ( { item } ) => item.description, - render: ( { item } ) => { - return item.description ? ( - decodeEntities( item.description ) - ) : ( - <> - <Text variant="muted" aria-hidden="true"> - &#8212; - </Text> - <VisuallyHidden> - { __( 'No description.' ) } - </VisuallyHidden> - </> - ); - }, - maxWidth: 200, - enableSorting: false, - }, - { - header: __( 'Author' ), - id: 'author', - getValue: ( { item } ) => item.author_text, - render: ( { item } ) => { - return <AuthorField view={ view } item={ item } />; - }, - enableHiding: false, - type: ENUMERATION_TYPE, - elements: authors, - }, - ], - [ authors, view ] - ); - - const { shownTemplates, paginationInfo } = useMemo( () => { - if ( ! allTemplates ) { - return { - shownTemplates: EMPTY_ARRAY, - paginationInfo: { totalItems: 0, totalPages: 0 }, - }; - } - let filteredTemplates = [ ...allTemplates ]; - // Handle global search. - if ( view.search ) { - const normalizedSearch = normalizeSearchInput( view.search ); - filteredTemplates = filteredTemplates.filter( ( item ) => { - const title = item.title?.rendered || item.slug; - return ( - normalizeSearchInput( title ).includes( - normalizedSearch - ) || - normalizeSearchInput( item.description ).includes( - normalizedSearch - ) - ); - } ); - } - - // Handle filters. - if ( view.filters.length > 0 ) { - view.filters.forEach( ( filter ) => { - if ( - filter.field === 'author' && - filter.operator === OPERATOR_IN && - !! filter.value - ) { - filteredTemplates = filteredTemplates.filter( ( item ) => { - return item.author_text === filter.value; - } ); - } else if ( - filter.field === 'author' && - filter.operator === OPERATOR_NOT_IN && - !! filter.value - ) { - filteredTemplates = filteredTemplates.filter( ( item ) => { - return item.author_text !== filter.value; - } ); - } - } ); - } - - // Handle sorting. - if ( view.sort ) { - const stringSortingFields = [ 'title', 'author' ]; - const fieldId = view.sort.field; - if ( stringSortingFields.includes( fieldId ) ) { - const fieldToSort = fields.find( ( field ) => { - return field.id === fieldId; - } ); - filteredTemplates.sort( ( a, b ) => { - const valueA = fieldToSort.getValue( { item: a } ) ?? ''; - const valueB = fieldToSort.getValue( { item: b } ) ?? ''; - return view.sort.direction === 'asc' - ? valueA.localeCompare( valueB ) - : valueB.localeCompare( valueA ); - } ); - } - } - - // Handle pagination. - const start = ( view.page - 1 ) * view.perPage; - const totalItems = filteredTemplates?.length || 0; - filteredTemplates = filteredTemplates?.slice( - start, - start + view.perPage - ); - return { - shownTemplates: filteredTemplates, - paginationInfo: { - totalItems, - totalPages: Math.ceil( totalItems / view.perPage ), - }, - }; - }, [ allTemplates, view, fields ] ); - - const resetTemplateAction = useResetTemplateAction(); - const actions = useMemo( - () => [ - resetTemplateAction, - deleteTemplateAction, - renameTemplateAction, - ], - [ resetTemplateAction ] - ); - const onChangeView = useCallback( - ( viewUpdater ) => { - let updatedView = - typeof viewUpdater === 'function' - ? viewUpdater( view ) - : viewUpdater; - if ( updatedView.type !== view.type ) { - updatedView = { - ...updatedView, - layout: { - ...defaultConfigPerViewType[ updatedView.type ], - }, - }; - } - - setView( updatedView ); - }, - [ view, setView ] - ); - return ( - <> - <Page - className={ - view.type === LAYOUT_LIST - ? 'edit-site-template-pages-list-view' - : null - } - title={ __( 'Templates' ) } - > - <DataViews - paginationInfo={ paginationInfo } - fields={ fields } - actions={ actions } - data={ shownTemplates } - getItemId={ ( item ) => item.id } - isLoading={ isLoadingData } - view={ view } - onChangeView={ onChangeView } - onSelectionChange={ onSelectionChange } - deferredRendering={ - ! view.hiddenFields?.includes( 'preview' ) - } - /> - </Page> - { view.type === LAYOUT_LIST && ( - <Page> - <div className="edit-site-template-pages-preview"> - { templateId !== null ? ( - <PostPreview - postId={ templateId } - postType={ TEMPLATE_POST_TYPE } - /> - ) : ( - <div - style={ { - display: 'flex', - flexDirection: 'column', - justifyContent: 'center', - textAlign: 'center', - height: '100%', - } } - > - <p>{ __( 'Select a template to preview' ) }</p> - </div> - ) } - </div> - </Page> - ) } - </> - ); -} diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 55c666970b5cc2..f534768a237c42 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -1,92 +1,415 @@ +/** + * External dependencies + */ +import removeAccents from 'remove-accents'; + /** * WordPress dependencies */ import { - VisuallyHidden, - __experimentalHeading as Heading, + Icon, + __experimentalView as View, __experimentalText as Text, + __experimentalHStack as HStack, __experimentalVStack as VStack, + VisuallyHidden, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { useState, useMemo, useCallback } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { parse } from '@wordpress/blocks'; +import { + BlockPreview, + privateApis as blockEditorPrivateApis, +} from '@wordpress/block-editor'; +import { DataViews } from '@wordpress/dataviews'; /** * Internal dependencies */ import Page from '../page'; -import Table from '../table'; import Link from '../routes/link'; -import AddedBy from '../list/added-by'; -import TemplateActions from '../template-actions'; import AddNewTemplate from '../add-new-template'; -import { TEMPLATE_POST_TYPE } from '../../utils/constants'; +import { useAddedBy, AvatarImage } from '../list/added-by'; +import { + TEMPLATE_POST_TYPE, + ENUMERATION_TYPE, + OPERATOR_IN, + OPERATOR_NOT_IN, + LAYOUT_GRID, + LAYOUT_TABLE, + LAYOUT_LIST, +} from '../../utils/constants'; +import { + useResetTemplateAction, + deleteTemplateAction, + renameTemplateAction, +} from './template-actions'; +import usePatternSettings from '../page-patterns/use-pattern-settings'; +import { unlock } from '../../lock-unlock'; +import PostPreview from '../post-preview'; + +const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( + blockEditorPrivateApis +); + +const EMPTY_ARRAY = []; -export default function PageTemplates() { - const { records: templates } = useEntityRecords( - 'postType', - TEMPLATE_POST_TYPE, - { +const defaultConfigPerViewType = { + [ LAYOUT_TABLE ]: {}, + [ LAYOUT_GRID ]: { + mediaField: 'preview', + primaryField: 'title', + }, + [ LAYOUT_LIST ]: { + primaryField: 'title', + mediaField: 'preview', + }, +}; + +const DEFAULT_VIEW = { + type: LAYOUT_TABLE, + search: '', + page: 1, + perPage: 20, + // All fields are visible by default, so it's + // better to keep track of the hidden ones. + hiddenFields: [ 'preview' ], + layout: {}, + filters: [], +}; + +function normalizeSearchInput( input = '' ) { + return removeAccents( input.trim().toLowerCase() ); +} + +function TemplateTitle( { item, view } ) { + if ( view.type === LAYOUT_LIST ) { + return ( + <> + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + </> + ); + } + + return ( + <VStack spacing={ 1 }> + <View as="span" className="edit-site-list-title__customized-info"> + <Link + params={ { + postId: item.id, + postType: item.type, + canvas: 'edit', + } } + > + { decodeEntities( item.title?.rendered || item.slug ) || + __( '(no title)' ) } + </Link> + </View> + </VStack> + ); +} + +function AuthorField( { item, view } ) { + const { text, icon, imageUrl } = useAddedBy( item.type, item.id ); + const withIcon = view.type !== LAYOUT_LIST; + + return ( + <HStack alignment="left" spacing={ 1 }> + { withIcon && imageUrl && <AvatarImage imageUrl={ imageUrl } /> } + { withIcon && ! imageUrl && ( + <div className="edit-site-list-added-by__icon"> + <Icon icon={ icon } /> + </div> + ) } + <span>{ text }</span> + </HStack> + ); +} + +function TemplatePreview( { content, viewType } ) { + const settings = usePatternSettings(); + const [ backgroundColor = 'white' ] = useGlobalStyle( 'color.background' ); + const blocks = useMemo( () => { + return parse( content ); + }, [ content ] ); + if ( ! blocks?.length ) { + return null; + } + // Wrap everything in a block editor provider to ensure 'styles' that are needed + // for the previews are synced between the site editor store and the block editor store. + // Additionally we need to have the `__experimentalBlockPatterns` setting in order to + // render patterns inside the previews. + // TODO: Same approach is used in the patterns list and it becomes obvious that some of + // the block editor settings are needed in context where we don't have the block editor. + // Explore how we can solve this in a better way. + return ( + <ExperimentalBlockEditorProvider settings={ settings }> + <div + className={ `page-templates-preview-field is-viewtype-${ viewType }` } + style={ { backgroundColor } } + > + <BlockPreview blocks={ blocks } /> + </div> + </ExperimentalBlockEditorProvider> + ); +} + +export default function DataviewsTemplates() { + const [ templateId, setTemplateId ] = useState( null ); + const [ view, setView ] = useState( DEFAULT_VIEW ); + const { records: allTemplates, isResolving: isLoadingData } = + useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, + } ); + + const onSelectionChange = ( items ) => + setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); + + const authors = useMemo( () => { + if ( ! allTemplates ) { + return EMPTY_ARRAY; } + const authorsSet = new Set(); + allTemplates.forEach( ( template ) => { + authorsSet.add( template.author_text ); + } ); + return Array.from( authorsSet ).map( ( author ) => ( { + value: author, + label: author, + } ) ); + }, [ allTemplates ] ); + + const fields = useMemo( + () => [ + { + header: __( 'Preview' ), + id: 'preview', + render: ( { item } ) => { + return ( + <TemplatePreview + content={ item.content.raw } + viewType={ view.type } + /> + ); + }, + minWidth: 120, + maxWidth: 120, + enableSorting: false, + }, + { + header: __( 'Template' ), + id: 'title', + getValue: ( { item } ) => item.title?.rendered || item.slug, + render: ( { item } ) => ( + <TemplateTitle item={ item } view={ view } /> + ), + maxWidth: 400, + enableHiding: false, + }, + { + header: __( 'Description' ), + id: 'description', + getValue: ( { item } ) => item.description, + render: ( { item } ) => { + return item.description ? ( + decodeEntities( item.description ) + ) : ( + <> + <Text variant="muted" aria-hidden="true"> + &#8212; + </Text> + <VisuallyHidden> + { __( 'No description.' ) } + </VisuallyHidden> + </> + ); + }, + maxWidth: 200, + enableSorting: false, + }, + { + header: __( 'Author' ), + id: 'author', + getValue: ( { item } ) => item.author_text, + render: ( { item } ) => { + return <AuthorField view={ view } item={ item } />; + }, + enableHiding: false, + type: ENUMERATION_TYPE, + elements: authors, + }, + ], + [ authors, view ] ); - const columns = [ - { - header: __( 'Template' ), - cell: ( template ) => ( - <VStack> - <Heading as="h3" level={ 5 }> - <Link - params={ { - postId: template.id, - postType: template.type, - canvas: 'edit', - } } - > - { decodeEntities( - template.title?.rendered || template.slug - ) } - </Link> - </Heading> - { template.description && ( - <Text variant="muted"> - { decodeEntities( template.description ) } - </Text> - ) } - </VStack> - ), - maxWidth: 400, - }, - { - header: __( 'Added by' ), - cell: ( template ) => ( - <AddedBy postType={ template.type } postId={ template.id } /> - ), - }, - { - header: <VisuallyHidden>{ __( 'Actions' ) }</VisuallyHidden>, - cell: ( template ) => ( - <TemplateActions - postType={ template.type } - postId={ template.id } - /> - ), - }, - ]; + const { shownTemplates, paginationInfo } = useMemo( () => { + if ( ! allTemplates ) { + return { + shownTemplates: EMPTY_ARRAY, + paginationInfo: { totalItems: 0, totalPages: 0 }, + }; + } + let filteredTemplates = [ ...allTemplates ]; + // Handle global search. + if ( view.search ) { + const normalizedSearch = normalizeSearchInput( view.search ); + filteredTemplates = filteredTemplates.filter( ( item ) => { + const title = item.title?.rendered || item.slug; + return ( + normalizeSearchInput( title ).includes( + normalizedSearch + ) || + normalizeSearchInput( item.description ).includes( + normalizedSearch + ) + ); + } ); + } + // Handle filters. + if ( view.filters.length > 0 ) { + view.filters.forEach( ( filter ) => { + if ( + filter.field === 'author' && + filter.operator === OPERATOR_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text === filter.value; + } ); + } else if ( + filter.field === 'author' && + filter.operator === OPERATOR_NOT_IN && + !! filter.value + ) { + filteredTemplates = filteredTemplates.filter( ( item ) => { + return item.author_text !== filter.value; + } ); + } + } ); + } + + // Handle sorting. + if ( view.sort ) { + const stringSortingFields = [ 'title', 'author' ]; + const fieldId = view.sort.field; + if ( stringSortingFields.includes( fieldId ) ) { + const fieldToSort = fields.find( ( field ) => { + return field.id === fieldId; + } ); + filteredTemplates.sort( ( a, b ) => { + const valueA = fieldToSort.getValue( { item: a } ) ?? ''; + const valueB = fieldToSort.getValue( { item: b } ) ?? ''; + return view.sort.direction === 'asc' + ? valueA.localeCompare( valueB ) + : valueB.localeCompare( valueA ); + } ); + } + } + + // Handle pagination. + const start = ( view.page - 1 ) * view.perPage; + const totalItems = filteredTemplates?.length || 0; + filteredTemplates = filteredTemplates?.slice( + start, + start + view.perPage + ); + return { + shownTemplates: filteredTemplates, + paginationInfo: { + totalItems, + totalPages: Math.ceil( totalItems / view.perPage ), + }, + }; + }, [ allTemplates, view, fields ] ); + + const resetTemplateAction = useResetTemplateAction(); + const actions = useMemo( + () => [ + resetTemplateAction, + deleteTemplateAction, + renameTemplateAction, + ], + [ resetTemplateAction ] + ); + const onChangeView = useCallback( + ( viewUpdater ) => { + let updatedView = + typeof viewUpdater === 'function' + ? viewUpdater( view ) + : viewUpdater; + if ( updatedView.type !== view.type ) { + updatedView = { + ...updatedView, + layout: { + ...defaultConfigPerViewType[ updatedView.type ], + }, + }; + } + + setView( updatedView ); + }, + [ view, setView ] + ); return ( - <Page - title={ __( 'Templates' ) } - actions={ - <AddNewTemplate - templateType={ TEMPLATE_POST_TYPE } - showIcon={ false } - toggleProps={ { variant: 'primary' } } + <> + <Page + className={ + view.type === LAYOUT_LIST + ? 'edit-site-template-pages-list-view' + : null + } + title={ __( 'Templates' ) } + actions={ + <AddNewTemplate + templateType={ TEMPLATE_POST_TYPE } + showIcon={ false } + toggleProps={ { variant: 'primary' } } + /> + } + > + <DataViews + paginationInfo={ paginationInfo } + fields={ fields } + actions={ actions } + data={ shownTemplates } + getItemId={ ( item ) => item.id } + isLoading={ isLoadingData } + view={ view } + onChangeView={ onChangeView } + onSelectionChange={ onSelectionChange } + deferredRendering={ + ! view.hiddenFields?.includes( 'preview' ) + } /> - } - > - { templates && <Table data={ templates } columns={ columns } /> } - </Page> + </Page> + { view.type === LAYOUT_LIST && ( + <Page> + <div className="edit-site-template-pages-preview"> + { templateId !== null ? ( + <PostPreview + postId={ templateId } + postType={ TEMPLATE_POST_TYPE } + /> + ) : ( + <div + style={ { + display: 'flex', + flexDirection: 'column', + justifyContent: 'center', + textAlign: 'center', + height: '100%', + } } + > + <p>{ __( 'Select a template to preview' ) }</p> + </div> + ) } + </div> + </Page> + ) } + </> ); } From d791ac8ab3d9364d81d9e4c0b217e60a9d2d190a Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Fri, 15 Dec 2023 16:25:05 +0000 Subject: [PATCH 211/325] Sort order: Use unicode characters instead of svg icons (#56833) Co-authored-by: Andrew Hayward <andrew.hayward@automattic.com> --- packages/dataviews/src/style.scss | 13 +++++++++++++ packages/dataviews/src/view-table.js | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index 81c6d1055d3285..f5e476ecebd15e 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -118,6 +118,19 @@ padding-bottom: $grid-unit-05; } } + + .dataviews-table-header-button { + padding: 0; + gap: $grid-unit-05; + + span { + speak: none; + + &:empty { + display: none; + } + } + } } .dataviews-grid-view { diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index 71d05e66c6b3c3..c5323bebea0ef5 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -4,8 +4,6 @@ import { __ } from '@wordpress/i18n'; import { useAsyncList } from '@wordpress/compose'; import { - chevronDown, - chevronUp, unseen, check, arrowUp, @@ -40,7 +38,7 @@ const sortingItemsInfo = { asc: { icon: arrowUp, label: __( 'Sort ascending' ) }, desc: { icon: arrowDown, label: __( 'Sort descending' ) }, }; -const sortIcons = { asc: chevronUp, desc: chevronDown }; +const sortArrows = { asc: '↑', desc: '↓' }; function HeaderMenu( { field, view, onChangeView } ) { const isSortable = field.enableSorting !== false; @@ -92,12 +90,17 @@ function HeaderMenu( { field, view, onChangeView } ) { align="start" trigger={ <Button - icon={ isSorted && sortIcons[ view.sort.direction ] } - iconPosition="right" - text={ field.header } - style={ { padding: 0 } } size="compact" - /> + className="dataviews-table-header-button" + style={ { padding: 0 } } + > + { field.header } + { isSorted && ( + <span aria-hidden="true"> + { isSorted && sortArrows[ view.sort.direction ] } + </span> + ) } + </Button> } > <WithSeparators> From 04b5889281e9dd38ad61a747169457892ea4491a Mon Sep 17 00:00:00 2001 From: Miguel Fonseca <150562+mcsf@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:05:53 +0000 Subject: [PATCH 212/325] Patterns (unsynced): prevent infinite loops due to recursive patterns (#56511) * Prevent infinite loops due to recursive patterns * Add tests for cycle detection * Add disclaimer about detection method * Add E2E guarding against pattern recursion --- packages/block-library/src/pattern/edit.js | 38 ++++- packages/block-library/src/pattern/index.php | 16 ++ .../src/pattern/recursion-detector.js | 145 ++++++++++++++++++ .../block-library/src/pattern/test/index.js | 74 +++++++++ .../e2e-tests/plugins/pattern-recursion.php | 22 +++ .../editor/plugins/pattern-recursion.spec.js | 35 +++++ 6 files changed, 327 insertions(+), 3 deletions(-) create mode 100644 packages/block-library/src/pattern/recursion-detector.js create mode 100644 packages/block-library/src/pattern/test/index.js create mode 100644 packages/e2e-tests/plugins/pattern-recursion.php create mode 100644 test/e2e/specs/editor/plugins/pattern-recursion.spec.js diff --git a/packages/block-library/src/pattern/edit.js b/packages/block-library/src/pattern/edit.js index e01fb37eb849e6..7befaa9cb2dee6 100644 --- a/packages/block-library/src/pattern/edit.js +++ b/packages/block-library/src/pattern/edit.js @@ -3,12 +3,19 @@ */ import { cloneBlock } from '@wordpress/blocks'; import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect } from '@wordpress/element'; +import { useState, useEffect } from '@wordpress/element'; import { + Warning, store as blockEditorStore, useBlockProps, } from '@wordpress/block-editor'; import { store as coreStore } from '@wordpress/core-data'; +import { __, sprintf } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { useParsePatternDependencies } from './recursion-detector'; const PatternEdit = ( { attributes, clientId } ) => { const selectedPattern = useSelect( @@ -32,6 +39,9 @@ const PatternEdit = ( { attributes, clientId } ) => { const { getBlockRootClientId, getBlockEditingMode } = useSelect( blockEditorStore ); + const [ hasRecursionError, setHasRecursionError ] = useState( false ); + const parsePatternDependencies = useParsePatternDependencies(); + // Duplicated in packages/edit-site/src/components/start-template-options/index.js. function injectThemeAttributeInBlockTemplateContent( block ) { if ( @@ -64,7 +74,14 @@ const PatternEdit = ( { attributes, clientId } ) => { // This change won't be saved. // It will continue to pull from the pattern file unless changes are made to its respective template part. useEffect( () => { - if ( selectedPattern?.blocks ) { + if ( ! hasRecursionError && selectedPattern?.blocks ) { + try { + parsePatternDependencies( selectedPattern ); + } catch ( error ) { + setHasRecursionError( true ); + return; + } + // We batch updates to block list settings to avoid triggering cascading renders // for each container block included in a tree and optimize initial render. // Since the above uses microtasks, we need to use a microtask here as well, @@ -93,7 +110,8 @@ const PatternEdit = ( { attributes, clientId } ) => { } }, [ clientId, - selectedPattern?.blocks, + hasRecursionError, + selectedPattern, __unstableMarkNextChangeAsNotPersistent, replaceBlocks, getBlockEditingMode, @@ -103,6 +121,20 @@ const PatternEdit = ( { attributes, clientId } ) => { const props = useBlockProps(); + if ( hasRecursionError ) { + return ( + <div { ...props }> + <Warning> + { sprintf( + // translators: A warning in which %s is the name of a pattern. + __( 'Pattern "%s" cannot be rendered inside itself.' ), + selectedPattern?.name + ) } + </Warning> + </div> + ); + } + return <div { ...props } />; }; diff --git a/packages/block-library/src/pattern/index.php b/packages/block-library/src/pattern/index.php index 436452f6853001..70c389e4ec8dbe 100644 --- a/packages/block-library/src/pattern/index.php +++ b/packages/block-library/src/pattern/index.php @@ -27,6 +27,8 @@ function register_block_core_pattern() { * @return string Returns the output of the pattern. */ function render_block_core_pattern( $attributes ) { + static $seen_refs = array(); + if ( empty( $attributes['slug'] ) ) { return ''; } @@ -38,6 +40,17 @@ function render_block_core_pattern( $attributes ) { return ''; } + if ( isset( $seen_refs[ $attributes['slug'] ] ) ) { + // WP_DEBUG_DISPLAY must only be honored when WP_DEBUG. This precedent + // is set in `wp_debug_mode()`. + $is_debug = WP_DEBUG && WP_DEBUG_DISPLAY; + + return $is_debug ? + // translators: Visible only in the front end, this warning takes the place of a faulty block. %s represents a pattern's slug. + sprintf( __( '[block rendering halted for pattern "%s"]' ), $slug ) : + ''; + } + $pattern = $registry->get_registered( $slug ); $content = $pattern['content']; @@ -48,11 +61,14 @@ function render_block_core_pattern( $attributes ) { $content = gutenberg_serialize_blocks( $blocks ); } + $seen_refs[ $attributes['slug'] ] = true; + $content = do_blocks( $content ); global $wp_embed; $content = $wp_embed->autoembed( $content ); + unset( $seen_refs[ $attributes['slug'] ] ); return $content; } diff --git a/packages/block-library/src/pattern/recursion-detector.js b/packages/block-library/src/pattern/recursion-detector.js new file mode 100644 index 00000000000000..f2e80087dd4b07 --- /dev/null +++ b/packages/block-library/src/pattern/recursion-detector.js @@ -0,0 +1,145 @@ +/** + * THIS MODULE IS INTENTIONALLY KEPT WITHIN THE PATTERN BLOCK'S SOURCE. + * + * This is because this approach for preventing infinite loops due to + * recursively rendering blocks is specific to the way that the `core/pattern` + * block behaves in the editor. Any other block types that deal with recursion + * SHOULD USE THE STANDARD METHOD for avoiding loops: + * + * @see https://github.com/WordPress/gutenberg/pull/31455 + * @see packages/block-editor/src/components/recursion-provider/README.md + */ + +/** + * WordPress dependencies + */ +import { useRegistry } from '@wordpress/data'; + +/** + * Naming is hard. + * + * @see useParsePatternDependencies + * + * @type {WeakMap<Object, Function>} + */ +const cachedParsers = new WeakMap(); + +/** + * Hook used by PatternEdit to parse block patterns. It returns a function that + * takes a pattern and returns nothing but throws an error if the pattern is + * recursive. + * + * @example + * ```js + * const parsePatternDependencies = useParsePatternDependencies(); + * parsePatternDependencies( selectedPattern ); + * ``` + * + * @see parsePatternDependencies + * + * @return {Function} A function to parse block patterns. + */ +export function useParsePatternDependencies() { + const registry = useRegistry(); + + // Instead of caching maps, go straight to the point and cache bound + // functions. Each of those functions is bound to a different Map that will + // keep track of patterns in the context of the given registry. + if ( ! cachedParsers.has( registry ) ) { + const deps = new Map(); + cachedParsers.set( + registry, + parsePatternDependencies.bind( null, deps ) + ); + } + return cachedParsers.get( registry ); +} + +/** + * Parse a given pattern and traverse its contents to detect any subsequent + * patterns on which it may depend. Such occurrences will be added to an + * internal dependency graph. If a circular dependency is detected, an + * error will be thrown. + * + * EXPORTED FOR TESTING PURPOSES ONLY. + * + * @param {Map<string, Set<string>>} deps Map of pattern dependencies. + * @param {Object} pattern Pattern. + * @param {string} pattern.name Pattern name. + * @param {Array} pattern.blocks Pattern's block list. + * + * @throws {Error} If a circular dependency is detected. + */ +export function parsePatternDependencies( deps, { name, blocks } ) { + const queue = [ ...blocks ]; + while ( queue.length ) { + const block = queue.shift(); + for ( const innerBlock of block.innerBlocks ?? [] ) { + queue.unshift( innerBlock ); + } + if ( block.name === 'core/pattern' ) { + registerDependency( deps, name, block.attributes.slug ); + } + } +} + +/** + * Declare that pattern `a` depends on pattern `b`. If a circular + * dependency is detected, an error will be thrown. + * + * EXPORTED FOR TESTING PURPOSES ONLY. + * + * @param {Map<string, Set<string>>} deps Map of pattern dependencies. + * @param {string} a Slug for pattern A. + * @param {string} b Slug for pattern B. + * + * @throws {Error} If a circular dependency is detected. + */ +export function registerDependency( deps, a, b ) { + if ( ! deps.has( a ) ) { + deps.set( a, new Set() ); + } + deps.get( a ).add( b ); + if ( hasCycle( deps, a ) ) { + throw new TypeError( + `Pattern ${ a } has a circular dependency and cannot be rendered.` + ); + } +} + +/** + * Determine if a given pattern has circular dependencies on other patterns. + * This will be determined by running a depth-first search on the current state + * of the graph represented by `patternDependencies`. + * + * @param {Map<string, Set<string>>} deps Map of pattern dependencies. + * @param {string} slug Pattern slug. + * @param {Set<string>} [visitedNodes] Set to track visited nodes in the graph. + * @param {Set<string>} [currentPath] Set to track and backtrack graph paths. + * @return {boolean} Whether any cycle was found. + */ +function hasCycle( + deps, + slug, + visitedNodes = new Set(), + currentPath = new Set() +) { + visitedNodes.add( slug ); + currentPath.add( slug ); + + const dependencies = deps.get( slug ) ?? new Set(); + + for ( const dependency of dependencies ) { + if ( ! visitedNodes.has( dependency ) ) { + if ( hasCycle( deps, dependency, visitedNodes, currentPath ) ) { + return true; + } + } else if ( currentPath.has( dependency ) ) { + return true; + } + } + + // Remove the current node from the current path when backtracking + currentPath.delete( slug ); + return false; +} diff --git a/packages/block-library/src/pattern/test/index.js b/packages/block-library/src/pattern/test/index.js new file mode 100644 index 00000000000000..0f682f6cce67b9 --- /dev/null +++ b/packages/block-library/src/pattern/test/index.js @@ -0,0 +1,74 @@ +/** + * Internal dependencies + */ +import { + parsePatternDependencies, + registerDependency, +} from '../recursion-detector'; + +describe( 'core/pattern', () => { + const deps = new Map(); + + beforeEach( () => { + deps.clear(); + } ); + + describe( 'parsePatternDependencies', () => { + it( "is silent for patterns that don't require other patterns", () => { + const pattern = { + name: 'test/benign-pattern', + blocks: [ { name: 'core/paragraph' } ], + }; + expect( () => { + parsePatternDependencies( deps, pattern ); + } ).not.toThrow(); + } ); + it( 'catches self-referencing patterns', () => { + const pattern = { + name: 'test/evil-pattern', + blocks: [ { name: 'core/pattern', slug: 'test/evil-pattern' } ], + }; + expect( () => { + parsePatternDependencies( deps, pattern ); + } ).toThrow(); + } ); + } ); + + describe( 'registerDependency', () => { + it( 'is silent for patterns with no circular dependencies', () => { + expect( () => { + registerDependency( deps, 'a', 'b' ); + } ).not.toThrow(); + } ); + it( 'catches self-referencing patterns', () => { + expect( () => { + registerDependency( deps, 'a', 'a' ); + } ).toThrow(); + } ); + it( 'catches mutually-referencing patterns', () => { + registerDependency( deps, 'a', 'b' ); + expect( () => { + registerDependency( deps, 'b', 'a' ); + } ).toThrow(); + } ); + it( 'catches longer cycles', () => { + registerDependency( deps, 'a', 'b' ); + registerDependency( deps, 'b', 'c' ); + registerDependency( deps, 'b', 'd' ); + expect( () => { + registerDependency( deps, 'd', 'a' ); + } ).toThrow(); + } ); + it( 'catches any pattern depending on a tainted one', () => { + registerDependency( deps, 'a', 'b' ); + registerDependency( deps, 'b', 'c' ); + registerDependency( deps, 'b', 'd' ); + expect( () => { + registerDependency( deps, 'd', 'a' ); + } ).toThrow(); + expect( () => { + registerDependency( deps, 'e', 'd' ); + } ).toThrow(); + } ); + } ); +} ); diff --git a/packages/e2e-tests/plugins/pattern-recursion.php b/packages/e2e-tests/plugins/pattern-recursion.php new file mode 100644 index 00000000000000..eb3a2051551a26 --- /dev/null +++ b/packages/e2e-tests/plugins/pattern-recursion.php @@ -0,0 +1,22 @@ +<?php +/** + * Plugin Name: Gutenberg Test Protection Against Recursive Patterns + * Plugin URI: https://github.com/WordPress/gutenberg + * Author: Gutenberg Team + * + * @package gutenberg-test-pattern-recursion + */ + +add_filter( + 'init', + function () { + register_block_pattern( + 'evil/recursive', + array( + 'title' => 'Evil recursive', + 'description' => 'Evil recursive', + 'content' => '<!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph --><!-- wp:pattern {"slug":"evil/recursive"} /-->', + ) + ); + } +); diff --git a/test/e2e/specs/editor/plugins/pattern-recursion.spec.js b/test/e2e/specs/editor/plugins/pattern-recursion.spec.js new file mode 100644 index 00000000000000..72498bcdf9914d --- /dev/null +++ b/test/e2e/specs/editor/plugins/pattern-recursion.spec.js @@ -0,0 +1,35 @@ +/** + * WordPress dependencies + */ +const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); + +test.describe( 'Preventing Pattern Recursion', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activatePlugin( + 'gutenberg-test-protection-against-recursive-patterns' + ); + } ); + + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost(); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deactivatePlugin( + 'gutenberg-test-protection-against-recursive-patterns' + ); + } ); + + test( 'prevents infinite loops due to recursive patterns', async ( { + editor, + } ) => { + await editor.insertBlock( { + name: 'core/pattern', + attributes: { slug: 'evil/recursive' }, + } ); + const warning = editor.canvas.getByText( + 'Pattern "evil/recursive" cannot be rendered inside itself' + ); + await expect( warning ).toBeVisible(); + } ); +} ); From 9879b85b822aba8454d83ba4faaeed48f20bfd73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Fri, 15 Dec 2023 19:42:17 +0100 Subject: [PATCH 213/325] Site editor: remove `isResizing` variable from layout component as it is always false (#57119) --- .../edit-site/src/components/layout/index.js | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 71d99b9a4bcbbd..6d62ba1df07f42 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -126,7 +126,6 @@ export default function Layout() { ( isMobileViewport && isListPage ) || ( isEditorPage && isEditing ); const [ canvasResizer, canvasSize ] = useResizeObserver(); const [ fullResizer ] = useResizeObserver(); - const [ isResizing ] = useState( false ); const isEditorLoading = useIsSiteEditorLoading(); const [ isResizableFrameOversized, setIsResizableFrameOversized ] = useState( false ); @@ -307,14 +306,7 @@ export default function Layout() { <> { isListPage && <PageMain /> } { isEditorPage && ( - <div - className={ classnames( - 'edit-site-layout__canvas-container', - { - 'is-resizing': isResizing, - } - ) } - > + <div className="edit-site-layout__canvas-container"> { canvasResizer } { !! canvasSize.width && ( <motion.div @@ -325,8 +317,7 @@ export default function Layout() { scale: 1.005, transition: { duration: - disableMotion || - isResizing + disableMotion ? 0 : 0.5, ease: 'easeOut', @@ -345,10 +336,9 @@ export default function Layout() { ) } transition={ { type: 'tween', - duration: - disableMotion || isResizing - ? 0 - : ANIMATION_DURATION, + duration: disableMotion + ? 0 + : ANIMATION_DURATION, ease: 'easeOut', } } > From e9d5dc4e9a1ce1a139a77bf73ef9e955c6bc76c1 Mon Sep 17 00:00:00 2001 From: Daniel Richards <daniel.richards@automattic.com> Date: Sat, 16 Dec 2023 03:10:04 +0800 Subject: [PATCH 214/325] Fix form token field suggestion list reopening after blurring the input (#57002) * Update boolean logic to ensure suggestions list closes when expandOnFocus prop is false * Improve readability * Add Changelog entry * Fix CHANGELOG, move entry to the "Unreleased" section --------- Co-authored-by: Marcelo Serpa <boss@fullofcaffeine.com> --- packages/components/CHANGELOG.md | 1 + .../components/src/form-token-field/index.tsx | 18 +++++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 025d342073aac8..970341765bfe69 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ ### Bug Fix - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). +- `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). ## 25.14.0 (2023-12-13) diff --git a/packages/components/src/form-token-field/index.tsx b/packages/components/src/form-token-field/index.tsx index bdc3c2a53ae1d0..895cbad033212b 100644 --- a/packages/components/src/form-token-field/index.tsx +++ b/packages/components/src/form-token-field/index.tsx @@ -177,13 +177,17 @@ export function FormTokenField( props: FormTokenFieldProps ) { setInputOffsetFromEnd( 0 ); setIsActive( false ); - // If `__experimentalExpandOnFocus` is true, don't close the suggestions list when - // the user clicks on it (`tokensAndInput` will be the element that caused the blur). - const shouldKeepSuggestionsExpanded = - ! __experimentalExpandOnFocus || - ( __experimentalExpandOnFocus && - event.relatedTarget === tokensAndInput.current ); - setIsExpanded( shouldKeepSuggestionsExpanded ); + if ( __experimentalExpandOnFocus ) { + // If `__experimentalExpandOnFocus` is true, don't close the suggestions list when + // the user clicks on it (`tokensAndInput` will be the element that caused the blur). + const hasFocusWithin = + event.relatedTarget === tokensAndInput.current; + setIsExpanded( hasFocusWithin ); + } else { + // Else collapse the suggestion list. This will result in the suggestion list closing + // after a suggestion has been submitted since that causes a blur. + setIsExpanded( false ); + } setSelectedSuggestionIndex( -1 ); setSelectedSuggestionScroll( false ); From b016254d153db43b24b669c13d7ed19d79d907f3 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Fri, 15 Dec 2023 20:31:13 +0100 Subject: [PATCH 215/325] Editor: Move the Excerpt panel to the editor package (#57096) --- .../sidebar/settings-sidebar/index.js | 4 ++-- packages/edit-post/src/index.js | 11 +++++++++-- .../sidebar-edit-mode/page-panels/index.js | 2 ++ .../sidebar-edit-mode/template-panel/index.js | 2 ++ packages/editor/src/components/index.js | 1 + .../src/components/post-excerpt/check.js | 18 ++++++++++++++++++ .../src/components/post-excerpt/panel.js} | 12 +++++------- .../src/components/post-excerpt/plugin.js} | 0 .../components/post-excerpt/test/plugin.js} | 2 +- packages/editor/src/private-apis.js | 2 ++ 10 files changed, 42 insertions(+), 12 deletions(-) rename packages/{edit-post/src/components/sidebar/post-excerpt/index.js => editor/src/components/post-excerpt/panel.js} (83%) rename packages/{edit-post/src/components/sidebar/plugin-post-excerpt/index.js => editor/src/components/post-excerpt/plugin.js} (100%) rename packages/{edit-post/src/components/sidebar/plugin-post-excerpt/test/index.js => editor/src/components/post-excerpt/test/plugin.js} (94%) diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 465088b39bf80f..76d1f1b63ad636 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -13,6 +13,7 @@ import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editorStore, + PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, PostTaxonomiesPanel, @@ -23,7 +24,6 @@ import { */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import PostExcerpt from '../post-excerpt'; import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; @@ -84,7 +84,7 @@ const SidebarContent = ( { <PostLastRevisionPanel /> <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> - <PostExcerpt /> + <PostExcerptPanel /> <DiscussionPanel /> <PageAttributes /> <MetaBoxes location="side" /> diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index ce30f3a26c1c2d..ffe55e50efab08 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -15,7 +15,10 @@ import { registerLegacyWidgetBlock, registerWidgetGroupBlock, } from '@wordpress/widgets'; -import { store as editorStore } from '@wordpress/editor'; +import { + privateApis as editorPrivateApis, + store as editorStore, +} from '@wordpress/editor'; /** * Internal dependencies @@ -24,6 +27,10 @@ import './hooks'; import './plugins'; import Editor from './editor'; import { store as editPostStore } from './store'; +import { unlock } from './lock-unlock'; + +const { PluginPostExcerpt: __experimentalPluginPostExcerpt } = + unlock( editorPrivateApis ); /** * Initializes and returns an instance of Editor. @@ -208,5 +215,5 @@ export { default as PluginSidebar } from './components/sidebar/plugin-sidebar'; export { default as PluginSidebarMoreMenuItem } from './components/header/plugin-sidebar-more-menu-item'; export { default as __experimentalFullscreenModeClose } from './components/header/fullscreen-mode-close'; export { default as __experimentalMainDashboardButton } from './components/header/main-dashboard-button'; -export { default as __experimentalPluginPostExcerpt } from './components/sidebar/plugin-post-excerpt'; +export { __experimentalPluginPostExcerpt }; export { store } from './store'; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index f6c84d8cfd3adc..e350225a1212aa 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -13,6 +13,7 @@ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { + PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, PostTaxonomiesPanel, @@ -102,6 +103,7 @@ export default function PagePanels() { <PostLastRevisionPanel /> <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> + <PostExcerptPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 66b5991872cf96..157a56b2461712 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -4,6 +4,7 @@ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; import { + PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, PostTaxonomiesPanel, @@ -66,6 +67,7 @@ export default function TemplatePanel() { <PostLastRevisionPanel /> <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> + <PostExcerptPanel /> </> ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 79a4d90663f669..0ae7ac0824a7fd 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -32,6 +32,7 @@ export { default as PostAuthorPanel } from './post-author/panel'; export { default as PostComments } from './post-comments'; export { default as PostExcerpt } from './post-excerpt'; export { default as PostExcerptCheck } from './post-excerpt/check'; +export { default as PostExcerptPanel } from './post-excerpt/panel'; export { default as PostFeaturedImage } from './post-featured-image'; export { default as PostFeaturedImageCheck } from './post-featured-image/check'; export { default as PostFeaturedImagePanel } from './post-featured-image/panel'; diff --git a/packages/editor/src/components/post-excerpt/check.js b/packages/editor/src/components/post-excerpt/check.js index 7d77ba77cd029a..f8ff4bb5d37dab 100644 --- a/packages/editor/src/components/post-excerpt/check.js +++ b/packages/editor/src/components/post-excerpt/check.js @@ -1,9 +1,27 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; + /** * Internal dependencies */ import PostTypeSupportCheck from '../post-type-support-check'; +import { store as editorStore } from '../../store'; function PostExcerptCheck( { children } ) { + const postType = useSelect( ( select ) => { + const { getEditedPostAttribute } = select( editorStore ); + return getEditedPostAttribute( 'type' ); + }, [] ); + + // This special case is unfortunate, but the REST API of wp_template and wp_template_part + // support the excerpt field throught the "description" field rather than "excerpt" which means + // the default ExcerptPanel won't work for these. + if ( [ 'wp_template', 'wp_template_part' ].includes( postType ) ) { + return null; + } + return ( <PostTypeSupportCheck supportKeys="excerpt"> { children } diff --git a/packages/edit-post/src/components/sidebar/post-excerpt/index.js b/packages/editor/src/components/post-excerpt/panel.js similarity index 83% rename from packages/edit-post/src/components/sidebar/post-excerpt/index.js rename to packages/editor/src/components/post-excerpt/panel.js index 8063ed6017b08c..ab4b60611493cc 100644 --- a/packages/edit-post/src/components/sidebar/post-excerpt/index.js +++ b/packages/editor/src/components/post-excerpt/panel.js @@ -3,24 +3,22 @@ */ import { __ } from '@wordpress/i18n'; import { PanelBody } from '@wordpress/components'; -import { - PostExcerpt as PostExcerptForm, - PostExcerptCheck, - store as editorStore, -} from '@wordpress/editor'; import { useDispatch, useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import PluginPostExcerpt from '../plugin-post-excerpt'; +import PostExcerptForm from './index'; +import PostExcerptCheck from './check'; +import PluginPostExcerpt from './plugin'; +import { store as editorStore } from '../../store'; /** * Module Constants */ const PANEL_NAME = 'post-excerpt'; -export default function PostExcerpt() { +export default function PostExcerptPanel() { const { isOpened, isEnabled } = useSelect( ( select ) => { const { isEditorPanelOpened, isEditorPanelEnabled } = select( editorStore ); diff --git a/packages/edit-post/src/components/sidebar/plugin-post-excerpt/index.js b/packages/editor/src/components/post-excerpt/plugin.js similarity index 100% rename from packages/edit-post/src/components/sidebar/plugin-post-excerpt/index.js rename to packages/editor/src/components/post-excerpt/plugin.js diff --git a/packages/edit-post/src/components/sidebar/plugin-post-excerpt/test/index.js b/packages/editor/src/components/post-excerpt/test/plugin.js similarity index 94% rename from packages/edit-post/src/components/sidebar/plugin-post-excerpt/test/index.js rename to packages/editor/src/components/post-excerpt/test/plugin.js index 4b81a1326d5176..d6f81d459deb33 100644 --- a/packages/edit-post/src/components/sidebar/plugin-post-excerpt/test/index.js +++ b/packages/editor/src/components/post-excerpt/test/plugin.js @@ -11,7 +11,7 @@ import { SlotFillProvider } from '@wordpress/components'; /** * Internal dependencies */ -import PluginPostExcerptPanel from '../'; +import PluginPostExcerptPanel from '../plugin'; describe( 'PluginPostExcerptPanel', () => { test( 'renders fill properly', () => { diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index ac5bd4324946ee..da86d138bb2fd8 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -8,6 +8,7 @@ import { EntitiesSavedStatesExtensible } from './components/entities-saved-state import useBlockEditorSettings from './components/provider/use-block-editor-settings'; import PostPanelRow from './components/post-panel-row'; import PreviewDropdown from './components/preview-dropdown'; +import PluginPostExcerpt from './components/post-excerpt/plugin'; export const privateApis = {}; lock( privateApis, { @@ -16,6 +17,7 @@ lock( privateApis, { EntitiesSavedStatesExtensible, PostPanelRow, PreviewDropdown, + PluginPostExcerpt, // This is a temporary private API while we're updating the site editor to use EditorProvider. useBlockEditorSettings, From 6e650daadfb8eabd919a812c3f881f0b1d252329 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Fri, 15 Dec 2023 19:49:42 +0000 Subject: [PATCH 216/325] Update Changelog for 17.2.3 --- changelog.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/changelog.txt b/changelog.txt index 8a56965c390e7e..c8aa6254923aab 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,21 @@ == Changelog == += 17.2.3 = + +## Changelog + +### Bug Fixes + +#### Components +- `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). + +## Contributors + +The following contributors merged PRs in this release: + +@talldan + + = 17.2.2 = This patch release fixes a WSOD which could occur in the site editor. See https://github.com/WordPress/gutenberg/pull/57035. From 9620ce01e265f0076ca3b9f32570ccea6f1098f3 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Fri, 15 Dec 2023 21:31:38 +0100 Subject: [PATCH 217/325] useSelect: only invalidate on subscribe if store changed (#57108) --- .../src/hooks/test/use-query-select.js | 18 ++-- .../data/src/components/use-select/index.js | 35 +++++-- .../src/components/use-select/test/index.js | 90 +++++++++--------- .../src/components/with-select/test/index.js | 91 ++++++------------- 4 files changed, 107 insertions(+), 127 deletions(-) diff --git a/packages/core-data/src/hooks/test/use-query-select.js b/packages/core-data/src/hooks/test/use-query-select.js index 52435f44eddfb3..2fbc6951a49361 100644 --- a/packages/core-data/src/hooks/test/use-query-select.js +++ b/packages/core-data/src/hooks/test/use-query-select.js @@ -50,11 +50,9 @@ describe( 'useQuerySelect', () => { <TestComponent keyName="foo" /> </RegistryProvider> ); - // 2 times expected - // - 1 for initial mount - // - 1 for after mount before subscription set. - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); - expect( TestComponent ).toHaveBeenCalledTimes( 2 ); + + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); + expect( TestComponent ).toHaveBeenCalledTimes( 1 ); // ensure expected state was rendered expect( screen.getByText( 'bar' ) ).toBeInTheDocument(); @@ -81,10 +79,9 @@ describe( 'useQuerySelect', () => { ); // ensure the selectors were properly memoized - expect( selectors ).toHaveLength( 4 ); + expect( selectors ).toHaveLength( 2 ); expect( selectors[ 0 ] ).toHaveProperty( 'testSelector' ); expect( selectors[ 0 ] ).toBe( selectors[ 1 ] ); - expect( selectors[ 1 ] ).toBe( selectors[ 2 ] ); // Re-render render( @@ -94,9 +91,10 @@ describe( 'useQuerySelect', () => { ); // ensure we still got the memoized results after re-rendering - expect( selectors ).toHaveLength( 8 ); - expect( selectors[ 3 ] ).toHaveProperty( 'testSelector' ); - expect( selectors[ 5 ] ).toBe( selectors[ 6 ] ); + expect( selectors ).toHaveLength( 4 ); + expect( selectors[ 2 ] ).toHaveProperty( 'testSelector' ); + expect( selectors[ 1 ] ).toBe( selectors[ 2 ] ); + expect( selectors[ 2 ] ).toBe( selectors[ 3 ] ); } ); it( 'returns the expected "response" details – no resolvers and arguments', () => { diff --git a/packages/data/src/components/use-select/index.js b/packages/data/src/components/use-select/index.js index 84cf7ed617f185..9afb4a6cfbdc65 100644 --- a/packages/data/src/components/use-select/index.js +++ b/packages/data/src/components/use-select/index.js @@ -44,6 +44,14 @@ function Store( registry, suspense ) { let lastIsAsync; let subscriber; let didWarnUnstableReference; + const storeStatesOnMount = new Map(); + + function getStoreState( name ) { + // If there's no store property (custom generic store), return an empty + // object. When comparing the state, the empty objects will cause the + // equality check to fail, setting `lastMapResultValid` to false. + return registry.stores[ name ]?.store?.getState?.() ?? {}; + } const createSubscriber = ( stores ) => { // The set of stores the `subscribe` function is supposed to subscribe to. Here it is @@ -56,12 +64,24 @@ function Store( registry, suspense ) { const activeSubscriptions = new Set(); function subscribe( listener ) { - // Invalidate the value right after subscription was created. React will - // call `getValue` after subscribing, to detect store updates that happened - // in the interval between the `getValue` call during render and creating - // the subscription, which is slightly delayed. We need to ensure that this - // second `getValue` call will compute a fresh value. - lastMapResultValid = false; + // Maybe invalidate the value right after subscription was created. + // React will call `getValue` after subscribing, to detect store + // updates that happened in the interval between the `getValue` call + // during render and creating the subscription, which is slightly + // delayed. We need to ensure that this second `getValue` call will + // compute a fresh value only if any of the store states have + // changed in the meantime. + if ( lastMapResultValid ) { + for ( const name of activeStores ) { + if ( + storeStatesOnMount.get( name ) !== getStoreState( name ) + ) { + lastMapResultValid = false; + } + } + } + + storeStatesOnMount.clear(); const onStoreChange = () => { // Invalidate the value on store update, so that a fresh value is computed. @@ -149,6 +169,9 @@ function Store( registry, suspense ) { } if ( ! subscriber ) { + for ( const name of listeningStores.current ) { + storeStatesOnMount.set( name, getStoreState( name ) ); + } subscriber = createSubscriber( listeningStores.current ); } else { subscriber.updateStores( listeningStores.current ); diff --git a/packages/data/src/components/use-select/test/index.js b/packages/data/src/components/use-select/test/index.js index 93ebf800be0a84..0b6948c6bfd8af 100644 --- a/packages/data/src/components/use-select/test/index.js +++ b/packages/data/src/components/use-select/test/index.js @@ -61,10 +61,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - // 2 selectSpy calls expected - // - 1 for initial mount - // - 1 for the subscription effect checking if value has changed - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); // Ensure expected state was rendered. @@ -93,7 +90,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); @@ -107,7 +104,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 0 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); @@ -121,7 +118,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectSpyFoo ).toHaveBeenCalledTimes( 2 ); + expect( selectSpyFoo ).toHaveBeenCalledTimes( 1 ); expect( selectSpyBar ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); @@ -163,7 +160,7 @@ describe( 'useSelect', () => { // Initial render renders only parent and subscribes the parent to store. expect( screen.getByText( 'none' ) ).toBeInTheDocument(); - expect( mapSelectParent ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectParent ).toHaveBeenCalledTimes( 1 ); expect( mapSelectChild ).toHaveBeenCalledTimes( 0 ); expect( Parent ).toHaveBeenCalledTimes( 1 ); expect( Child ).toHaveBeenCalledTimes( 0 ); @@ -174,8 +171,8 @@ describe( 'useSelect', () => { // Child was rendered and subscribed to the store, as the _second_ subscription. expect( screen.getByText( 'yes' ) ).toBeInTheDocument(); - expect( mapSelectParent ).toHaveBeenCalledTimes( 3 ); - expect( mapSelectChild ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectParent ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectChild ).toHaveBeenCalledTimes( 1 ); expect( Parent ).toHaveBeenCalledTimes( 2 ); expect( Child ).toHaveBeenCalledTimes( 1 ); @@ -187,8 +184,8 @@ describe( 'useSelect', () => { // I.e., `mapSelectChild` was called again, and state update was scheduled, we cannot // avoid that, but the state update is never executed and doesn't do a rerender. expect( screen.getByText( 'none' ) ).toBeInTheDocument(); - expect( mapSelectParent ).toHaveBeenCalledTimes( 4 ); - expect( mapSelectChild ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectParent ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectChild ).toHaveBeenCalledTimes( 2 ); expect( Parent ).toHaveBeenCalledTimes( 3 ); expect( Child ).toHaveBeenCalledTimes( 1 ); } ); @@ -217,7 +214,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( mapSelect ).toHaveBeenCalledTimes( 2 ); + expect( mapSelect ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0:0' ); @@ -226,7 +223,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-even' ).inc(); } ); - expect( mapSelect ).toHaveBeenCalledTimes( 3 ); + expect( mapSelect ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0:2' ); @@ -235,7 +232,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-odd' ).inc(); } ); - expect( mapSelect ).toHaveBeenCalledTimes( 3 ); + expect( mapSelect ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0:2' ); @@ -244,7 +241,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-main' ).inc(); } ); - expect( mapSelect ).toHaveBeenCalledTimes( 4 ); + expect( mapSelect ).toHaveBeenCalledTimes( 3 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '1:3' ); @@ -253,7 +250,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-odd' ).inc(); } ); - expect( mapSelect ).toHaveBeenCalledTimes( 5 ); + expect( mapSelect ).toHaveBeenCalledTimes( 4 ); expect( TestComponent ).toHaveBeenCalledTimes( 4 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '1:5' ); @@ -263,7 +260,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-even' ).inc(); } ); - expect( mapSelect ).toHaveBeenCalledTimes( 6 ); + expect( mapSelect ).toHaveBeenCalledTimes( 5 ); expect( TestComponent ).toHaveBeenCalledTimes( 4 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '1:5' ); } ); @@ -336,7 +333,7 @@ describe( 'useSelect', () => { expect( screen.getByRole( 'status' ).dataset.d ).toBe( JSON.stringify( valueB ) ); - expect( mapSelectSpy ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectSpy ).toHaveBeenCalledTimes( 2 ); } ); } ); @@ -368,8 +365,8 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount1 ).toHaveBeenCalledTimes( 1 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0' ); @@ -377,8 +374,8 @@ describe( 'useSelect', () => { registry.dispatch( 'store-2' ).inc(); } ); - expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1 ).toHaveBeenCalledTimes( 1 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0' ); @@ -386,8 +383,8 @@ describe( 'useSelect', () => { registry.dispatch( 'store-1' ).inc(); } ); - expect( selectCount1 ).toHaveBeenCalledTimes( 3 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 3 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '1' ); @@ -425,21 +422,21 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount1And2 ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0,0' ); act( () => { registry.dispatch( 'store-2' ).inc(); } ); - expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0,1' ); act( () => { registry.dispatch( 'store-3' ).inc(); } ); - expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0,1' ); } ); @@ -475,7 +472,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectCount1AndDep ).toHaveBeenCalledTimes( 2 ); + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count:0,dep:0' ); @@ -484,7 +481,7 @@ describe( 'useSelect', () => { setDep( 1 ); } ); - expect( selectCount1AndDep ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count:0,dep:1' ); @@ -493,7 +490,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-1' ).inc(); } ); - expect( selectCount1AndDep ).toHaveBeenCalledTimes( 4 ); + expect( selectCount1AndDep ).toHaveBeenCalledTimes( 3 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count:1,dep:1' ); @@ -525,8 +522,8 @@ describe( 'useSelect', () => { </RegistryProvider> ); - // One select on initial render, and one in `checkIfSnapshotChanged` after subscribing. - // There's a third selector call on the second render, but that one returns a memoized value. + // One select on initial render. + // There's a second selector call on the second render, but that one returns a memoized value. expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); // Initial render and second render after counter increment (which is expected to be detected). @@ -636,7 +633,7 @@ describe( 'useSelect', () => { </RegistryProvider> ); - expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount1And2 ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count1:0,count2:0' ); @@ -645,7 +642,7 @@ describe( 'useSelect', () => { registry.dispatch( 'store-2' ).inc(); } ); - expect( selectCount1And2 ).toHaveBeenCalledTimes( 3 ); + expect( selectCount1And2 ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count1:0,count2:1' ); @@ -691,7 +688,7 @@ describe( 'useSelect', () => { ); expect( selectCount1 ).toHaveBeenCalledTimes( 0 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count2:0' ); @@ -699,7 +696,7 @@ describe( 'useSelect', () => { act( () => screen.getByText( 'Toggle' ).click() ); expect( selectCount1 ).toHaveBeenCalledTimes( 1 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count1:0' ); @@ -710,7 +707,7 @@ describe( 'useSelect', () => { } ); expect( selectCount1 ).toHaveBeenCalledTimes( 2 ); - expect( selectCount2 ).toHaveBeenCalledTimes( 2 ); + expect( selectCount2 ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'count1:1' ); @@ -967,8 +964,8 @@ describe( 'useSelect', () => { </AsyncModeProvider> ); - // initial render + missed update catcher in subscribing effect - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); + // initial render + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); // Ensure expected state was rendered. @@ -979,12 +976,12 @@ describe( 'useSelect', () => { } ); // still not called right after increment - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '0' ); expect( await screen.findByText( 1 ) ).toBeInTheDocument(); - expect( selectSpy ).toHaveBeenCalledTimes( 3 ); + expect( selectSpy ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -1026,9 +1023,8 @@ describe( 'useSelect', () => { // Ensure the async update was flushed during the rerender. expect( screen.getByRole( 'status' ) ).toHaveTextContent( '1' ); - // initial render + subscription check + rerender with isAsync=false - expect( selectSpy ).toHaveBeenCalledTimes( 3 ); // initial render + rerender with isAsync=false + expect( selectSpy ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -1077,7 +1073,7 @@ describe( 'useSelect', () => { // Give the async update time to run in case it wasn't cancelled await new Promise( setImmediate ); - expect( selectA ).toHaveBeenCalledTimes( 2 ); + expect( selectA ).toHaveBeenCalledTimes( 1 ); expect( selectB ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -1120,7 +1116,7 @@ describe( 'useSelect', () => { await new Promise( setImmediate ); // only the initial render, no state updates - expect( selectSpy ).toHaveBeenCalledTimes( 2 ); + expect( selectSpy ).toHaveBeenCalledTimes( 1 ); expect( TestComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -1163,7 +1159,7 @@ describe( 'useSelect', () => { await new Promise( setImmediate ); // initial render + registry change rerender, no state updates - expect( selectSpy ).toHaveBeenCalledTimes( 4 ); + expect( selectSpy ).toHaveBeenCalledTimes( 2 ); expect( TestComponent ).toHaveBeenCalledTimes( 2 ); } ); } ); diff --git a/packages/data/src/components/with-select/test/index.js b/packages/data/src/components/with-select/test/index.js index 01afcfe5db6ad1..fcce689d03faf9 100644 --- a/packages/data/src/components/with-select/test/index.js +++ b/packages/data/src/components/with-select/test/index.js @@ -52,10 +52,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // Expected two times: - // - Once on initial render. - // - Once on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); // Wrapper is the enhanced component. @@ -107,10 +104,7 @@ describe( 'withSelect', () => { ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( mapDispatchToProps ).toHaveBeenCalledTimes( 1 ); // Simulate a click on the button. @@ -119,16 +113,14 @@ describe( 'withSelect', () => { await user.click( button ); expect( button ).toHaveTextContent( '1' ); - // 2 times = - // 1. Initial mount + // 1. Initial mount // 2. When click handler is called. expect( mapDispatchToProps ).toHaveBeenCalledTimes( 2 ); - // 4 times + // 3 times // - 1 on initial render - // - 1 on effect before subscription set. // - 1 on click triggering subscription firing. // - 1 on rerender. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); // Verifies component only renders twice. expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -253,10 +245,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); rerender( @@ -266,7 +255,7 @@ describe( 'withSelect', () => { ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( '10' ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -296,10 +285,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); rerender( @@ -308,7 +294,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -339,15 +325,12 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); await act( async () => registry.dispatch( 'demo' ).update() ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -373,10 +356,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); rerender( @@ -385,7 +365,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -411,15 +391,12 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); await act( async () => store.dispatch( { type: 'dummy' } ) ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); } ); @@ -451,10 +428,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( @@ -470,7 +444,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( JSON.stringify( { @@ -510,10 +484,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'Unknown' ); @@ -523,7 +494,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'OK' ); @@ -533,7 +504,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 3 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'Unknown' ); } ); @@ -574,11 +545,8 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 1 ); @@ -588,8 +556,8 @@ describe( 'withSelect', () => { registry.dispatch( 'childRender' ).toggleRender(); } ); - expect( childMapSelectToProps ).toHaveBeenCalledTimes( 2 ); - expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( childMapSelectToProps ).toHaveBeenCalledTimes( 1 ); + expect( parentMapSelectToProps ).toHaveBeenCalledTimes( 3 ); expect( ChildOriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( ParentOriginalComponent ).toHaveBeenCalledTimes( 2 ); } ); @@ -620,10 +588,7 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 2 times: - // - 1 on initial render - // - 1 on effect before subscription set. - expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 1 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 1 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'first' ); @@ -642,12 +607,10 @@ describe( 'withSelect', () => { </RegistryProvider> ); - // 4 times: + // 2 times: // - 1 on initial render - // - 1 on effect before subscription set. // - 1 on re-render - // - 1 on effect before new subscription set (because registry has changed) - expect( mapSelectToProps ).toHaveBeenCalledTimes( 4 ); + expect( mapSelectToProps ).toHaveBeenCalledTimes( 2 ); expect( OriginalComponent ).toHaveBeenCalledTimes( 2 ); expect( screen.getByRole( 'status' ) ).toHaveTextContent( 'second' ); } ); From 5313e6fc1dcfe94164378badb935b9445db59492 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Fri, 15 Dec 2023 22:25:39 +0100 Subject: [PATCH 218/325] Added additional explanations to attributes and supports sections (#57120) * Added additional explanations to attributes and supports sections and remove unneeded pages * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-json.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update sentence about attributes description * Fine-tune sentence about attributes * restore these parenthesis as this sentence refers only to the last part of the paragraph --------- Co-authored-by: Nick Diego <nick.diego@automattic.com> --- .../fundamentals/block-json.md | 20 +- .../block-supports-in-dynamic-blocks.md | 218 ------------------ .../block-supports-in-static-blocks.md | 92 -------- .../generate-blocks-with-wp-cli.md | 7 - ...roducing-attributes-and-editable-fields.md | 107 --------- docs/manifest.json | 24 -- docs/toc.json | 12 - 7 files changed, 14 insertions(+), 466 deletions(-) delete mode 100644 docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md delete mode 100644 docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md delete mode 100644 docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md delete mode 100644 docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md index 1f572a12598136..d06d2579969f4a 100644 --- a/docs/getting-started/fundamentals/block-json.md +++ b/docs/getting-started/fundamentals/block-json.md @@ -4,6 +4,7 @@ The `block.json` file simplifies the processs of defining and registering a bloc [![Open block.json diagram image](https://developer.wordpress.org/files/2023/11/block-json.png)](https://developer.wordpress.org/files/2023/11/block-json.png "Open block.json diagram image") + <div class="callout callout-tip"> Click <a href="https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-supports-6aa4dd">here</a> to see a full block example and check <a href="https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-supports-6aa4dd/src/block.json">its <code>block.json</code></a> </div> @@ -42,9 +43,12 @@ All these properties (`editorScript`, `editorStyle`, `script` `style`,`viewScrip The [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) property ([introduced on WordPress 6.1](https://make.wordpress.org/core/2022/10/12/block-api-changes-in-wordpress-6-1/)) sets the path of a `.php` template file that will render the markup returned to the front end. This only method will be used to return the markup for the block on request only if `$render_callback` function has not been passed to the `register_block_type` function. -## Data Storage in the Block with `attributes` +## Using `attributes` to store block data + +Block [attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#attributes) are settings or data assigned to blocks. They can determine various aspects of a block, such as its content, layout, style, and any other specific information you need to store along with your block's structure. If the user changes a block, such as modifying the font size, you need a way to persist these changes. Attributes are the solution. + +When registering a new block type, the `attributes` property of `block.json` describes the custom data the block requires and how they're stored in the database. This allows the Editor to parse these values correctly and pass the `attributes` to the block's `Edit` and `save` functions. -The [`attributes` property](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#attributes) allows a block to declare "variables" that store data or content for the block. _Example: Attributes as defined in block.json_ ```json @@ -60,7 +64,7 @@ _Example: Attributes as defined in block.json_ } }, ``` -By default `attributes` are serialized and stored in the block's delimiter but this [can be configured](https://developer.wordpress.org/news/2023/09/understanding-block-attributes/). +By default, attributes are serialized and stored in the block's delimiter, but this [can be configured](https://developer.wordpress.org/news/2023/09/understanding-block-attributes/). _Example: Atributes stored in the Markup representation of the block_ ```html @@ -69,7 +73,7 @@ _Example: Atributes stored in the Markup representation of the block_ <!-- /wp:block-development-examples/copyright-date-block-09aac3 -->x ``` -These [`attributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) are passed to the React component `Edit`(to display in the Block Editor) and the `save` function (to return the markup saved to the DB) of the block, and to any server-side render definition for the block (see `render` prop above). +These [attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) are passed to the React component `Edit`(to display in the Block Editor), and the `save` function (to return the markup saved to the database) of the block, and to any server-side render definition for the block (see the `render` property above). The `Edit` component receives exclusively the capability of updating the attributes via the [`setAttributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#setattributes) function. @@ -84,7 +88,11 @@ Check the <a href="https://developer.wordpress.org/block-editor/reference-guides ## Enable UI settings panels for the block with `supports` -The [`supports`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#supports) property allows a block to declare support for certain features, enabling users to customize specific settings (like colors or margins) from the Settings Sidebar. +Many blocks, including core blocks, offer similar customization options, whether changing the background color, text color, or adding padding customization options. + +The [`supports`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#supports) property in `block.json` allows a block to declare support for certain features, enabling users to customize specific settings (like colors or margins) from the Settings Sidebar. + +Using the available block `supports` allows you to align your block's behavior with core blocks and avoid replicating the same functionality yourself. _Example: Supports as defined in block.json_ @@ -98,7 +106,7 @@ _Example: Supports as defined in block.json_ } ``` -The use of `supports` generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. +The use of `supports` generates a set of properties that need to be manually added to the [wrapping element of the block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper/). This ensures they're properly stored as part of the block data and taken into account when generating the markup of the block that will be delivered to the front end. _Example: Supports custom settings stored in the Markup representation of the block_ diff --git a/docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md b/docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md deleted file mode 100644 index 2d852d04d28754..00000000000000 --- a/docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md +++ /dev/null @@ -1,218 +0,0 @@ -# Block Supports in dynamic blocks - -Dynamic blocks are blocks that build their structure and content on the fly when the block is rendered on the front end. - -**Note :** All the details about the creation of dynamic blocks are documented [here](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md). The below examples demonstrate the usage of `block supports` in dynamic blocks. - -A lot of blocks, including core blocks, offer similar customization options. Whether that is to change the background color, text color, or to add padding, margin customization options... - -Let's examine the scenario to enable a user to change the background color and text color of a block. - -### Without using block supports - -```jsx -import { registerBlockType } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; -import { - useBlockProps, - ColorPalette, - InspectorControls, -} from '@wordpress/block-editor'; -import { __ } from '@wordpress/i18n'; - -registerBlockType( 'gutenberg-examples/example-dynamic', { - apiVersion: 3, - title: 'Example: last post title', - icon: 'megaphone', - category: 'widgets', - attributes: { - bgColor: { type: 'string' }, - textColor: { type: 'string' }, - }, - edit: function BlockEdit( { - attributes: { bgColor, textColor }, - setAttributes, - } ) { - const blockProps = useBlockProps(); - const posts = useSelect( ( select ) => { - return select( 'core' ).getEntityRecords( 'postType', 'post', { - per_page: 1, - } ); - }, [] ); - const onChangeBGColor = ( hexColor ) => { - setAttributes( { bgColor: hexColor } ); - }; - const onChangeTextColor = ( newColor ) => { - setAttributes( { textColor: newColor } ); - }; - if ( ! posts ) { - return null; - } - return ( - <div { ...blockProps }> - <InspectorControls key="setting"> - <fieldset> - <legend className="blocks-base-control__label"> - { __( 'Background color' ) } - </legend> - <ColorPalette // Element Tag for Gutenberg standard colour selector - onChange={ onChangeBGColor } // onChange event callback - /> - </fieldset> - <fieldset> - <legend className="blocks-base-control__label"> - { __( 'Text color' ) } - </legend> - <ColorPalette // Element Tag for Gutenberg standard colour selector - onChange={ onChangeTextColor } // onChange event callback - /> - </fieldset> - </InspectorControls> - { !! posts.length && ( - <h3 - style={ { - backgroundColor: bgColor, - color: textColor, - } } - > - { posts[ 0 ].title.rendered } - </h3> - ) } - </div> - ); - }, -} ); -``` - -Because it is a dynamic block it doesn't need to override the default `save` implementation on the client. Instead, it needs a server component. The contents in the front of your site depend on the function called by the `render_callback` property of `register_block_type`. - -```php -<?php - -function gutenberg_examples_dynamic_render_callback( $block_attributes, $content ) { - $recent_posts = wp_get_recent_posts( array( - 'numberposts' => 1, - 'post_status' => 'publish', - ) ); - if ( count( $recent_posts ) === 0 ) { - return 'No posts'; - } - $post = $recent_posts[ 0 ]; - $post_id = $post['ID']; - $styles = ''; - if ( ! empty( $block_attributes['bgColor'] ) ) { - $styles .= "background-color:{$block_attributes['bgColor']};"; - } - if ( ! empty( $block_attributes['textColor'] ) ) { - $styles .= "color:{$block_attributes['textColor']};"; - } - $wrapper_attributes = get_block_wrapper_attributes(); - return sprintf( - '<h3 %1$s href="%2$s" style="%3$s">%4$s<h3>', - $wrapper_attributes, - esc_url( get_permalink( $post_id ) ), - esc_attr( $styles ), - esc_html( get_the_title( $post_id ) ) - ); -} - -function gutenberg_examples_dynamic() { - register_block_type( - 'gutenberg-examples/example-dynamic', - array( - 'api_version' => 3, - 'category' => 'widgets', - 'attributes' => array( - 'bgColor' => array( 'type' => 'string' ), - 'textColor' => array( 'type' => 'string' ), - ), - 'render_callback' => 'gutenberg_examples_dynamic_render_callback', - 'skip_inner_blocks' => true, - ) - ); -} -add_action( 'init', 'gutenberg_examples_dynamic' ); - -``` - -### With block supports - -Let's see how we can achieve the same functionality, but by using `block supports`. - -```jsx -import { registerBlockType } from '@wordpress/blocks'; -import { useSelect } from '@wordpress/data'; -import { useBlockProps } from '@wordpress/block-editor'; - -registerBlockType( 'gutenberg-examples/example-dynamic-block-supports', { - apiVersion: 3, - title: 'Example: last post title(block supports)', - icon: 'megaphone', - category: 'widgets', - edit: function BlockEdit() { - const blockProps = useBlockProps(); - const posts = useSelect( ( select ) => { - return select( 'core' ).getEntityRecords( 'postType', 'post', { - per_page: 1, - } ); - }, [] ); - if ( ! posts ) { - return null; - } - return ( - <div { ...blockProps }> - { !! posts.length && <h3>{ posts[ 0 ].title.rendered }</h3> } - </div> - ); - }, - supports: { color: true }, -} ); -``` - -And the server side part becomes: - -```php -<?php -function gutenberg_examples_dynamic_block_supports_render_callback( $block_attributes, $content ) { - $recent_posts = wp_get_recent_posts( array( - 'numberposts' => 1, - 'post_status' => 'publish', - ) ); - if ( count( $recent_posts ) === 0 ) { - return 'No posts'; - } - $post = $recent_posts[ 0 ]; - $post_id = $post['ID']; - $wrapper_attributes = get_block_wrapper_attributes(); - return sprintf( - '<h3 %1$s href="%2$s">%3$s<h3>', - $wrapper_attributes, - esc_url( get_permalink( $post_id ) ), - esc_html( get_the_title( $post_id ) ) - ); -} - -function gutenberg_examples_dynamic_block_supports() { - register_block_type( - 'gutenberg-examples/example-dynamic-block-supports', - array( - 'api_version' => 3, - 'category' => 'widgets', - 'supports' => array( 'color' => true ), - 'render_callback' => 'gutenberg_examples_dynamic_block_supports_render_callback', - 'skip_inner_blocks' => true, - ) - ); -} -add_action( 'init', 'gutenberg_examples_dynamic_block_supports' ); - -``` - -And that's it, the addition of the "supports" key above, will automatically make the following changes to the block: - -- Add a `style` attribute to the block to store the link, text and background colors. -- Add a "Colors" panel to the sidebar of the block editor to allow users to tweak the text, link and background colors. -- Automatically use the `theme.json` config: allow disabling colors, inherit palettes... -- Automatically inject the right styles and apply them to the block wrapper when the user make changes to the colors. - -To learn more about the block supports and see all the available properties that you can enable for your own blocks, please refer to [the supports documentation](/docs/reference-guides/block-api/block-supports.md). diff --git a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md b/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md deleted file mode 100644 index 47fa3a86b75eb9..00000000000000 --- a/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md +++ /dev/null @@ -1,92 +0,0 @@ -# Block Supports - -A lot of blocks, including core blocks, offer similar customization options. Whether that is to change the background color, text color, or to add padding, margin customization options... - -To avoid duplicating the same logic over and over in your blocks and to align the behavior of your block with core blocks, you can make use of the different `supports` properties. - -Let's take the block we wrote in the previous chapter (example 3) and with just a single line of code, add support for text, link and background color customizations. - -Here's the exact same code we used to register the block previously. - - -```jsx -import { registerBlockType } from '@wordpress/blocks'; -import { useBlockProps, RichText } from '@wordpress/block-editor'; - -registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { - apiVersion: 3, - title: 'Example: Basic with block supports', - icon: 'universal-access-alt', - category: 'design', - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: ( props ) => { - const { - attributes: { content }, - setAttributes, - className, - } = props; - const blockProps = useBlockProps(); - const onChangeContent = ( newContent ) => { - setAttributes( { content: newContent } ); - }; - return ( - <RichText - { ...blockProps } - tagName="p" - onChange={ onChangeContent } - value={ content } - /> - ); - }, - save: ( props ) => { - const blockProps = useBlockProps.save(); - return ( - <RichText.Content - { ...blockProps } - tagName="p" - value={ props.attributes.content } - /> - ); - }, -} ); -``` - -Now, let's alter the block.json file for that block, and add the supports key. (If you're not using a block.json file, you can also add the key to the `registerBlockType` function call) - -```json -{ - "apiVersion": 3, - "name": "gutenberg-examples/example-03-editable-esnext", - "title": "Example: Basic with block supports", - "icon": "universal-access-alt", - "category": "layout", - "editorScript": "file:./build/index.js", - "supports": { - "color": { - "text": true, - "background": true, - "link": true - } - } -} -``` - -And that's it, the addition of the "supports" key above, will automatically make the following changes to the block: - - - Add a `style` attribute to the block to store the link, text and background colors. - - Add a "Colors" panel to the sidebar of the block editor to allow users to tweak the text, link and background colors. - - Automatically use the `theme.json` config: allow disabling colors, inherit palettes... - - Automatically inject the right styles and apply them to the block wrapper when the user make changes to the colors. - -To learn more about the block supports and see all the available properties that you can enable for your own blocks, please refer to [the supports documentation](/docs/reference-guides/block-api/block-supports.md). diff --git a/docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md b/docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md deleted file mode 100644 index 891f8e7d4e1b80..00000000000000 --- a/docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md +++ /dev/null @@ -1,7 +0,0 @@ -# Generate Blocks with WP-CLI - -## WARNING - -**Deprecated:** It is no longer recommended to use WP-CLI or create-guten-block to generate block scaffolding. - -The official script to generate a block is the new [@wordpress/create-block](/packages/create-block/README.md) package. This package follows the new block directory guidelines, and creates the proper block, environment, and standards set by the project. See the new [Create a Block tutorial](/docs/getting-started/create-block/README.md) for a complete walk-through. diff --git a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md b/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md deleted file mode 100644 index 3d8e10cae7ab2a..00000000000000 --- a/docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md +++ /dev/null @@ -1,107 +0,0 @@ -# Introducing Attributes and Editable Fields - -The example blocks so far are still not very interesting because they lack options to customize the appearance of the message. In this section, we will implement a RichText field allowing the user to specify their own message. Before doing so, it's important to understand how the state of a block (its "attributes") is maintained and changed over time. - -## Attributes - -Until now, the `edit` and `save` functions have returned a simple representation of a paragraph element. We also learned how these functions are responsible for _describing_ the structure of the block's appearance. If the user changes a block, this structure may need to change. To achieve this, the state of a block is maintained throughout the editing session as a plain JavaScript object, and when an update occurs, the `edit` function is invoked again. Put another way: **the output of a block is a function of its attributes**. - -One challenge of maintaining the representation of a block as a JavaScript object is that we must be able to extract this object again from the saved content of a post. This is achieved with the block type's `attributes` property: - -```js - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, -``` - -When registering a new block type, the `attributes` property describes the shape of the attributes object you'd like to receive in the `edit` and `save` functions. Each value is a [source function](/docs/reference-guides/block-api/block-attributes.md) to find the desired value from the markup of the block. - -In the code snippet above, when loading the editor, the `content` value will be extracted from the HTML of the paragraph element in the saved post's markup. - -## Components and the `RichText` Component - -Earlier examples used the `createElement` function to create DOM nodes, but it's also possible to encapsulate this behavior into "components". This abstraction helps you share common behaviors and hide complexity in self-contained units. - -There are a number of [components available](/docs/reference-guides/packages/packages-editor.md#components) to use in implementing your blocks. You can see one such component in the code below: the [`RichText` component](/docs/reference-guides/richtext.md) is part of the `wp-block-editor` package. - -The `RichText` component can be considered as a super-powered `textarea` element, enabling rich content editing including bold, italics, hyperlinks, etc. - -To use the `RichText` component, and using ES5 code, remember to add `wp-block-editor` to the dependency array of registered script handles when calling `wp_register_script`. - -```php -// automatically load dependencies and version -$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php'); - -wp_register_script( - 'gutenberg-examples-03-esnext', - plugins_url( 'build/index.js', __FILE__ ), - $asset_file['dependencies'], - $asset_file['version'] -); -``` - -Do not forget to also update the `editor_script` handle in `register_block_type` to `gutenberg-examples-03-esnext`. - -Implementing this behavior as a component enables you as the block implementer to be much more granular about editable fields. Your block may not need `RichText` at all, or it may need many independent `RichText` elements, each operating on a subset of the overall block state. - -Because `RichText` allows for nested nodes, you'll most often use it in conjunction with the `html` attribute source when extracting the value from saved content. You'll also use `RichText.Content` in the `save` function to output RichText values. - -Here is the complete block definition for Example 03. - - -```jsx -import { registerBlockType } from '@wordpress/blocks'; -import { useBlockProps, RichText } from '@wordpress/block-editor'; - -registerBlockType( 'gutenberg-examples/example-03-editable-esnext', { - apiVersion: 3, - title: 'Example: Editable (esnext)', - icon: 'universal-access-alt', - category: 'design', - attributes: { - content: { - type: 'string', - source: 'html', - selector: 'p', - }, - }, - example: { - attributes: { - content: 'Hello World', - }, - }, - edit: ( props ) => { - const { - attributes: { content }, - setAttributes, - className, - } = props; - const blockProps = useBlockProps(); - const onChangeContent = ( newContent ) => { - setAttributes( { content: newContent } ); - }; - return ( - <RichText - { ...blockProps } - tagName="p" - onChange={ onChangeContent } - value={ content } - /> - ); - }, - save: ( props ) => { - const blockProps = useBlockProps.save(); - return ( - <RichText.Content - { ...blockProps } - tagName="p" - value={ props.attributes.content } - /> - ); - }, -} ); -``` diff --git a/docs/manifest.json b/docs/manifest.json index b8939951d71837..b91732962e0a91 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -173,42 +173,18 @@ "markdown_source": "../docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md", "parent": "block-tutorial" }, - { - "title": "Introducing Attributes and Editable Fields", - "slug": "introducing-attributes-and-editable-fields", - "markdown_source": "../docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md", - "parent": "block-tutorial" - }, { "title": "Block Controls: Block Toolbar and Settings Sidebar", "slug": "block-controls-toolbar-and-sidebar", "markdown_source": "../docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md", "parent": "block-tutorial" }, - { - "title": "Block Supports", - "slug": "block-supports-in-static-blocks", - "markdown_source": "../docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md", - "parent": "block-tutorial" - }, { "title": "Creating dynamic blocks", "slug": "creating-dynamic-blocks", "markdown_source": "../docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md", "parent": "block-tutorial" }, - { - "title": "Block Supports in dynamic blocks", - "slug": "block-supports-in-dynamic-blocks", - "markdown_source": "../docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md", - "parent": "block-tutorial" - }, - { - "title": "Generate Blocks with WP-CLI", - "slug": "generate-blocks-with-wp-cli", - "markdown_source": "../docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md", - "parent": "block-tutorial" - }, { "title": "Nested Blocks: Using InnerBlocks", "slug": "nested-blocks-inner-blocks", diff --git a/docs/toc.json b/docs/toc.json index 91017ce69643c3..34be591647c174 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -80,24 +80,12 @@ { "docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md": [] }, - { - "docs/how-to-guides/block-tutorial/introducing-attributes-and-editable-fields.md": [] - }, { "docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md": [] }, - { - "docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md": [] - }, { "docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md": [] }, - { - "docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md": [] - }, - { - "docs/how-to-guides/block-tutorial/generate-blocks-with-wp-cli.md": [] - }, { "docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md": [] }, From 1ac5c23c759753f36f5c0f29f177f381cf245eec Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Fri, 15 Dec 2023 15:49:31 -0600 Subject: [PATCH 219/325] Docs: Add new "Build your first block" tutorial to the Getting Started section of the BEH (#56931) * Initial draft of the new block tutorial. * Fix link. * Fix grammar and address feedback. * Add reference to Block Development Examples repo. --- docs/getting-started/create-block/README.md | 21 - .../create-block/attributes.md | 87 -- .../create-block/author-experience.md | 147 --- .../create-block/block-anatomy.md | 80 -- .../create-block/block-code.md | 46 - .../getting-started/create-block/finishing.md | 27 - .../submitting-to-block-directory.md | 104 -- .../getting-started/create-block/wp-plugin.md | 146 --- docs/getting-started/tutorial.md | 1003 +++++++++++++++++ docs/manifest.json | 48 +- docs/toc.json | 26 +- 11 files changed, 1007 insertions(+), 728 deletions(-) delete mode 100644 docs/getting-started/create-block/README.md delete mode 100644 docs/getting-started/create-block/attributes.md delete mode 100644 docs/getting-started/create-block/author-experience.md delete mode 100644 docs/getting-started/create-block/block-anatomy.md delete mode 100644 docs/getting-started/create-block/block-code.md delete mode 100644 docs/getting-started/create-block/finishing.md delete mode 100644 docs/getting-started/create-block/submitting-to-block-directory.md delete mode 100644 docs/getting-started/create-block/wp-plugin.md create mode 100644 docs/getting-started/tutorial.md diff --git a/docs/getting-started/create-block/README.md b/docs/getting-started/create-block/README.md deleted file mode 100644 index 22a28560c76a81..00000000000000 --- a/docs/getting-started/create-block/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# Create a Block Tutorial - -Let's get you started creating your first block for the WordPress Block Editor. We will create a simple block that allows the user to type a message and style it. - -The tutorial includes setting up your development environment, tools, and getting comfortable with the new development model. If you are already comfortable, try the quick start below, otherwise step through whatever part of the tutorial you need. - -## Prerequisites - -The first thing you need is a development environment and tools. This includes setting up your WordPress environment, Node, NPM, and your code editor. If you need help, see the [setting up your development environment documentation](/docs/getting-started/devenv/README.md). - -## Table of Contents - -The create a block tutorials breaks down to the following sections. - -1. [WordPress Plugin](/docs/getting-started/create-block/wp-plugin.md) -2. [Anatomy of a Gutenberg Block ](/docs/getting-started/create-block/block-anatomy.md) -3. [Block Attributes](/docs/getting-started/create-block/attributes.md) -4. [Code Implementation](/docs/getting-started/create-block/block-code.md) -5. [Authoring Experience](/docs/getting-started/create-block/author-experience.md) -6. [Finishing Touches](/docs/getting-started/create-block/finishing.md) -7. [Share your Block with the World](/docs/getting-started/create-block/submitting-to-block-directory.md) diff --git a/docs/getting-started/create-block/attributes.md b/docs/getting-started/create-block/attributes.md deleted file mode 100644 index 02a55f380dcee1..00000000000000 --- a/docs/getting-started/create-block/attributes.md +++ /dev/null @@ -1,87 +0,0 @@ -# Block Attributes - -Attributes are the way a block stores data, they define how a block is parsed to extract data from the saved content. - -For this block tutorial, we want to allow the user to type in a message that we will display stylized in the published post. So, we need to add a **message** attribute that will hold the user message. The following code defines a **message** attribute; the attribute type is a string; the source is the text from the selector which is a `div` tag. - -```json -"attributes": { - "message": { - "type": "string", - "source": "text", - "selector": "div", - "default": "" - } -}, -``` - -Add this to the `src/block.json` file. The `attributes` are at the same level as the _name_ and _title_ fields. - -When the block loads it will look at the saved content for the block, look for the div tag, take the text portion, and store the content in an `attributes.message` variable. - -Note: The text portion is equivalent to `innerText` attribute of a DOM element. For more details and other examples see the [Block Attributes documentation](/docs/reference-guides/block-api/block-attributes.md). - -## Edit and Save - -The **attributes** are passed to both the `edit` and `save` functions. The **setAttributes** function is also passed, but only to the `edit` function. The **setAttributes** function is used to set the values. Additional parameters are also passed in to the `edit` and `save` functions, see [the edit/save documentation](/docs/reference-guides/block-api/block-edit-save.md) for more details. - -The `attributes` is a JavaScript object containing the values of each attribute, or default values if defined. The `setAttributes` is a function to update an attribute. - -```js -export default function Edit( { attributes, setAttributes } ) { - // ... -} -``` - -## TextControl Component - -For our example block, the component we are going to use is the **TextControl** component, it is similar to an HTML text input field. You can see [documentation for TextControl component](/packages/components/src/text-control/README.md). You can browse an [interactive set of components in this Storybook](https://wordpress.github.io/gutenberg/). - -The component is added similar to an HTML tag, setting a label, the `value` is set to the `attributes.message` and the `onChange` function uses the `setAttributes` to update the message attribute value. - -The save function will simply write the `attributes.message` as a div tag since that is how we defined it to be parsed. - -OPTIONAL: For IDE support (code completion and hints), you can install the `@wordpress/components` module which is where the TextControl component is imported from. This install command is optional since the build process automatically detects `@wordpress/*` imports and specifies as dependencies in the assets file. - -```shell -npm install @wordpress/components --save -``` - -Update the edit.js and save.js files to the following, replacing the existing functions. - -**edit.js** file: - -```js -import { __ } from '@wordpress/i18n'; -import { useBlockProps } from '@wordpress/block-editor'; -import { TextControl } from '@wordpress/components'; -import './editor.scss'; - -export default function Edit( { attributes, setAttributes } ) { - return ( - <div { ...useBlockProps() }> - <TextControl - label={ __( 'Message', 'gutenpride' ) } - value={ attributes.message } - onChange={ ( val ) => setAttributes( { message: val } ) } - /> - </div> - ); -} -``` - -**save.js** file: - -```jsx -import { useBlockProps } from '@wordpress/block-editor'; - -export default function save( { attributes } ) { - const blockProps = useBlockProps.save(); - return <div { ...blockProps }>{ attributes.message }</div>; -} -``` - -If you have previously run `npm run start`, and the script is still running, you can reload the editor now and add the block to test. -Otherwise rebuild the block using `npm run build`, reload the editor and add the block. Type a message in the editor, save, and view it in the post. - -Next Section: [Code Implementation](/docs/getting-started/create-block/block-code.md) diff --git a/docs/getting-started/create-block/author-experience.md b/docs/getting-started/create-block/author-experience.md deleted file mode 100644 index a52ea808f69658..00000000000000 --- a/docs/getting-started/create-block/author-experience.md +++ /dev/null @@ -1,147 +0,0 @@ -# Authoring Experience - -## Background - -One of the primary tenets of Gutenberg as a WYSIWYG editor is that what you see in the editor should be as close as possible to what you get when published. Keep this in mind when building blocks. - -## Placeholder - -The state when a block has been inserted, but no data has been entered yet, is called a placeholder. There is a `Placeholder` component built that gives us a standard look. You can see example placeholders in use with the image and embed blocks. - -To use the Placeholder, wrap the `<TextControl>` component so it becomes a child element of the `<Placeholder>` component. Try it out in your code. After updating, you might have something like: - -```jsx -import { useBlockProps } from '@wordpress/block-editor'; -import { Placeholder, TextControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; - -export default function Edit( { attributes, className, setAttributes } ) { - return ( - <div { ...useBlockProps() }> - <Placeholder - label={ __( 'Gutenpride Block', 'gutenpride' ) } - instructions={ __( 'Add your message', 'gutenpride' ) } - > - <TextControl - value={ attributes.message } - onChange={ ( val ) => setAttributes( { message: val } ) } - /> - </Placeholder> - </div> - ); -} -``` - -## isSelected Ternary Function - -The placeholder looks ok, for a simple text message it may or may not be what you are looking for. However, the placeholder can be useful if you are replacing the block after what is typed in, similar to the embed blocks. - -For this we can use a ternary function, to display content based on a value being set or not. A ternary function is an inline if-else statement, using the syntax: - -```js -clause ? doIfTrue : doIfFalse; -``` - -This can be used inside a block to control what shows when a parameter is set or not. A simple case that displays a `message` if set, otherwise show the form element: - -```jsx -return ( - <div { ...useBlockProps() }> - { attributes.message ? ( - <div>Message: { attributes.message }</div> - ) : ( - <div> - <p>No Message.</p> - <TextControl - value={ attributes.message } - onChange={ ( val ) => setAttributes( { message: val } ) } - /> - </div> - ) } - </div> -); -``` - -There is a problem with the above, if we only use the `attributes.message` check, as soon as we type in the text field it would disappear since the message would then be set to a value. So we need to pair with an additional `isSelected` parameter. - -The `isSelected` parameter is passed in to the `edit` function and is set to true if the block is selected in the editor (currently editing) otherwise set to false (editing elsewhere). - -Using that parameter, we can use the logic: - -```js -attributes.message && ! isSelected; -``` - -If the message is set and `!isSelected`, meaning we are not editing the block, the focus is elsewhere, then display the message not the text field. - -All of this combined together, here's what the edit function looks like: - -```jsx -import { Placeholder, TextControl } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useBlockProps } from "@wordpress/block-editor"; - -export default function Edit( { attributes, isSelected, setAttributes } ) { - return ( - <div { ...useBlockProps() }> - { attributes.message && ! isSelected ? ( - <div>{ attributes.message }</div> - ) : ( - <Placeholder - label="Gutenpride Block" - instructions="Add your message" - > - <TextControl - value={ attributes.message } - onChange={ ( val ) => - setAttributes( { message: val } ) - } - /> - </Placeholder> - ) } - </div> - ); -} -``` - -With that in place, rebuild and reload and when you are not editing the message is displayed as it would be for the view, when you click into the block you see the text field. - -## A Better Solution - -The switching between a Placeholder and input control works well with a visual element like an image or video, but for the text example in this block we can do better. - -The simpler and better solution is to modify the `src/editor.scss` to include the proper stylized text while typing. - -Update `src/editor.scss` to: - -```scss -.wp-block-create-block-gutenpride input[type='text'] { - font-family: Gilbert, sans-serif; - font-size: 64px; - color: inherit; - background: inherit; - border: 0; -} -``` - -The edit function can simply be: - -```jsx -import { TextControl } from '@wordpress/components'; -import { useBlockProps } from '@wordpress/block-editor'; - -import './editor.scss'; - -export default function Edit( { attributes, setAttributes } ) { - const blockProps = useBlockProps(); - return ( - <TextControl - { ...blockProps } - value={ attributes.message } - onChange={ ( val ) => setAttributes( { message: val } ) } - /> - ); -} -``` - -Next Section: [Finishing Touches](/docs/getting-started/create-block/finishing.md) diff --git a/docs/getting-started/create-block/block-anatomy.md b/docs/getting-started/create-block/block-anatomy.md deleted file mode 100644 index edd8b5200ae8f7..00000000000000 --- a/docs/getting-started/create-block/block-anatomy.md +++ /dev/null @@ -1,80 +0,0 @@ -# Anatomy of a Block - -At its simplest, a block in the WordPress block editor is a JSON object with a specific set of properties. - -<div class="callout callout-info"> -<strong>Note:</strong> Block development uses ESNext syntax, this refers to the latest JavaScript standard. If this is unfamiliar, review the <a href="https://developer.wordpress.org/block-editor/how-to-guides/javascript/esnext-js/">ESNext syntax documentation</a> to familiarize yourself with the newer syntax. -</div> - -The javascript part is done in the `src/index.js` file. - -```js -import { registerBlockType } from '@wordpress/blocks'; - -import './style.scss'; - -import Edit from './edit'; -import save from './save'; -import metadata from './block.json'; - -registerBlockType( metadata.name, { - /** - * @see ./edit.js - */ - edit: Edit, - /** - * @see ./save.js - */ - save, -} ); -``` - -The first parameter in the **registerBlockType** function is the block name, this should match exactly to the `name` property in the `block.json` file. By importing the metadata from `block.json` and referencing the `name` property in the first parameter we ensure that they will match, and continue to match even if the name is subsequently changed in `block.json`. - -The second parameter to the function is the block object. See the [block registration documentation](/docs/reference-guides/block-api/block-registration.md) for full details. - -Two common object properties are **edit** and **save**, these are the key parts of a block. Both properties are functions that are included via the import above. - -The results of the edit function is what the editor will render to the editor page when the block is inserted. - -The results of the save function is what the editor will insert into the **post_content** field when the post is saved. The post_content field is the field in the **wp_posts** table in the WordPress database that is used to store the content of the post. - -Most of the properties are set in the `src/block.json` file. - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 3, - "name": "create-block/gutenpride", - "version": "0.1.0", - "title": "Gutenpride", - "category": "text", - "icon": "flag", - "description": "A Gutenberg block to show your pride! This block enables you to type text and style it with the color font Gilbert from Type with Pride.", - "supports": { - "html": false - }, - "textdomain": "gutenpride", - "editorScript": "file:./index.js", - "editorStyle": "file:./index.css", - "style": "file:./style-index.css" -} -``` - -The **title** is the title of the block shown in the Inserter and in other areas of the editor. - -The **icon** is the icon shown in the Inserter. The icon property expects any Dashicon name as a string, see [list of available icons](https://developer.wordpress.org/resource/dashicons/). You can also provide an SVG object, but for now it's easiest to just pick a Dashicon name. - -The **category** specified is a string and must be one of: "text, media, design, widgets, theme, or embed". You can create your own custom category name, [see documentation for details](/docs/reference-guides/filters/block-filters.md#managing-block-categories). - -## Internationalization - -If you look at the generated `src/save.js` file, the block title and description are wrapped in a function that looks like this: - -```js -__( 'Gutenpride – hello from the saved content!', 'gutenpride' ); -``` - -This is an internationalization wrapper that allows for the string "Gutenpride" to be translated. The second parameter, "gutenpride" is called the text domain and gives context for where the string is from. The JavaScript internationalization, often abbreviated i18n, matches the core WordPress internationalization process. See the [Internationalization in Plugin Developer Handbook](https://developer.wordpress.org/plugins/internationalization/) for more details. - -Next Section: [Block Attributes](/docs/getting-started/create-block/attributes.md) diff --git a/docs/getting-started/create-block/block-code.md b/docs/getting-started/create-block/block-code.md deleted file mode 100644 index 11f478772c6dfe..00000000000000 --- a/docs/getting-started/create-block/block-code.md +++ /dev/null @@ -1,46 +0,0 @@ -# Code Implementation - -The basic block is in place, the next step is to add styles to the block. Feel free to style and adjust for your own preference, the main lesson is showing how to create and load external resources. For this example we're going to load the colorized gilbert font from [Type with Pride](https://www.typewithpride.com/). - -Note: The color may not work with all browsers until they support the proper color font properly, but the font itself still loads and styles. See [colorfonts.wtf](https://www.colorfonts.wtf/) for browser support and details on color fonts. - -## Load Font File - -Download and extract the font from the Type with Pride site, and copy it in the `assets` directory of your plugin naming it `gilbert-color.otf`. To load the font file, we need to add CSS using standard WordPress enqueue, [see Including CSS & JavaScript documentation](https://developer.wordpress.org/themes/basics/including-css-javascript/). - -In the `gutenpride.php` file, the enqueue process is already setup from the generated script, so `build/index.css` and `build/style-index.css` files are loaded using: - -```php -function create_block_gutenpride_block_init() { - register_block_type( __DIR__ . '/build' ); -} -add_action( 'init', 'create_block_gutenpride_block_init' ); -``` - -This function checks the `build/block.json` file for JS and CSS files, and will pass them on to [enqueue](https://developer.wordpress.org/themes/basics/including-css-javascript/) these files, so they are loaded on the appropriate pages. - -The `build/index.css` is compiled from `src/editor.scss` and loads only within the editor, and after the `style-index.css`. -The `build/style-index.css` is compiled from `src/style.scss` and loads in both the editor and front-end — published post view. - -## Add CSS Style for Block - -We only need to add the style to `build/style-index.css` since it will show while editing and viewing the post. Edit the `src/style.scss` to add the following. - -Note: the block classname is prefixed with `wp-block`. The `create-block/gutenpride` is converted to the classname `.wp-block-create-block-gutenpride`. - -```scss -@font-face { - font-family: Gilbert; - src: url( ../assets/gilbert-color.otf ); - font-weight: 700; -} - -.wp-block-create-block-gutenpride { - font-family: Gilbert, sans-serif; - font-size: 64px; -} -``` - -After updating, rebuild the block using `npm run build` then reload the post and refresh the browser. If you are using a browser that supports color fonts (Firefox) then you will see it styled. - -Next Section: [Authoring Experience](/docs/getting-started/create-block/author-experience.md) diff --git a/docs/getting-started/create-block/finishing.md b/docs/getting-started/create-block/finishing.md deleted file mode 100644 index cd619a69207ca2..00000000000000 --- a/docs/getting-started/create-block/finishing.md +++ /dev/null @@ -1,27 +0,0 @@ -# Finishing Touches - -This tutorial covers general concepts and structure for creating basic blocks. - -## Additional Components - -The block editor provides a [components package](/packages/components/README.md) which contains numerous prebuilt components you can use to build your block. - -You can visually browse the components and what their implementation looks like using the Storybook tool published at [https://wordpress.github.io/gutenberg](https://wordpress.github.io/gutenberg). - -## Additional Tutorials - -The **RichText component** allows for creating a richer input besides plain text, allowing for bold, italic, links, and other inline formatting. See the [RichText Reference](/docs/reference-guides/richtext.md) for documentation using this component. - -The InspectorPanel (the settings on the right for a block) and Block Controls (toolbar controls) have a standard way to be implemented. See the [Block controls tutorial](/docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md) for additional information. - -The [Sidebar tutorial](/docs/how-to-guides/plugin-sidebar-0.md) is a good resource on how to create a sidebar for your plugin. - -Nested blocks, a block that contains additional blocks, is a common pattern used by various blocks such as Columns, Cover, and Social Links. The **InnerBlocks component** enables this functionality, see the [Using InnerBlocks documentation](/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md). - -## How did they do that - -One of the best sources for information and reference is the Block Editor itself, all the core blocks are built the same way. A good way to learn how things are done is to find a core block code that does something close to what you are interested in and then using the same approach for your own block. - -All core blocks source are in the [block library package on GitHub](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src). - -Next Section: [Share your Block with the World](/docs/getting-started/create-block/submitting-to-block-directory.md) diff --git a/docs/getting-started/create-block/submitting-to-block-directory.md b/docs/getting-started/create-block/submitting-to-block-directory.md deleted file mode 100644 index f898a9d52caaa0..00000000000000 --- a/docs/getting-started/create-block/submitting-to-block-directory.md +++ /dev/null @@ -1,104 +0,0 @@ -# Share your Block with the World - -So you've created an awesome block? Care to share? - -**Contents**: - -1. Help users understand your block -2. Analyze your plugin -3. Zip & Submit - -## Step 1: Help users understand your block - -It is important to the Block Directory and our end users to provide easy to understand information on how your block was created. - -**Guidelines**: - -- Name your block based on what it does -- Clearly describe your block -- Add Keywords for all contexts -- Choose the right category - -### Name your block based on what it does - -Users typically search the Block Directory within the Block Editor and do so in the context of a task. For example, when building their post, a user may search the Block Directory for an “image gallery”. Naming your block accordingly will help the Block Directory surface it when it's needed. - -**Not So Good**: WebTeam5 Image Works -**Good**: Responsive Image Slider by WebTeam5 - -**Question: What happens when there are multiple blocks with similar names?** -Try your best to make your block's name functional and unique to make it stand out. Look for applicable synonyms or include a prefix if necessary. - -### Clearly describe your block - -The description really helps to communicate what your block does.The quicker a user understands how your block will help them, the more likely it is a user will use your block. Users will be reading your block's description within the Block Editor where space can be limited. Try to keep it short and concise. - -**Not So Good**: The best way to show images on your website using jQuery and CSS. -**Good**: A responsive image gallery block. - -**Tip**: It’s not about marketing your block, in fact we want to avoid marketing in blocks. You can read more about it in the [plugin guidelines]. Stick to being as clear as you can. The Block Directory will provide metrics to let users know how awesome your block is! - -### Add Keywords for broader context - -Keywords add extra context to your block and make it more likely to be found in the inserter. - -Examples for an Image Slider block: - -- slider -- carousel -- gallery - -[Read more about keywords.](/docs/reference-guides/block-api/block-metadata.md#keywords) - -### Choose the right category - -The Block Editor allows you to indicate the category your block belongs in, making it easier for users to locate your block in the menu. - -**Possible Values**: - -- text -- media -- design -- widgets -- theme -- embed - -[Read more about categories.](/docs/reference-guides/block-api/block-metadata.md#category) - -Wondering where to input all this information? Read the next section :) - -## Step 2: Analyze your plugin - -Each block in your plugin should have a corresponding `block.json` file with the [block metadata](/docs/reference-guides/block-api/block-metadata.md). This file provides the Block Directory important information about your block. Along with being the place to store contextual information about your block like the: `name`, `description`, `keywords` and `category`, the `block.json` file stores the location of your block’s files. - -Block plugins submitted to the Block Directory can contain multiple blocks only if they are children of a single parent/ancestor. There should only be one main block. For example, a list block can contain list-item blocks. Children blocks must set the `parent` property in their `block.json` file. - -Double check that the following is true for your block: - -- `editorScript` is pointing to the JavaScript bundle that includes all the code used in the **editor**. -- `editorStyle` is pointing to the CSS bundle that includes all the css used in the **editor**. -- `script` is pointing to the JavaScript bundle that includes all the code used on the **website**. -- `style` is pointing to the CSS bundle that includes all the code used on the **website**. - -We encourage the separation of code by using both editorScript/editorStyle and script/style files listed in your block.json to keep the backend and frontend interfaces running smoothly. Even though only one file is required. - -Here is an example of a basic block.json file. - -```json -{ - "name": "plugin-slug/image-slider", - "title": "Responsive Image Slider", - "description": "A responsive and easy to use image gallery block.", - "keywords": [ "slider", "carousel", "gallery" ], - "category": "media", - "editorScript": "file:./dist/editor.js" -} -``` - -The `block.json` file also contains other important properties. Take a look at an [example block.json](/docs/reference-guides/block-api/block-metadata.md) for additional properties to be included in the block.json file. - -## Step 3: Zip & Submit - -The community is thankful for your contribution. It is time to submit your plugin. - -Go through [the block guidelines](https://github.com/WordPress/wporg-plugin-guidelines/blob/block-guidelines/blocks.md). Create a zip file of your block and go to the [block plugin validator](https://wordpress.org/plugins/developers/block-plugin-validator/) and upload your plugin. diff --git a/docs/getting-started/create-block/wp-plugin.md b/docs/getting-started/create-block/wp-plugin.md deleted file mode 100644 index 8d6e618dad40ca..00000000000000 --- a/docs/getting-started/create-block/wp-plugin.md +++ /dev/null @@ -1,146 +0,0 @@ -# WordPress Plugin - -A block is added to the block editor using a WordPress plugin. You can create your own plugin, and after installing and activating the plugin use the block. Let's first look at what makes up a WordPress plugin. - -## Plugin Details - -A WordPress plugin is a set of files within the site's `wp-content/plugins` directory. For our tutorial, we will use the `@wordpress/create-block` package to generate the necessary plugin files. - -### Switch to Working Directory - -(1A) If you do not plan to use `wp-env`, change to your local WordPress plugin directory. For example in Local it is: `~\Local Sites\mywp\app\public\wp-content\plugins` - --or- - -(1B) If using `wp-env start`, you can work from any directory for your project; `wp-env` will map it as a plugin directory for your site. - -### Generate Plugin Files - -(2) Once in the right directory for your environment, the next step is to run the following command to generate plugin files: - -```sh -npx @wordpress/create-block gutenpride -cd gutenpride -``` - -A new directory `gutenpride` is created with all the necessary plugin files. This tutorial will walk through and explain the plugin files, please explore and become familiar with them also. - -The main plugin file created is the PHP file `gutenpride.php`, at the top of this file is the Plugin Header comment block that defines the plugin. - -```php -/** - * Plugin Name: Gutenpride - * Description: Example static block scaffolded with Create Block tool. - * Requires at least: 5.8 - * Requires PHP: 7.0 - * Version: 0.1.0 - * Author: The WordPress Contributors - * License: GPL-2.0-or-later - * License URI: https://www.gnu.org/licenses/gpl-2.0.html - * Text Domain: gutenpride - * - * @package create-block - */ -``` - -### Start WordPress - -Let's confirm the plugin is loaded and working. - -(3A) If you are using Local, or other environment confirm your WordPress site is started. - --or- - -(3B) If you are using `wp-env`, see [Development Environment setup](/docs/getting-started/devenv/README.md), then you should now run from inside the `gutenpride` directory: - -```sh -wp-env start -``` - -This will start your local WordPress site and use the current directory as your plugin directory. In your browser, go to http://localhost:8888/wp-admin/ and login, the default username is "admin" and password is "password", no quotes. - -### Confirm Plugin Installed - -The generated plugin should now be listed on the Plugins admin page in your WordPress install. Switch WordPress to the plugins page and activate. - -For more on creating a WordPress plugin see [Plugin Basics](https://developer.wordpress.org/plugins/plugin-basics/), and [Plugin Header requirements](https://developer.wordpress.org/plugins/plugin-basics/header-requirements/) for explanation and additional fields you can include in your plugin header. - -## package.json - -The `package.json` file defines the JavaScript properties for your project. This is a standard file used by NPM for defining properties and scripts it can run, the file and process is not specific to WordPress. - -A `package.json` file was created with the create script, this defines the dependencies and scripts needed. You can install dependencies. The only initial dependency is the `@wordpress/scripts` package that bundles the tools and configurations needed to build blocks. - -In `package.json`, there is a `scripts` property that defines what command to run when using `npm run (cmd)`. In our generated `package.json` file, the two main scripts point to the commands in the `wp-scripts` package: - -```json - "scripts": { - "build": "wp-scripts build", - "start": "wp-scripts start" - }, -``` - -These scripts are run by using: `npm run build` or `npm run start`. - -Use `npm run build` for running once to create a "production" build. This compresses the code down so it downloads faster, but makes it harder to read using browser tools—good for final deployment but not while developing. - -Use `npm run start` for creating a "development" build, this does not compress the code so it is easier to read using browser tools, plus [source maps](https://developer.mozilla.org/en-US/docs/Tools/Debugger/How_to/Use_a_source_map) that make debugging easier. Additionally, development build will start a watch process that waits and watches for changes to the file and will rebuild each time it is saved; so you don't have to run the command for each change. - -By default, the build scripts looks for `src/index.js` for the JavaScript file to build and will save the built file to `build/index.js`. In the upcoming sections, we will look closer at that script, but first let's make sure it is loaded in WordPress. - -## Plugin to Load Script - -To load the built script, so it is run within the editor, you need to tell WordPress about the script. This is done in the init action in the `gutenpride.php` file. - -```php -function create_block_gutenpride_block_init() { - register_block_type( __DIR__ . '/build' ); -} -add_action( 'init', 'create_block_gutenpride_block_init' ); -``` - -The `register_block_type` function registers the block we are going to create and specifies the editor script handle registered from the metadata provided in `build/block.json` file with the `editorScript` field. So now when the editor loads it will load this script. The source metadata file `src/block.json` is copied during the build process: - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 3, - "name": "create-block/gutenpride", - "version": "0.1.0", - "title": "Gutenpride", - "category": "widgets", - "icon": "smiley", - "description": "Example static block scaffolded with Create Block tool.", - "supports": { - "html": false - }, - "textdomain": "gutenpride", - "editorScript": "file:./index.js", - "editorStyle": "file:./index.css", - "style": "file:./style-index.css" -} -``` - -For the `editorScript` provided in the block metadata, the build process creates a secondary asset file that contains the list of dependencies and a file version based on the timestamp, this is the `build/index.asset.php` file. - -The `wp_register_script` function used internally registers a name, called the handle, and relates that name to the script file. The dependencies are used to specify if the script requires including other libraries. The version is specified so the browser will reload if the file is changed. - -The `wp_set_script_translations` function tells WordPress to load translations for this script, if they exist. See more about [translations & internationalization.](/docs/how-to-guides/internationalization.md) - -With the above in place, create a new post to load the editor and check your plugin is in the inserter. You can use `/` to search, or click the box with the [+] and search for "Gutenpride" to find the block. - -## Troubleshooting - -It is a good skill to learn and get comfortable using the web console. This is where JavaScript errors are shown and a nice way to test out snippets of JavaScript. See [Firefox Developer Tools documentation](https://developer.mozilla.org/en-US/docs/Tools). - -To open the developer tools in Firefox, use the menu selecting Web Developer : Toggle Tools, on Chrome, select More Tools -> Developers Tools. For both browsers, the keyboard shortcut on Windows is Ctrl+Shift+I, or on Mac Cmd+Shift+I. On Windows & Linux, the F12 key also works. You can then click Console to view logs. - -Try running `npm run start` that will start the watch process for automatic rebuilds. If you then make an update to `src/index.js` file, you will see the build run, and if you reload the WordPress editor you'll see the change. - -For more info, see the build section of the [Getting Started with JavaScript tutorial](/docs/how-to-guides/javascript/js-build-setup.md) in the Block Editor Handbook. - -## Summary - -Hopefully, at this point, you have your plugin created and activated. We have the `package.json` with the `@wordpress/scripts` dependency, that defines the build and start scripts. The basic block is in place and can be added to the editor. - -Next Section: [Anatomy of a Block](/docs/getting-started/create-block/block-anatomy.md) diff --git a/docs/getting-started/tutorial.md b/docs/getting-started/tutorial.md new file mode 100644 index 00000000000000..e70b4aba9234eb --- /dev/null +++ b/docs/getting-started/tutorial.md @@ -0,0 +1,1003 @@ +# Tutorial: Build your first block + +In this tutorial, you will build a "Copyright Date Block"—a basic yet practical block that displays the copyright symbol (©), the current year, and an optional starting year. This type of content is commonly used in website footers. + +The tutorial will guide you through the complete process, from scaffolding the block plugin using the [`create-block`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) package to modifying each file. While previous WordPress development experience is beneficial, it's not a prerequisite for this tutorial. + +By the end of this guide, you will have a clear understanding of block development fundamentals and the necessary skills to create your own WordPress blocks. + +## What you're going to build + +Here's a quick look at what you're going to build. + +![What you're going to build](https://developer.wordpress.org/files/2023/12/block-tutorial-1.png) + +You can also interact with the finished project in [WordPress Playground](https://playground.wordpress.net/?blueprint-url=https://raw.githubusercontent.com/WordPress/block-development-examples/trunk/plugins/copyright-date-block-09aac3/_playground/blueprint.json) or use the [Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) to install the complete block plugin in your local WordPress environment. + +## Prerequisites + +To complete this tutorial, you will need: + +1. Code editor +2. Node.js development tools +3. Local WordPress environment + +If you don't have one or more of these items, the [Block Development Environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) documentation will help you get started. Come back here once you are all set up. + +<div class="callout callout-info"> + This tutorial uses <a href="https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/"><code>wp-env</code></a> to create a local WordPress development environment. However, feel free to use alternate local development tools if you already have one that you prefer. +</div> + +## Scaffolding the block + +The first step in creating the Copyright Date Block is to scaffold the initial block structure using the [`@wordpress/create-block`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-create-block/) package. + +<div class="callout callout-info"> + Review the <a href="https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/">Get started with create-block</a> documentation for an introduction to using this package. +</div> + +You can use `create-block` from just about any directory on your computer and then use `wp-env` to create a local WordPress development environment with your new block plugin installed and activated. + +Therefore, create a new directory (folder) on your computer called "Block Tutorial". Open your terminal and `cd` to this directory. Then run the following command. + +<div class="callout callout-info"> + If you are not using <code>wp-env</code>, instead, navigate to the <code>plugins/</code> folder in your local WordPress installation using the terminal and run the following command. +</div> + +```bash +npx @wordpress/create-block@latest copyright-date-block --variant=dynamic +cd copyright-date-block +``` + +After executing this command, you'll find a new directory named `copyright-date-block` in the plugins folder. This directory contains all the initial files needed to start customizing your block. + +This command also sets up the basic structure of your block, with `copyright-date-block` as its slug. This slug uniquely identifies your block within WordPress. + +<div class="callout callout-info"> + You might have noticed that the command uses the <code>--variant=dynamic</code> flag. This tells <code>create-block</code> you want to scaffold a dynamically rendered block. Later in this tutorial, you will learn about dynamic and static rendering and add static rendering to this block. +</div> + +Navigate to the Plugins page in the WordPress admin and confirm that the plugin is active. Then, create a new page or post and ensure you can insert the Copyright Date Block. It should look like this once inserted. + +![The scaffolded block in the Editor](https://developer.wordpress.org/files/2023/12/block-tutorial-2.png) + +## Reviewing the files +Before we begin modifying the scaffolded block, it's important to review the plugin's file structure. Open the plugin folder in your code editor. + +![The files that make up the block plugin](https://developer.wordpress.org/files/2023/12/block-tutorial-3.png) + +Next, look at the [File structure of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block/) documentation for a thorough overview of what each file does. Don't worry if this is overwhelming right now. You will learn how to use each file throughout this tutorial. + +<div class="callout callout-info"> + Since you scaffolded a dynamic block, you will not see a <code>save.js</code> file. Later in the tutorial, you will add this file to the plugin to enable static rendering, so stay tuned. +</div> + +## Initial setup + +Let's start by creating the simplest Copyright Date Block possible, which will be a dynamically rendered block that simply displays the copyright symbol (©) and the current year. We'll also add a few controls allowing the user to modify font size and text color. + +Before proceeding to the following steps, run `npm run start` in the terminal from within the plugin directory. This command will watch each file in the `/src` folder for changes. The block's build files will be updated each time you save a file. + +Check out the [Working with JavaScript for the Block Editor](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor/) documentation to learn more. + +### Updating block.json + +Open the `block.json` file in the `/src` folder. + +```json +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "create-block/copyright-date-block", + "version": "0.1.0", + "title": "Copyright Date Block", + "category": "widgets", + "icon": "smiley", + "description": "Example block scaffolded with Create Block tool.", + "example": {}, + "supports": { + "html": false + }, + "textdomain": "copyright-date-block", + "editorScript": "file:./index.js", + "editorStyle": "file:./index.css", + "style": "file:./style-index.css", + "render": "file:./render.php", + "viewScript": "file:./view.js" +} +``` + +<div class="callout callout-info"> + Review the <a href="https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/">block.json</a> documentation for an introduction to this file. +</div> + +Since this scaffolding process created this file, it requires some updating to suit the needs of the Copyright Date Block. + +#### Modifying the block identity + +Begin by removing the icon and adding a more appropriate description. You will add a custom icon later. + +1. Remove the line for `icon` +2. Update the description to "Display your site's copyright date." +3. Save the file + +After you refresh the Editor, you should now see that the block no longer has the smiley face icon, and its description has been updated. + +![The block in the Editor with updated information](https://developer.wordpress.org/files/2023/12/block-tutorial-4.png) + +#### Adding block supports + +Next, let's add a few [block supports](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) so that the user can control the font size and text color of the block. + +<div class="callout callout-tip"> + You should always try to use native block supports before building custom functionality. This approach provides users with a consistent editing experience across blocks, and your block benefits from Core functionality with only a few lines of code. +</div> + +Update the [`supports`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#enable-ui-settings-panels-for-the-block-with-supports) section of the `block.json` file to look like this. + +```json +"supports": { + "color": { + "background": false, + "text": true + }, + "html": false, + "typography": { + "fontSize": true + } +}, +``` + +Note that when you enable text color support with `"text": true`, the background color is also enabled by default. You are welcome to keep it enabled, but it's not required for this tutorial, so you can manually set `"background": false`. + +Save the file and select the block in the Editor. You will now see both Color and Typography panels in the Settings Sidebar. Try modifying the settings and see what happens. + +![The block in the Editor with block supports](https://developer.wordpress.org/files/2023/12/block-tutorial-5.png) + +#### Removing unnecessary code + +For simplicity, the styling for the Copyright Date Block will be controlled entirely by the color and typography block supports. This block also does not have any front-end Javascript. Therefore, you don't need to specify stylesheets or a `viewScript` in the `block.json` file. + +1. Remove the line for `editorStyle` +2. Remove the line for `style` +3. Remove the line for `viewScript` +4. Save the file + +Refresh the Editor, and you will see that the block styling now matches your current theme. + +![The block in the Editor without default styling](https://developer.wordpress.org/files/2023/12/block-tutorial-6.png) + +#### Putting it all together + +Your final `block.json` file should look like this: + +```json +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "create-block/copyright-date-block", + "version": "0.1.0", + "title": "Copyright Date Block", + "category": "widgets", + "description": "Display your site's copyright date.", + "example": {}, + "supports": { + "color": { + "background": false, + "text": true + }, + "html": false, + "typography": { + "fontSize": true + } + }, + "textdomain": "copyright-date-block", + "editorScript": "file:./index.js", + "render": "file:./render.php" +} +``` + +### Updating index.js + +Before you start building the functionality of the block itself, let's do a bit more cleanup and add a custom icon to the block. + +Open the [`index.js`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block/#index-js) file. This is the main JavaScript file of the block and is used to register it on the client. You can learn more about client-side and server-side registration in the [Registration of a block](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/) documentation. + +Start by looking at the [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/) function. This function accepts the name of the block, which we are getting from the imported `block.js` file, and the block configuration object. + +```js +import Edit from './edit'; +import metadata from './block.json'; + +registerBlockType( metadata.name, { + edit: Edit, +} ); +``` + +By default, the object just includes the `edit` property, but you can add many more, including `icon`. While most of these properties are already defined in `block.json`, you need to specify the icon here to use a custom SVG. + +#### Adding a custom icon + +Using the calendar icon from the [Gutenberg Storybook](https://wordpress.github.io/gutenberg/?path=/story/icons-icon--library), add the SVG to the function like so: + +```js +const calendarIcon = ( + <svg + viewBox="0 0 24 24" + xmlns="http://www.w3.org/2000/svg" + aria-hidden="true" + focusable="false" + > + <path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm.5 16c0 .3-.2.5-.5.5H5c-.3 0-.5-.2-.5-.5V7h15v12zM9 10H7v2h2v-2zm0 4H7v2h2v-2zm4-4h-2v2h2v-2zm4 0h-2v2h2v-2zm-4 4h-2v2h2v-2zm4 0h-2v2h2v-2z"></path> + </svg> +); + +registerBlockType( metadata.name, { + icon: calendarIcon, + edit: Edit +} ); +``` + +<div class="callout callout-tip"> + All block icons should be 24 pixels square. Note the <code>viewBox</code> parameter above. +</div> + +Save the `index.js` file and refresh the Editor. You will now see the calendar icon instead of the default. + +![The block in the Editor a custom icon](https://developer.wordpress.org/files/2023/12/block-tutorial-7.png) + +At this point, the block's icon and description are correct, and block supports allow you to change the font size and text color. Now, it's time to move on to the actual functionality of the block. + +### Updating edit.js + +The [`edit.js`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block/#edit-js) file controls how the block functions and appears in the Editor. Right now, the user sees the message " Copyright Date Block – hello from the editor!". Let's change that. + +Open the file and see that the `Edit()` function returns a paragraph tag with the default message. + +```js +export default function Edit() { + return ( + <p { ...useBlockProps() }> + { __( + 'Copyright Date Block – hello from the editor!', + 'copyright-date-block-demo' + ) } + </p> + ); +} +``` + +It looks a bit more complicated than it is. + +- [`useBlockProps()`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#block-wrapper-props) outputs all the necessary CSS classes and styles in the [block's wrapper](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper/#the-edit-components-markup) needed by the Editor, which includes the style provided by the block supports you added earlier +- [`__()`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-i18n/) is used for the internationalization of text strings + +<div class="callout callout-info"> + Review the <a href="https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper/">block wrapper</a> documentation for an introductory guide on how to ensure the block's markup wrapper has the proper attributes. +</div> + +As a reminder, the main purpose of this block is to display the copyright symbol (©) and the current year. So, you first need to get the current year in string form, which can be done with the following code. + +```js +const currentYear = new Date().getFullYear().toString(); +``` + +Next, update the function to display the correct information. + +```js +export default function Edit() { + const currentYear = new Date().getFullYear().toString(); + + return ( + <p { ...useBlockProps() }>© { currentYear }</p> + ); +} +``` + +Save the `edit.js` file and refresh the Editor. You will now see the copyright symbol (©) followed by the current year. + +![The block in the Editor displays the correct content](https://developer.wordpress.org/files/2023/12/block-tutorial-8.png) + +### Updating render.php + +While the block is working properly in the Editor, the default block message is still being displayed on the front end. To fix this, open the [`render.php`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block/#render-php) file, and you will see the following. + +```php +<?php +... +?> +<p <?php echo get_block_wrapper_attributes(); ?>> + <?php esc_html_e( 'Copyright Date Block – hello from a dynamic block!', 'copyright-date-block' ); ?> +</p> + +``` + +Similar to the `useBlockProps()` function in the Editor, [`get_block_wrapper_attributes()`](https://developer.wordpress.org/reference/functions/get_block_wrapper_attributes/) outputs all the necessary CSS classes and styles in the [block's wrapper](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper/#the-server-side-render-markup). Only the content needs to be updated. + +You can use `date( "Y" )` to get the current year in PHP, and your `render.php` should look like this. + +```php +<?php +... +?> +<p <?php echo get_block_wrapper_attributes(); ?>>© <?php echo date( "Y" ); ?></p> +``` + +Save the file and confirm that the block appears correctly in the Editor and on the front end. + +### Cleaning up + +When you use the `create-block` package to scaffold a block, it might include files that you don't need. In the case of this tutorial, the block doesn't use stylesheets or font end JavaScipt. Clean up the plugin's `src/` folder with the following actions. + +1. In the `edit.js` file, remove the lines that import `editor.scss` +2. In the `index.js` file, remove the lines that import `style.scss` +3. Delete the editor.scss, style.scss, and view.js files + +Finally, make sure that there are no unsaved changes and then terminate the `npm run start` command. Run `npm run build` to optimize your code and make it production-ready. + +You have built a fully functional WordPress block, but let's not stop here. In the next sections, we'll add functionality and enable static rendering. + +## Adding block attributes + +The Copyright Date Block you have built shows the current year, but what if you wanted to display a starting year as well? + +![What you're going to build](https://developer.wordpress.org/files/2023/12/block-tutorial-1.png) + +This functionality would require users to enter their starting year somewhere on the block. They should also have the ability to toggle it on or off. + +You could implement this in different ways, but all would require [block attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/). Attributes allow you to store custom data for the block that can then be used to modify the block's markup. + +To enable this starting year functionality, you will need one attribute to store the starting year and another that will be used to tell WordPress whether the starting year should be displayed or not. + +### Updating block.json + +Block attributes are generally specified in the [`block.json`](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#data-storage-in-the-block-with-attributes) file. So open up the file and add the following section after the `example` in line 9. + +```json +"attributes": { + "showStartingYear": { + "type": "boolean" + }, + "startingYear": { + "type": "string" + } +}, +``` + +You must indicate the `type` when defining attributes. In this case, the `showStartingYear` should be true or false, so it's set to `boolean`. The `startingYear` is just a string. + +Save the file, and you can now move on to the Editor. + +### Updating edit.js + +Open the `edit.js` file. You will need to accomplish two tasks. + +Add a user interface that allows the user to enter a starting year, toggle the functionality on or off, and store these settings as attributes +Update the block to display the correct content depending on the defined attributes + +#### Adding the user interface + +Earlier in this tutorial, you added block supports that automatically created Color and Typography panels in the Settings Sidebar of the block. You can create your own custom panels using the `InspectorControls` component. + +##### Inspector controls + +The `InspectorControls` belongs to the [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/) package, so you can import it into the `edit.js` file by adding the component name on line 14. The result should look like this. + +```js +import { InspectorControls, useBlockProps } from '@wordpress/block-editor'; +``` + +Next, update the Edit function to return the current block content and an `InspectorControls` component that includes the text "Testing." You can wrap everything in a [Fragment](https://react.dev/reference/react/Fragment) (`<></>`) to ensure proper JSX syntax. The result should look like this. + +```js +export default function Edit() { +const currentYear = new Date().getFullYear().toString(); + + return ( + <> + <InspectorControls> + Testing + </InspectorControls> + <p { ...useBlockProps() }>© { currentYear }</p> + </> + ); +} +``` +Save the file and refresh the Editor. When selecting the block, you should see the "Testing" message in the Settings Sidebar. + +![The Setting Sidebar now displays the message](https://developer.wordpress.org/files/2023/12/block-tutorial-9.png) + +##### Components and panels + +Now, let's use a few more Core components to add a custom panel and the user interface for the starting year functionality. You will want to import [`PanelBody`](https://developer.wordpress.org/block-editor/reference-guides/components/panel/#panelbody), [`TextControl`](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/), and [`ToggleControl`](https://developer.wordpress.org/block-editor/reference-guides/components/toggle-control/) from the [`@wordpress/components`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/) package. + +Add the following line below the other imports in the `edit.js` file. + +```js +import { PanelBody, TextControl, ToggleControl } from '@wordpress/components'; +``` + +Then wrap the "Testing" message in the `PanelBody` component and set the `title` parameter to "Settings". Refer to the [component documentation](https://developer.wordpress.org/block-editor/reference-guides/components/panel/#panelbody) for additional parameter options. + +```js +export default function Edit() { +const currentYear = new Date().getFullYear().toString(); + + return ( + <> + <InspectorControls> + <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }> + Testing + </PanelBody> + </InspectorControls> + <p { ...useBlockProps() }>© { currentYear }</p> + </> + ); +} +``` + +Save the file and refresh the Editor. You should now see the new Settings panel. + +![The Setting Sidebar now displays a custom panel](https://developer.wordpress.org/files/2023/12/block-tutorial-10.png) + +##### Text control + +The next step is to replace the "Testing" message with a `TextControl` component that allows the user to set the `startingYear` attribute. However, you must include two parameters in the `Edit()` function before doing so. + +- `attributes` is an object that contains all the attributes for the block +- `setAttributes` is a function that allows you to update the value of an attribute + +With these parameters included, you can fetch the `showStartingYear` and `startingYear` attributes. + +Update the top of the `Edit()` function to look like this. + +```js +export default function Edit( { attributes, setAttributes } ) { + const { showStartingYear, startingYear } = attributes; + ... +``` + +<div class="callout callout-tip"> + To see all the attributes associated with the Copyright Date Block, add <code>console.log( attributes );</code> at the top of the <code>Edit()</code> function. This can be useful when building and testing a custom block. +</div> + +Now, you can remove the "Testing" message and add a `TextControl`. It should include: + +1. A `label` property set to "Starting year" +2. A `value` property set to the attribute `startingYear` +3. An `onChange` property that updates the `startingYear` attribute whenever the value changes + +Putting it all together, the `Edit()` function should look like the following. + +```js +export default function Edit( { attributes, setAttributes } ) { + const { showStartingYear, startingYear } = attributes; + const currentYear = new Date().getFullYear().toString(); + + return ( + <> + <InspectorControls> + <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }> + <TextControl + label={ __( + 'Starting year', + 'copyright-date-block' + ) } + value={ startingYear } + onChange={ ( value ) => + setAttributes( { startingYear: value } ) + } + /> + </PanelBody> + </InspectorControls> + <p { ...useBlockProps() }>© { currentYear }</p> + </> + ); +} +``` + +Save the file and refresh the Editor. Confirm that a text field now exists in the Settings panel. Add a starting year and confirm that when you update the page, the value is saved. + +![A live look at editing the new Starting Year field in the Settings Sidebar](https://developer.wordpress.org/files/2023/12/block-tutorial-11.gif) + +##### Toggle control + +Next, let's add a toggle that will turn the starting year functionality on or off. You can do this with a `ToggleControl` component that sets the `showStartingYear` attribute. It should include: + +1. A `label` property set to "Show starting year" +2. A `checked` property set to the attribute `showStartingYear` +3. An `onChange` property that updates the `showStartingYear` attribute whenever the toggle is checked or unchecked + +You can also update the "Starting year" text input so it's only displayed when `showStartingYear` is `true`, which can be done using the `&&` logical operator. + +The `Edit()` function should look like the following. + +```js +export default function Edit( { attributes, setAttributes } ) { + const { showStartingYear, startingYear } = attributes; + const currentYear = new Date().getFullYear().toString(); + + return ( + <> + <InspectorControls> + <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }> + <ToggleControl + checked={ showStartingYear } + label={ __( + 'Show starting year', + 'copyright-date-block' + ) } + onChange={ () => + setAttributes( { + showStartingYear: ! showStartingYear, + } ) + } + /> + { showStartingYear && ( + <TextControl + label={ __( + 'Starting year', + 'copyright-date-block' + ) } + value={ startingYear } + onChange={ ( value ) => + setAttributes( { startingYear: value } ) + } + /> + ) } + </PanelBody> + </InspectorControls> + <p { ...useBlockProps() }>© { currentYear }</p> + </> + ); +} +``` + +Save the file and refresh the Editor. Confirm that clicking the toggle displays the text input, and when you update the page, the toggle remains active. + +![A live look at editing the new Show Starting Year toggle in the Settings Sidebar](https://developer.wordpress.org/files/2023/12/block-tutorial-12.gif) + +#### Updating the block content + +So far, you have created the user interface for adding a starting year and updating the associated block attributes. Now you need to actually update the block content in the Editor. + +Let's create a new variable called `displayDate`. When `showStartingYear` is `true` and the user has provided a `startingYear`, the `displayDate` should include the `startingYear` and the `currentYear` separated by an em dash. Otherwise, just display the `currentYear`. + +The code should look something like this. + +```js +let displayDate; + +if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + currentYear; +} else { + displayDate = currentYear; +} +``` + +<div class="callout callout-tip"> + When you declare a variable with <code>let</code>, it means that the variable may be reassigned later. Declaring a variable with <code>const</code> means that the variable will never change. You could rewrite this code using <code>const</code>. It's just a matter of personal preference. +</div> + +Next, you just need to update the block content to use the `displayDate` instead of the `currentYear` variable. + +The `Edit()` function should look like the following. + +```js +export default function Edit( { attributes, setAttributes } ) { + const { showStartingYear, startingYear } = attributes; + const currentYear = new Date().getFullYear().toString(); + + let displayDate; + + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + currentYear; + } else { + displayDate = currentYear; + } + + return ( + <> + <InspectorControls> + <PanelBody title={ __( 'Settings', 'copyright-date-block' ) }> + <ToggleControl + checked={ showStartingYear } + label={ __( + 'Show starting year', + 'copyright-date-block' + ) } + onChange={ () => + setAttributes( { + showStartingYear: ! showStartingYear, + } ) + } + /> + { showStartingYear && ( + <TextControl + label={ __( + 'Starting year', + 'copyright-date-block' + ) } + value={ startingYear } + onChange={ ( value ) => + setAttributes( { startingYear: value } ) + } + /> + ) } + </PanelBody> + </InspectorControls> + <p { ...useBlockProps() }>© { displayDate }</p> + </> + ); +} +``` + +Save the file and refresh the Editor. Confirm that the block content updates correctly when you make changes in the Settings panel. + +![A live look at the block content being updated by the new fields in the Setting Sidebar](https://developer.wordpress.org/files/2023/12/block-tutorial-13.gif) + +### Updating render.php + +While the Editor looks great, the starting year functionality has yet to be added to the front end. Let's fix that by updating the `render.php` file. + +Start by adding a variable called `$display_date` and replicate what you did in the `Edit()` function above. + +This variable should display the value of the `startingYear` attribute and the `$current_year` variable separated by an em dash, or just the `$current_year` is the `showStartingYear` attribute is `false`. + +<div class="callout callout-tip"> + <p>Three variables are exposed in the <code>render.php</code>, which you can use to customize the block's output:</p> + <ul> + <li><code>$attributes</code> (array): The block attributes.</li> + <li><code>$content</code> (string): The block default content.</li> + <li><code>$block</code> (WP_Block): The block instance.</li> + </ul> +</div> + +The code should look something like this. + +```php +if ( ! empty( $attributes['startingYear'] ) && ! empty( $attributes['showStartingYear'] ) ) { + $display_date = $attributes['startingYear'] . '–' . $current_year; +} else { + $display_date = $current_year; +} +``` + +Next, you just need to update the block content to use the `$display_date` instead of the `$current_year` variable. + +Your final `render.php` file should look like this. + +```php +<?php +$current_year = date( "Y" ); + +if ( ! empty( $attributes['startingYear'] ) && ! empty( $attributes['showStartingYear'] ) ) { + $display_date = $attributes['startingYear'] . '–' . $current_year; +} else { + $display_date = $current_year; +} +?> +<p <?php echo get_block_wrapper_attributes(); ?>> + © <?php echo esc_html( $display_date ); ?> +</p> +``` + +Save the file and confirm that the correct block content is now appearing on the front end of your site. + +You have now successfully built a dynamically rendered custom block that utilizes block supports, core WordPress components, and custom attributes. In many situations, this is as far as you would need to go for a block displaying the copyright date with some additional functionality. + +In the next section, however, you will add static rendering to the block. This exercise will illustrate how block data is stored in WordPress and provide a fallback should this plugin ever be inadvertently disabled. + +## Adding static rendering + +A block can be dynamically rendered, statically rendered, or both. The block you have built so far is dynamically rendered. The HTML output of the block is not actually stored in the database, only the block markup and the associated attributes. + +You will see the following if you switch to the Code editor from within the Editor. + +```html +<!-- wp:create-block/copyright-date-block {"showStartingYear":true,"startingYear":"2017"} /--> +``` + +Compare this to a statically rendered block like the Paragraph block. + +```html +<!-- wp:paragraph --> +<p>This is a test.</p> +<!-- /wp:paragraph --> +``` + +The HTML of the paragraph is stored in post content and saved in the database. + +You can learn more about dynamic and static rendering in the [Fundamentals documentation](https://developer.wordpress.org/block-editor/getting-started/fundamentals/). While most blocks are either dynamically or statically rendered, you can build a block that utilizes both methods. + +### Why add static rendering? + +When you add static rendering to a dynamically rendered block, the `render.php` file will still control the output on the front end, but the block's HTML content will be saved in the database. This means that the content will remain if the plugin is ever removed from the site. In the case of this Copyright Date Block, the content will revert to a Custom HTML block that you can easily convert to a Paragraph block. + +![An error message in the Editor when a block type no longer exists](https://developer.wordpress.org/files/2023/12/block-tutorial-14.png) + +While not necessary in all situations, adding static rendering to a dynamically rendered block can provide a helpful fallback should the plugin ever be disabled unintentionally. + +Also, consider a situation where the block markup is included in a block pattern or theme template. If a user installs that theme or uses the pattern without the Copyright Date Block installed, they will get a notice that the block is not available, but the content will still be displayed. + +Adding static rendering is also a good exploration of how block content is stored and rendered in WordPress. + +### Adding a save function + +Start by adding a new file named `save.js` to the `src/` folder. In this file, add the following. + +```js +import { useBlockProps } from '@wordpress/block-editor'; + +export default function save() { + return ( + <p { ...useBlockProps.save() }> + { 'Copyright Date Block – hello from the saved content!' } + </p> + ); +} +``` + +This should look similar to the original `edit.js` file, and you can refer to the [block wrapper](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper/#the-save-components-markup) documentation for additional information. + +Next, in the `index.js` file, import this `save()` function and add a save property to the `registerBlockType()` function. Here's a simplified view of the updated file. + +```js +import save from './save'; + +... + +registerBlockType( metadata.name, { + icon: calendarIcon, + edit: Edit, + save +} ); +``` + +<div class="callout callout-tip"> + When defining properties of an object, if the property name and the variable name are the same, you can use shorthand property names. This is why the code above uses <code>save</code> instead of <code>save: save</code>. +</div> + +Save both `save.js` and `index.js` files and refresh the Editor. It should look like this. + +![A block validation error message in the Editor](https://developer.wordpress.org/files/2023/12/block-tutorial-15.png) + +Don't worry, the error is expected. If you open the inspector in your browser, you should see the following message. + +![A block validation error message in the console](https://developer.wordpress.org/files/2023/12/block-tutorial-16.png) + +This block validation error occurs because the `save()` function returns block content, but no HTML is stored in the block markup since the previously saved block was dynamic. Remember that this is what the markup currently looks like. + +```html +<!-- wp:create-block/copyright-date-block {"showStartingYear":true,"startingYear":"2017"} /--> +``` + +You will see more of these errors as you update the `save()` function in subsequent steps. Just click "Attempt Block Recovery" and update the page. + +After preforming block recovery, open the Code editor and you will see the markup now looks like this. + +```html +<!-- wp:create-block/copyright-date-block {"showStartingYear":true,"startingYear":"2017"} --> +<p class="wp-block-create-block-copyright-date-block">Copyright Date Block – hello from the saved content!</p> +<!-- /wp:create-block/copyright-date-block --> +``` + +You will often encounter block validation errors when building a block with static rendering, and that's ok. The output of the `save()` function must match the HTML in the post content exactly, which may end up out of sync as you add functionality. So long as there are no validation errors when you're completely finished building the block, you will be all set. + +### Updating save.js + +Next, let's update the output of the `save()` function to display the correct content. Start by copying the same approach used in `edit.js`. + +1. Add the `attributes` parameter to the function +2. Define the `showStartingYear` and `startingYear` variables +3. Define a `currentYear` variable +4. Define a `displayDate` variable depending on the values of `currentYear`, `showStartingYear`, and `startingYear` + +The result should look like this. + +```js +export default function save( { attributes } ) { + const { showStartingYear, startingYear } = attributes; + const currentYear = new Date().getFullYear().toString(); + + let displayDate; + + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + currentYear; + } else { + displayDate = currentYear; + } + + return ( + <p { ...useBlockProps.save() }>© { displayDate }</p> + ); +} +``` + +Save the file and refresh the Editor. Click "Attempt Block Recovery" and update the page. Check the Code editor, and the block markup should now look something like this. + +```html +<!-- wp:create-block/copyright-date-block {"showStartingYear":true,"startingYear":"2017"} --> +<p class="wp-block-create-block-copyright-date-block">© 2017–2023</p> +<!-- /wp:create-block/copyright-date-block --> +``` + +At this point, it might look like you're done. The block content is now saved as HTML in the database and the output on the front end is dynamically rendered. However, there are still a few things that need to be addressed. + +Consider the situation where the user added the block to a page in 2023 and then went back to edit the page in 2024. The front end will update as expected, but in the Editor, there will be a block validation error. The `save()` function knows that it's 2024, but the block content saved in the database still says 2023. + +Let's fix this in the next section. + +### Handling dynamic content in statically rendered blocks + +Generally, you want to avoid dynamic content in statically rendered blocks. This is part of the reason why the term "dynamic" is used when referring to dynamic rendering. + +That said, in this tutorial, you are combining both rendering methods, and you just need a bit more code to avoid any block validation errors when the year changes. + +The root of the issue is that the `currentYear` variable is set dynamically in the `save()` function. Instead, this should be a static variable within the function, which can be solved with an additional attribute. + +#### Adding a new attribute + +Open the `block.json` file and add a new attribute called `fallbackCurrentYear` with the type `string`. The `attributes` section of the file should now look like this. + +```json +"attributes": { + "fallbackCurrentYear": { + "type": "string" + }, + "showStartingYear": { + "type": "boolean" + }, + "startingYear": { + "type": "string" + } +}, +``` + +Next, open the `save.js` file and use the new `fallbackCurrentYear` attribute in place of `currentYear`. Your updated `save()` function should look like this. + +```js +export default function save( { attributes } ) { + const { fallbackCurrentYear, showStartingYear, startingYear } = attributes; + + let displayDate; + + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + fallbackCurrentYear; + } else { + displayDate = fallbackCurrentYear; + } + + return ( + <p { ...useBlockProps.save() }>© { displayDate }</p> + ); +} +``` + +Now, what happens if the `fallbackCurrentYear` is undefined? + +Before the `currentYear` was defined within the function, so the `save()` function always had content to return, even if `showStartingYear` and `startingYear` were undefined. + +Instead of returning just the copyright symbol, let's add a condition that if `fallbackCurrentYear` is not set, return `null`. It's generally better to save no HTML in the database than incomplete data. + +The final `save()` function should look like this. + +```js +export default function save( { attributes } ) { + const { fallbackCurrentYear, showStartingYear, startingYear } = attributes; + + if ( ! fallbackCurrentYear ) { + return null; + } + + let displayDate; + + if ( showStartingYear && startingYear ) { + displayDate = startingYear + '–' + fallbackCurrentYear; + } else { + displayDate = fallbackCurrentYear; + } + + return ( + <p { ...useBlockProps.save() }>© { displayDate }</p> + ); +} +``` + +Save both the `block.json` and `save.js` files; you won't need to make any more changes. + +#### Setting the attribute in edit.js + +The `save()` function now uses the new `fallbackCurrentYear`, so it needs to be set somewhere. Let's use the `Edit()` function. + +Open the `edit.js` file and start by defining the `fallbackCurrentYear` variable at the top of the `Edit()` functional alongside the other attributes. Next, review what's happening in the function. + +When the block loads in the Editor, the `currentYear` variable is defined. The function then uses this variable to set the content of the block. + +Now, let's set the `fallbackCurrentYear` attribute to the `currentYear` when the block loads if the attribute is not already set. + +```js +if ( currentYear !== fallbackCurrentYear ) { + setAttributes( { fallbackCurrentYear: currentYear } ); +} +``` + +This will work but can be improved by ensuring this code only runs once when the block is initialized. To do so, you can use the [`useEffect`](https://react.dev/reference/react/useEffect) React hook. Refer to the React documentation for more information about how to use this hook. + +First, import `useEffect` with the following code. + +```js +import { useEffect } from 'react'; +``` + +Then wrap the `setAttribute()` code above in a `useEffect` and place this code after the `currentYear` definition in the `Edit()` function. The result should look like this. + +```js +export default function Edit( { attributes, setAttributes } ) { + const { fallbackCurrentYear, showStartingYear, startingYear } = attributes; + + // Get the current year and make sure it's a string. + const currentYear = new Date().getFullYear().toString(); + + // When the block loads, set the fallbackCurrentYear attribute to the + // current year if it's not already set. + useEffect( () => { + if ( currentYear !== fallbackCurrentYear ) { + setAttributes( { fallbackCurrentYear: currentYear } ); + } + }, [ currentYear, fallbackCurrentYear, setAttributes ] ); + + ... +``` + +When the block is initialized in the Editor, the `fallbackCurrentYear` attribute will be immediately set. This value will then be available to the `save()` function, and the correct block content will be displayed without block validation errors. + +The one caveat is when the year changes. If a Copyright Date Block was added to a page in 2023 and then edited in 2024, the `fallbackCurrentYear` attribute will no longer equal the `currentYear`, and the attribute will be automatically updated to `2024`. This will update the HTML returned by the `save()` function. + +You will not get any block validation errors, but the Editor will detect that changes have been made to the page and prompt you to update. + +#### Optimizing render.php + +The final step is to optimize the `render.php` file. If the `currentYear` and the `fallbackCurrentYear` attribute are the same, then there is no need to dynamically create the block content. It is already saved in the database and is available in the `render.php` file via the `$block_content` variable. + +Therefore, update the file to render the `$block_content` if `currentYear` and `fallbackCurrentYear` match. + +```php +$current_year = date( "Y" ); + +// Determine which content to display. +if ( isset( $attributes['fallbackCurrentYear'] ) && $attributes['fallbackCurrentYear'] === $current_year ) { + + // The current year is the same as the fallback, so use the block content saved in the database (by the save.js function). + $block_content = $content; +} else { + + // The current year is different from the fallback, so render the updated block content. + if ( ! empty( $attributes['startingYear'] ) && ! empty( $attributes['showStartingYear'] ) ) { + $display_date = $attributes['startingYear'] . '–' . $current_year; + } else { + $display_date = $current_year; + } + + $block_content = '<p ' . get_block_wrapper_attributes() . '>© ' . esc_html( $display_date ) . '</p>'; +} + +echo wp_kses_post( $block_content ); +``` + +That's it! You now have a block that utilizes both dynamic and static rendering. + +## Wrapping up + +Congratulations on completing this tutorial and building your very own Copyright Date Block. Throughout this journey, you have gained a solid foundation in WordPress block development and are now ready to start building your own blocks. + +For final reference, the complete code for this tutorial is available in the [Block Development Examples](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/copyright-date-block-09aac3) repository on GitHub. + +Now, whether you're now looking to refine your skills, tackle more advanced projects, or stay updated with the latest WordPress trends, the following resources will help you improve your block development skills: + +- [Block Development Environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) +- [Fundamentals of Block Development](https://developer.wordpress.org/block-editor/getting-started/fundamentals/) +- [WordPress Developer Blog](https://developer.wordpress.org/news/) +- [Block Development Examples](https://github.com/WordPress/block-development-examples) | GitHub repository + +Remember, every expert was once a beginner. Keep learning, experimenting, and, most importantly, have fun building with WordPress. diff --git a/docs/manifest.json b/docs/manifest.json index b91732962e0a91..6b5ee58d4d6a50 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -48,53 +48,11 @@ "parent": "getting-started" }, { - "title": "Create a Block Tutorial", - "slug": "create-block", - "markdown_source": "../docs/getting-started/create-block/README.md", + "title": "Tutorial: Build your first block", + "slug": "tutorial", + "markdown_source": "../docs/getting-started/tutorial.md", "parent": "getting-started" }, - { - "title": "WordPress Plugin", - "slug": "wp-plugin", - "markdown_source": "../docs/getting-started/create-block/wp-plugin.md", - "parent": "create-block" - }, - { - "title": "Anatomy of a Block", - "slug": "block-anatomy", - "markdown_source": "../docs/getting-started/create-block/block-anatomy.md", - "parent": "create-block" - }, - { - "title": "Block Attributes", - "slug": "attributes", - "markdown_source": "../docs/getting-started/create-block/attributes.md", - "parent": "create-block" - }, - { - "title": "Code Implementation", - "slug": "block-code", - "markdown_source": "../docs/getting-started/create-block/block-code.md", - "parent": "create-block" - }, - { - "title": "Authoring Experience", - "slug": "author-experience", - "markdown_source": "../docs/getting-started/create-block/author-experience.md", - "parent": "create-block" - }, - { - "title": "Finishing Touches", - "slug": "finishing", - "markdown_source": "../docs/getting-started/create-block/finishing.md", - "parent": "create-block" - }, - { - "title": "Share your Block with the World", - "slug": "submitting-to-block-directory", - "markdown_source": "../docs/getting-started/create-block/submitting-to-block-directory.md", - "parent": "create-block" - }, { "title": "Fundamentals of Block Development", "slug": "fundamentals", diff --git a/docs/toc.json b/docs/toc.json index 34be591647c174..fec39245761c94 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -21,31 +21,7 @@ ] }, { "docs/getting-started/quick-start-guide.md": [] }, - { - "docs/getting-started/create-block/README.md": [ - { - "docs/getting-started/create-block/wp-plugin.md": [] - }, - { - "docs/getting-started/create-block/block-anatomy.md": [] - }, - { - "docs/getting-started/create-block/attributes.md": [] - }, - { - "docs/getting-started/create-block/block-code.md": [] - }, - { - "docs/getting-started/create-block/author-experience.md": [] - }, - { - "docs/getting-started/create-block/finishing.md": [] - }, - { - "docs/getting-started/create-block/submitting-to-block-directory.md": [] - } - ] - }, + { "docs/getting-started/tutorial.md": [] }, { "docs/getting-started/fundamentals/README.md": [ { From 41e18933df3e9851635fe8f0d78b5bc59bf52237 Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Fri, 15 Dec 2023 23:12:37 -0600 Subject: [PATCH 220/325] Fix grammar and typos. (#57123) --- docs/getting-started/devenv/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/getting-started/devenv/README.md b/docs/getting-started/devenv/README.md index 7d7323d9c7b0f2..c891490437d431 100644 --- a/docs/getting-started/devenv/README.md +++ b/docs/getting-started/devenv/README.md @@ -2,7 +2,7 @@ This guide will help you set up the right development environment to create blocks and other plugins that extend and modify the Block Editor in WordPress. -To contribute to the Gutenberg project itself, refer to the additional documentation in the [code contribution guide](/docs/contributors/code/getting-started-with-code-contribution.md).` +To contribute to the Gutenberg project itself, refer to the additional documentation in the [code contribution guide](/docs/contributors/code/getting-started-with-code-contribution.md). A block development environment includes the tools you need on your computer to successfully develop for the Block Editor. The three essential requirements are: @@ -14,7 +14,7 @@ A block development environment includes the tools you need on your computer to A code editor is used to write code, and you can use whichever editor you're most comfortable with. The key is having a way to open, edit, and save text files. -If you do not already have a preferred code editor, [Visual Studio Code](https://code.visualstudio.com/) (VS Code) is a popular choice for JavaScript development among Core contributors. It works well across the three major platforms (Windows, Linux, and Mac) and is open-source and actively maintained by Microsoft. VS Code also has a vibrant community providing plugins and extensions, including many for WordPress development. +If you do not already have a preferred code editor, [Visual Studio Code](https://code.visualstudio.com/) (VS Code) is a popular choice for JavaScript development among Core contributors. It works well across the three major platforms (Windows, Linux, and Mac), is open-source, and is actively maintained by Microsoft. VS Code also has a vibrant community providing plugins and extensions, including many for WordPress development. ## Node.js development tools @@ -30,9 +30,9 @@ Node.js and its accompanying development tools allow you to: The list goes on. While modern JavaScript development can be challenging, WordPress provides several tools, like [`wp-scripts`](/docs/getting-started/devenv/get-started-with-wp-scripts.md) and [`create-block`](/docs/getting-started/devenv/get-started-with-create-block.md), that streamline the process and are made possible by Node.js development tools. -**The recommended Node.js version for block development is [Active LTS](https://nodejs.dev/en/about/releases/) (Long Term Support)**. However, there are times when you need to to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to easily change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. +**The recommended Node.js version for block development is [Active LTS](https://nodejs.dev/en/about/releases/) (Long Term Support)**. However, there are times when you need to use different versions. A Node.js version manager tool like `nvm` is strongly recommended and allows you to easily change your `node` version when required. You will also need Node Package Manager (`npm`) and the Node Package eXecute (`npx`) to work with some WordPress packages. Both are installed automatically with Node.js. -To be able to use the Node.js tools and [packages provided by WordPress](https://github.com/WordPress/gutenberg/tree/trunk/packages) for block development, you'll need to set a proper Node.js runtime environment on your machine.. To learn more about how to do this, refer to the links below. +To be able to use the Node.js tools and [packages provided by WordPress](https://github.com/WordPress/gutenberg/tree/trunk/packages) for block development, you'll need to set a proper Node.js runtime environment on your machine. To learn more about how to do this, refer to the links below. - [Install Node.js for Mac and Linux](/docs/getting-started/devenv/nodejs-development-environment.md#node-js-installation-on-mac-and-linux-with-nvm) - [Install Node.js for Windows](/docs/getting-started/devenv/nodejs-development-environment.md#node-js-installation-on-windows-and-others) @@ -41,7 +41,7 @@ To be able to use the Node.js tools and [packages provided by WordPress](https:/ A local WordPress environment (site) provides a controlled, efficient, and secure space for development, allowing you to build and test your code before deploying it to a production site. The [same requirements](https://en-gb.wordpress.org/about/requirements/) for WordPress apply to local sites. -In the boarder WordPress community, there are many available tools for setting up a local WordPress environment on your computer. The Block Editor Handbook covers `wp-env`, which is open-source and maintained by the WordPress project itself. It's also the recommended tool for Gutenberg development. +In the broader WordPress community, there are many available tools for setting up a local WordPress environment on your computer. The Block Editor Handbook covers `wp-env`, which is open-source and maintained by the WordPress project itself. It's also the recommended tool for Gutenberg development. Refer to the [Get started with `wp-env`](/docs/getting-started/devenv/get-started-with-wp-env.md) guide for setup instructions. From a2516660ef94bede9321fbabeb378a475e58317b Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Sat, 16 Dec 2023 20:41:46 +0100 Subject: [PATCH 221/325] Block editor: rewrite moving animation for better load performance (#57133) --- .../src/components/block-list/block.js | 23 -- .../block-list/use-block-props/index.js | 10 +- .../components/use-moving-animation/index.js | 210 +++++++++--------- 3 files changed, 108 insertions(+), 135 deletions(-) diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 0bd5d0b7e199f8..c053235c2d0d36 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -39,12 +39,6 @@ import { PrivateBlockContext } from './private-block-context'; import { unlock } from '../../lock-unlock'; -/** - * If the block count exceeds the threshold, we disable the reordering animation - * to avoid laginess. - */ -const BLOCK_ANIMATION_THRESHOLD = 200; - /** * Merges wrapper props with special handling for classNames and styles. * @@ -516,9 +510,7 @@ function BlockListBlockProvider( props ) { getBlockIndex, isTyping, - getGlobalBlockCount, isBlockMultiSelected, - isAncestorMultiSelected, isBlockSubtreeDisabled, isBlockHighlighted, __unstableIsFullySelected, @@ -550,9 +542,6 @@ function BlockListBlockProvider( props ) { const canRemove = canRemoveBlock( clientId, rootClientId ); const canMove = canMoveBlock( clientId, rootClientId ); const { name: blockName, attributes, isValid } = block; - const isPartOfMultiSelection = - isBlockMultiSelected( clientId ) || - isAncestorMultiSelected( clientId ); const blockType = getBlockType( blockName ); const match = getActiveBlockVariation( blockName, attributes ); const { outlineMode, supportsLayout } = getSettings(); @@ -600,12 +589,6 @@ function BlockListBlockProvider( props ) { index: getBlockIndex( clientId ), blockApiVersion: blockType?.apiVersion || 1, blockTitle: match?.title || blockType?.title, - isPartOfSelection: _isSelected || isPartOfMultiSelection, - adjustScrolling: - _isSelected || isFirstMultiSelectedBlock( clientId ), - enableAnimation: - ! typing && - getGlobalBlockCount() <= BLOCK_ANIMATION_THRESHOLD, isSubtreeDisabled: isBlockSubtreeDisabled( clientId ), isOutlineEnabled: outlineMode, hasOverlay: __unstableHasActiveBlockOverlayActive( clientId ), @@ -662,9 +645,6 @@ function BlockListBlockProvider( props ) { index, blockApiVersion, blockTitle, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, @@ -699,9 +679,6 @@ function BlockListBlockProvider( props ) { blockApiVersion, blockTitle, isSelected, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, diff --git a/packages/block-editor/src/components/block-list/use-block-props/index.js b/packages/block-editor/src/components/block-list/use-block-props/index.js index fea20506c28a1f..9c7ae30b5997a5 100644 --- a/packages/block-editor/src/components/block-list/use-block-props/index.js +++ b/packages/block-editor/src/components/block-list/use-block-props/index.js @@ -80,9 +80,6 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { blockApiVersion, blockTitle, isSelected, - isPartOfSelection, - adjustScrolling, - enableAnimation, isSubtreeDisabled, isOutlineEnabled, hasOverlay, @@ -114,12 +111,7 @@ export function useBlockProps( props = {}, { __unstableIsHtml } = {} ) { useNavModeExit( clientId ), useIsHovered( { isEnabled: isOutlineEnabled } ), useIntersectionObserver(), - useMovingAnimation( { - isSelected: isPartOfSelection, - adjustScrolling, - enableAnimation, - triggerAnimationOnChange: index, - } ), + useMovingAnimation( { triggerAnimationOnChange: index, clientId } ), useDisabled( { isDisabled: ! hasOverlay } ), ] ); diff --git a/packages/block-editor/src/components/use-moving-animation/index.js b/packages/block-editor/src/components/use-moving-animation/index.js index e6c888330fca71..4a66fe6fb6e637 100644 --- a/packages/block-editor/src/components/use-moving-animation/index.js +++ b/packages/block-editor/src/components/use-moving-animation/index.js @@ -1,35 +1,32 @@ /** * External dependencies */ -import { useSpring } from '@react-spring/web'; +import { Controller } from '@react-spring/web'; /** * WordPress dependencies */ -import { - useState, - useLayoutEffect, - useReducer, - useMemo, - useRef, -} from '@wordpress/element'; -import { useReducedMotion } from '@wordpress/compose'; +import { useLayoutEffect, useMemo, useRef } from '@wordpress/element'; import { getScrollContainer } from '@wordpress/dom'; +import { useSelect } from '@wordpress/data'; /** - * Simple reducer used to increment a counter. - * - * @param {number} state Previous counter value. - * @return {number} New state value. + * Internal dependencies */ -const counterReducer = ( state ) => state + 1; +import { store as blockEditorStore } from '../../store'; -const getAbsolutePosition = ( element ) => { +/** + * If the block count exceeds the threshold, we disable the reordering animation + * to avoid laginess. + */ +const BLOCK_ANIMATION_THRESHOLD = 200; + +function getAbsolutePosition( element ) { return { top: element.offsetTop, left: element.offsetLeft, }; -}; +} /** * Hook used to compute the styles required to move a div into a new position. @@ -42,114 +39,121 @@ const getAbsolutePosition = ( element ) => { * - It uses the "resetAnimation" flag to reset the animation * from the beginning in order to animate to the new destination point. * - * @param {Object} $1 Options - * @param {boolean} $1.isSelected Whether it's the current block or not. - * @param {boolean} $1.adjustScrolling Adjust the scroll position to the current block. - * @param {boolean} $1.enableAnimation Enable/Disable animation. - * @param {*} $1.triggerAnimationOnChange Variable used to trigger the animation if it changes. + * @param {Object} $1 Options + * @param {*} $1.triggerAnimationOnChange Variable used to trigger the animation if it changes. + * @param {string} $1.clientId */ -function useMovingAnimation( { - isSelected, - adjustScrolling, - enableAnimation, - triggerAnimationOnChange, -} ) { +function useMovingAnimation( { triggerAnimationOnChange, clientId } ) { const ref = useRef(); - const prefersReducedMotion = useReducedMotion() || ! enableAnimation; - const [ triggeredAnimation, triggerAnimation ] = useReducer( - counterReducer, - 0 - ); - const [ finishedAnimation, endAnimation ] = useReducer( counterReducer, 0 ); - const [ transform, setTransform ] = useState( { x: 0, y: 0 } ); - const previous = useMemo( - () => ( ref.current ? getAbsolutePosition( ref.current ) : null ), + const { + isTyping, + getGlobalBlockCount, + isBlockSelected, + isFirstMultiSelectedBlock, + isBlockMultiSelected, + isAncestorMultiSelected, + } = useSelect( blockEditorStore ); + + // Whenever the trigger changes, we need to take a snapshot of the current + // position of the block to use it as a destination point for the animation. + const { previous, prevRect } = useMemo( + () => ( { + previous: ref.current && getAbsolutePosition( ref.current ), + prevRect: ref.current && ref.current.getBoundingClientRect(), + } ), + // eslint-disable-next-line react-hooks/exhaustive-deps [ triggerAnimationOnChange ] ); - // Calculate the previous position of the block relative to the viewport and - // return a function to maintain that position by scrolling. - const preserveScrollPosition = useMemo( () => { - if ( ! adjustScrolling || ! ref.current ) { - return () => {}; + useLayoutEffect( () => { + if ( ! previous || ! ref.current ) { + return; } const scrollContainer = getScrollContainer( ref.current ); - - if ( ! scrollContainer ) { - return () => {}; - } - - const prevRect = ref.current.getBoundingClientRect(); - return () => { - const blockRect = ref.current.getBoundingClientRect(); - const diff = blockRect.top - prevRect.top; - - if ( diff ) { - scrollContainer.scrollTop += diff; + const isSelected = isBlockSelected( clientId ); + const adjustScrolling = + isSelected || isFirstMultiSelectedBlock( clientId ); + + function preserveScrollPosition() { + if ( adjustScrolling && prevRect ) { + const blockRect = ref.current.getBoundingClientRect(); + const diff = blockRect.top - prevRect.top; + + if ( diff ) { + scrollContainer.scrollTop += diff; + } } - }; - }, [ triggerAnimationOnChange, adjustScrolling ] ); - - useLayoutEffect( () => { - if ( triggeredAnimation ) { - endAnimation(); - } - }, [ triggeredAnimation ] ); - useLayoutEffect( () => { - if ( ! previous ) { - return; } - if ( prefersReducedMotion ) { + // We disable the animation if the user has a preference for reduced + // motion, if the user is typing (insertion by Enter), or if the block + // count exceeds the threshold (insertion caused all the blocks that + // follow to animate). + // To do: consider enableing the _moving_ animation even for large + // posts, while only disabling the _insertion_ animation? + const disableAnimation = + window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches || + isTyping() || + getGlobalBlockCount() > BLOCK_ANIMATION_THRESHOLD; + + if ( disableAnimation ) { // If the animation is disabled and the scroll needs to be adjusted, // just move directly to the final scroll position. preserveScrollPosition(); - return; } + const isPartOfSelection = + isSelected || + isBlockMultiSelected( clientId ) || + isAncestorMultiSelected( clientId ); + // Make sure the other blocks move under the selected block(s). + const zIndex = isPartOfSelection ? '1' : ''; + + const controller = new Controller( { + x: 0, + y: 0, + config: { mass: 5, tension: 2000, friction: 200 }, + onChange( { value } ) { + if ( ! ref.current ) { + return; + } + let { x, y } = value; + x = Math.round( x ); + y = Math.round( y ); + const finishedMoving = x === 0 && y === 0; + ref.current.style.transformOrigin = 'center center'; + ref.current.style.transform = finishedMoving + ? null // Set to `null` to explicitly remove the transform. + : `translate3d(${ x }px,${ y }px,0)`; + ref.current.style.zIndex = zIndex; + preserveScrollPosition(); + }, + } ); + ref.current.style.transform = undefined; const destination = getAbsolutePosition( ref.current ); - triggerAnimation(); - setTransform( { - x: Math.round( previous.left - destination.left ), - y: Math.round( previous.top - destination.top ), - } ); - }, [ triggerAnimationOnChange ] ); + const x = Math.round( previous.left - destination.left ); + const y = Math.round( previous.top - destination.top ); - function onChange( { value } ) { - if ( ! ref.current ) { - return; - } - let { x, y } = value; - x = Math.round( x ); - y = Math.round( y ); - const finishedMoving = x === 0 && y === 0; - ref.current.style.transformOrigin = 'center center'; - ref.current.style.transform = finishedMoving - ? null // Set to `null` to explicitly remove the transform. - : `translate3d(${ x }px,${ y }px,0)`; - ref.current.style.zIndex = isSelected ? '1' : ''; - - preserveScrollPosition(); - } - - useSpring( { - from: { - x: transform.x, - y: transform.y, - }, - to: { - x: 0, - y: 0, - }, - reset: triggeredAnimation !== finishedAnimation, - config: { mass: 5, tension: 2000, friction: 200 }, - immediate: prefersReducedMotion, - onChange, - } ); + controller.start( { x: 0, y: 0, from: { x, y } } ); + + return () => { + controller.stop(); + }; + }, [ + previous, + prevRect, + clientId, + isTyping, + getGlobalBlockCount, + isBlockSelected, + isFirstMultiSelectedBlock, + isBlockMultiSelected, + isAncestorMultiSelected, + ] ); return ref; } From e8c4fa9fdeed7999c80406e5941469d41d010ed9 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Sun, 17 Dec 2023 00:44:31 +0100 Subject: [PATCH 222/325] Performance: Prevent layout re-rendering when changing selected block (#57136) --- packages/edit-post/src/components/layout/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index eb67cc82783e4a..6d51b2cf175c3b 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -197,7 +197,7 @@ function Layout() { // translators: Default label for the Document in the Block Breadcrumb. documentLabel: postTypeLabel || _x( 'Document', 'noun' ), hasBlockSelected: - select( blockEditorStore ).getBlockSelectionStart(), + !! select( blockEditorStore ).getBlockSelectionStart(), }; }, [] ); From cdeca67b63635cca295ba8f4874130c53f394ab0 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Sun, 17 Dec 2023 10:38:09 +0900 Subject: [PATCH 223/325] Add missing period in block descriptions (#57131) Co-authored-by: Jb Audras <audrasjb@gmail.com> --- docs/reference-guides/core-blocks.md | 4 ++-- packages/block-library/src/comments-title/block.json | 2 +- .../block-library/src/query-pagination-numbers/block.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index c68bb419467f36..2ba78177e9ec60 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -225,7 +225,7 @@ Displays the previous comment's page link. ([Source](https://github.com/WordPres ## Comments Title -Displays a title with the number of comments ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/comments-title)) +Displays a title with the number of comments. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/comments-title)) - **Name:** core/comments-title - **Category:** theme @@ -738,7 +738,7 @@ Displays the next posts page link. ([Source](https://github.com/WordPress/gutenb ## Page Numbers -Displays a list of page numbers for pagination ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/query-pagination-numbers)) +Displays a list of page numbers for pagination. ([Source](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src/query-pagination-numbers)) - **Name:** core/query-pagination-numbers - **Category:** theme diff --git a/packages/block-library/src/comments-title/block.json b/packages/block-library/src/comments-title/block.json index 12b105afe9a31c..4107f5d590cdee 100644 --- a/packages/block-library/src/comments-title/block.json +++ b/packages/block-library/src/comments-title/block.json @@ -5,7 +5,7 @@ "title": "Comments Title", "category": "theme", "ancestor": [ "core/comments" ], - "description": "Displays a title with the number of comments", + "description": "Displays a title with the number of comments.", "textdomain": "default", "usesContext": [ "postId", "postType" ], "attributes": { diff --git a/packages/block-library/src/query-pagination-numbers/block.json b/packages/block-library/src/query-pagination-numbers/block.json index f05e269d2ece20..f22d88115d68cd 100644 --- a/packages/block-library/src/query-pagination-numbers/block.json +++ b/packages/block-library/src/query-pagination-numbers/block.json @@ -5,7 +5,7 @@ "title": "Page Numbers", "category": "theme", "parent": [ "core/query-pagination" ], - "description": "Displays a list of page numbers for pagination", + "description": "Displays a list of page numbers for pagination.", "textdomain": "default", "attributes": { "midSize": { From 2c79c9ed3a1cdced15d9d09f2da6320990b2eadc Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Sun, 17 Dec 2023 17:46:24 +0100 Subject: [PATCH 224/325] Prevent re-rendering the editor header when the selected block changes (#57140) --- .../src/components/navigable-toolbar/index.js | 10 +++------- .../edit-post/src/components/header/index.js | 16 +++++++--------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 8954f7e17f132e..1a85fa0f3d7fb4 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -164,12 +164,7 @@ function useToolbarFocus( { }; }, [ initialIndex, initialFocusOnMount, onIndexChange, toolbarRef ] ); - const { lastFocus } = useSelect( ( select ) => { - const { getLastFocus } = select( blockEditorStore ); - return { - lastFocus: getLastFocus(), - }; - }, [] ); + const { getLastFocus } = useSelect( blockEditorStore ); /** * Handles returning focus to the block editor canvas when pressing escape. */ @@ -178,6 +173,7 @@ function useToolbarFocus( { if ( focusEditorOnEscape ) { const handleKeyDown = ( event ) => { + const lastFocus = getLastFocus(); if ( event.keyCode === ESCAPE && lastFocus?.current ) { // Focus the last focused element when pressing escape. event.preventDefault(); @@ -192,7 +188,7 @@ function useToolbarFocus( { ); }; } - }, [ focusEditorOnEscape, lastFocus, toolbarRef ] ); + }, [ focusEditorOnEscape, getLastFocus, toolbarRef ] ); } export default function NavigableToolbar( { diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index c86b24b4b7ccf8..d6871f95f036ac 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -64,7 +64,7 @@ function Header( { const isLargeViewport = useViewportMatch( 'medium' ); const blockToolbarRef = useRef(); const { - blockSelectionStart, + hasBlockSelection, hasActiveMetaboxes, hasFixedToolbar, isEditingTemplate, @@ -74,8 +74,8 @@ function Header( { const { get: getPreference } = select( preferencesStore ); return { - blockSelectionStart: - select( blockEditorStore ).getBlockSelectionStart(), + hasBlockSelection: + !! select( blockEditorStore ).getBlockSelectionStart(), hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), isEditingTemplate: select( editorStore ).getRenderingMode() === 'template-only', @@ -90,14 +90,12 @@ function Header( { const [ isBlockToolsCollapsed, setIsBlockToolsCollapsed ] = useState( true ); - const hasBlockSelected = !! blockSelectionStart; - useEffect( () => { // If we have a new block selection, show the block tools - if ( blockSelectionStart ) { + if ( hasBlockSelection ) { setIsBlockToolsCollapsed( false ); } - }, [ blockSelectionStart ] ); + }, [ hasBlockSelection ] ); return ( <div className="edit-post-header"> @@ -136,7 +134,7 @@ function Header( { ref={ blockToolbarRef } name="block-toolbar" /> - { isEditingTemplate && hasBlockSelected && ( + { isEditingTemplate && hasBlockSelection && ( <Button className="edit-post-header__block-tools-toggle" icon={ isBlockToolsCollapsed ? next : previous } @@ -158,7 +156,7 @@ function Header( { className={ classnames( 'edit-post-header__center', { 'is-collapsed': isEditingTemplate && - hasBlockSelected && + hasBlockSelection && ! isBlockToolsCollapsed && hasFixedToolbar && isLargeViewport, From 1ac1d88898a1bbf3d987db434e44d8e211fd4615 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:02:03 +1100 Subject: [PATCH 225/325] StylesPreview: Fix endless loop of ratio calculations when on the threshold of a scrollbar (#57090) * StylesPreview: Fix endless loop of ratio calculations when on the threshold of a scrollbar * Revert stray line --- .../src/components/global-styles/preview.js | 58 ++++++++++++++++++- 1 file changed, 55 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/preview.js b/packages/edit-site/src/components/global-styles/preview.js index 2cb9ca49bc7cef..4e48c7ab76e760 100644 --- a/packages/edit-site/src/components/global-styles/preview.js +++ b/packages/edit-site/src/components/global-styles/preview.js @@ -11,8 +11,12 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; -import { useReducedMotion, useResizeObserver } from '@wordpress/compose'; -import { useState, useMemo } from '@wordpress/element'; +import { + useThrottle, + useReducedMotion, + useResizeObserver, +} from '@wordpress/compose'; +import { useLayoutEffect, useState, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -60,6 +64,13 @@ const normalizedHeight = 152; const normalizedColorSwatchSize = 32; +// Throttle options for useThrottle. Must be defined outside of the component, +// so that the object reference is the same on each render. +const THROTTLE_OPTIONS = { + leading: true, + trailing: true, +}; + const StylesPreview = ( { label, isFocused, withHoverView } ) => { const [ fontWeight ] = useGlobalStyle( 'typography.fontWeight' ); const [ fontFamily = 'serif' ] = useGlobalStyle( 'typography.fontFamily' ); @@ -79,7 +90,47 @@ const StylesPreview = ( { label, isFocused, withHoverView } ) => { const disableMotion = useReducedMotion(); const [ isHovered, setIsHovered ] = useState( false ); const [ containerResizeListener, { width } ] = useResizeObserver(); - const ratio = width ? width / normalizedWidth : 1; + const [ throttledWidth, setThrottledWidthState ] = useState( width ); + const [ ratioState, setRatioState ] = useState(); + + const setThrottledWidth = useThrottle( + setThrottledWidthState, + 250, + THROTTLE_OPTIONS + ); + + // Must use useLayoutEffect to avoid a flash of the iframe at the wrong + // size before the width is set. + useLayoutEffect( () => { + if ( width ) { + setThrottledWidth( width ); + } + }, [ width, setThrottledWidth ] ); + + // Must use useLayoutEffect to avoid a flash of the iframe at the wrong + // size before the width is set. + useLayoutEffect( () => { + const newRatio = throttledWidth ? throttledWidth / normalizedWidth : 1; + const ratioDiff = newRatio - ( ratioState || 0 ); + + // Only update the ratio state if the difference is big enough + // or if the ratio state is not yet set. This is to avoid an + // endless loop of updates at particular viewport heights when the + // presence of a scrollbar causes the width to change slightly. + const isRatioDiffBigEnough = Math.abs( ratioDiff ) > 0.1; + + if ( isRatioDiffBigEnough || ! ratioState ) { + setRatioState( newRatio ); + } + }, [ throttledWidth, ratioState ] ); + + // Set a fallbackRatio to use before the throttled ratio has been set. + const fallbackRatio = width ? width / normalizedWidth : 1; + // Use the throttled ratio if it has been calculated, otherwise + // use the fallback ratio. The throttled ratio is used to avoid + // an endless loop of updates at particular viewport heights. + // See: https://github.com/WordPress/gutenberg/issues/55112 + const ratio = ratioState ? ratioState : fallbackRatio; const { paletteColors, highlightedColors } = useStylesPreviewColors(); @@ -108,6 +159,7 @@ const StylesPreview = ( { label, isFocused, withHoverView } ) => { <Iframe className="edit-site-global-styles-preview__iframe" style={ { + width: '100%', height: normalizedHeight * ratio, } } onMouseEnter={ () => setIsHovered( true ) } From bf8614e49f2e79e17d646bb6bd1ecf512456252b Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Mon, 18 Dec 2023 14:50:39 +1100 Subject: [PATCH 226/325] Global styles revisions e2e: tidy up selectors to open revisions panel (#57146) * Since https://github.com/WordPress/gutenberg/pull/56454 the revisions button is no longer in the drop down actions list so we don't need to click it. * delete openRevisions --- .../user-global-styles-revisions.spec.js | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index a27bb28adbb911..8c37f6e48ac485 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -49,7 +49,7 @@ test.describe( 'Global styles revisions', () => { await editor.saveSiteEditorEntities(); // Now there should be enough revisions to show the revisions UI. - await userGlobalStylesRevisions.openRevisions(); + await page.getByRole( 'button', { name: 'Revisions' } ).click(); const revisionButtons = page.getByRole( 'button', { name: /^Changes saved by /, @@ -81,7 +81,7 @@ test.describe( 'Global styles revisions', () => { .getByRole( 'option', { name: 'Color: Luminous vivid amber' } ) .click( { force: true } ); - await userGlobalStylesRevisions.openRevisions(); + await page.getByRole( 'button', { name: 'Revisions' } ).click(); const unSavedButton = page.getByRole( 'button', { name: /^Unsaved changes/, @@ -117,7 +117,7 @@ test.describe( 'Global styles revisions', () => { } ) => { await editor.canvas.locator( 'body' ).click(); await userGlobalStylesRevisions.openStylesPanel(); - await userGlobalStylesRevisions.openRevisions(); + await page.getByRole( 'button', { name: 'Revisions' } ).click(); const lastRevisionButton = page .getByLabel( 'Global styles revisions' ) .getByRole( 'button' ) @@ -147,13 +147,6 @@ class UserGlobalStylesRevisions { return []; } - async openRevisions() { - await this.page - .getByRole( 'menubar', { name: 'Styles actions' } ) - .click(); - await this.page.getByRole( 'button', { name: 'Revisions' } ).click(); - } - async openStylesPanel() { await this.page .getByRole( 'region', { name: 'Editor top bar' } ) From e290be5e0bc675fd4d0e33d2f023484ee099b75e Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:23:37 +1100 Subject: [PATCH 227/325] Global styles: simplify the conditions in GlobalStylesEditorCanvasContainerLink (#57144) * Simplifying the conditions in GlobalStylesEditorCanvasContainerLink * Updating e2e tests --- .../screen-revisions/revisions-buttons.js | 2 +- .../src/components/global-styles/ui.js | 30 +++++++------------ .../index.js | 2 +- .../specs/site-editor/command-center.spec.js | 15 ++++++++++ .../user-global-styles-revisions.spec.js | 19 +++++++++++- 5 files changed, 46 insertions(+), 22 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index 08930069425729..9eff635bbc4a6a 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -138,7 +138,7 @@ function RevisionsButtons( { return ( <ol className="edit-site-global-styles-screen-revisions__revisions-list" - aria-label={ __( 'Global styles revisions' ) } + aria-label={ __( 'Global styles revisions list' ) } role="group" > { userRevisions.map( ( revision, index ) => { diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 8f602b1abb3934..2a6af2e19229c6 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -230,7 +230,7 @@ function GlobalStylesBlockLink() { } function GlobalStylesEditorCanvasContainerLink() { - const { goTo, location } = useNavigator(); + const { goTo } = useNavigator(); const editorCanvasContainerView = useSelect( ( select ) => unlock( select( editSiteStore ) ).getEditorCanvasContainerView(), @@ -241,25 +241,17 @@ function GlobalStylesEditorCanvasContainerLink() { // to the appropriate screen. This effectively allows deep linking to the // desired screens from outside the global styles navigation provider. useEffect( () => { - if ( editorCanvasContainerView === 'global-styles-revisions' ) { - // Switching to the revisions container view should - // redirect to the revisions screen. - goTo( '/revisions' ); - } else if ( - !! editorCanvasContainerView && - location?.path === '/revisions' - ) { - // Switching to any container other than revisions should - // redirect from the revisions screen to the root global styles screen. - goTo( '/' ); - } else if ( editorCanvasContainerView === 'global-styles-css' ) { - goTo( '/css' ); + switch ( editorCanvasContainerView ) { + case 'global-styles-revisions': + goTo( '/revisions' ); + break; + case 'global-styles-css': + goTo( '/css' ); + break; + default: + goTo( '/' ); + break; } - - // location?.path is not a dependency because we don't want to track it. - // Doing so will cause an infinite loop. We could abstract logic to avoid - // having to disable the check later. - // eslint-disable-next-line react-hooks/exhaustive-deps }, [ editorCanvasContainerView, goTo ] ); } diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-details-footer/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-details-footer/index.js index 77b19a50c8af44..899911df3a13b9 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-details-footer/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-details-footer/index.js @@ -42,7 +42,7 @@ export default function SidebarNavigationScreenDetailsFooter( { return ( <ItemGroup className="edit-site-sidebar-navigation-screen-details-footer"> <SidebarNavigationItem - label={ __( 'Revisions' ) } + aria-label={ __( 'Revisions' ) } { ...hrefProps } { ...otherProps } > diff --git a/test/e2e/specs/site-editor/command-center.spec.js b/test/e2e/specs/site-editor/command-center.spec.js index 60c5ec30b1247f..6608f37f1701b3 100644 --- a/test/e2e/specs/site-editor/command-center.spec.js +++ b/test/e2e/specs/site-editor/command-center.spec.js @@ -47,4 +47,19 @@ test.describe( 'Site editor command palette', () => { 'Index' ); } ); + + test( 'Open the command palette and navigate to Customize CSS', async ( { + page, + } ) => { + await page + .getByRole( 'button', { name: 'Open command palette' } ) + .click(); + await page.keyboard.type( 'Customize' ); + await page.getByRole( 'option', { name: 'customize css' } ).click(); + await expect( + page + .getByRole( 'region', { name: 'Editor settings' } ) + .getByLabel( 'Additional CSS' ) + ).toBeVisible(); + } ); } ); diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 8c37f6e48ac485..fe917425f6e2cf 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -119,7 +119,7 @@ test.describe( 'Global styles revisions', () => { await userGlobalStylesRevisions.openStylesPanel(); await page.getByRole( 'button', { name: 'Revisions' } ).click(); const lastRevisionButton = page - .getByLabel( 'Global styles revisions' ) + .getByLabel( 'Global styles revisions list' ) .getByRole( 'button' ) .last(); await expect( lastRevisionButton ).toContainText( 'Default styles' ); @@ -128,6 +128,23 @@ test.describe( 'Global styles revisions', () => { page.getByRole( 'button', { name: 'Reset to defaults' } ) ).toBeVisible(); } ); + + test( 'should access from the site editor sidebar', async ( { page } ) => { + const navigationContainer = page.getByRole( 'region', { + name: 'Navigation', + } ); + await navigationContainer + .getByRole( 'button', { name: 'Styles' } ) + .click(); + + await navigationContainer + .getByRole( 'button', { name: 'Revisions' } ) + .click(); + + await expect( + page.getByLabel( 'Global styles revisions list' ) + ).toBeVisible(); + } ); } ); class UserGlobalStylesRevisions { From df9f8579bf65b7811e0f125f26dbe720bbbb608f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BA=86?= <31400297+ddryo@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:59:12 +0900 Subject: [PATCH 228/325] Replace isSmall prop #53560 (#53599) Co-authored-by: Lena Morita <lena@jaguchi.com> --- packages/block-editor/src/components/block-card/index.js | 2 +- .../src/components/border-radius-control/linked-button.js | 2 +- .../src/components/image-size-control/index.js | 7 +++++-- .../input-controls/spacing-input-control.js | 2 +- .../block-library/src/cover/edit/inspector-controls.js | 2 +- packages/block-library/src/search/edit.js | 2 +- .../border-box-control-linked-button/component.tsx | 2 +- .../border-control-style-picker/component.tsx | 2 +- packages/components/src/box-control/index.tsx | 2 +- packages/components/src/box-control/linked-button.tsx | 2 +- packages/components/src/color-picker/color-copy-button.tsx | 2 +- packages/components/src/font-size-picker/index.tsx | 2 +- packages/components/src/navigation/menu/menu-title.tsx | 2 +- packages/components/src/number-control/index.tsx | 4 ++-- packages/components/src/palette-edit/index.tsx | 6 +++--- packages/components/src/range-control/index.tsx | 2 +- 16 files changed, 23 insertions(+), 20 deletions(-) diff --git a/packages/block-editor/src/components/block-card/index.js b/packages/block-editor/src/components/block-card/index.js index fd89697a54c5d7..4b5478485d87a3 100644 --- a/packages/block-editor/src/components/block-card/index.js +++ b/packages/block-editor/src/components/block-card/index.js @@ -56,7 +56,7 @@ function BlockCard( { title, icon, description, blockType, className } ) { { minWidth: 24, padding: 0 } } icon={ isRTL() ? chevronRight : chevronLeft } - isSmall + size="small" /> ) } <BlockIcon icon={ icon } showColors /> diff --git a/packages/block-editor/src/components/border-radius-control/linked-button.js b/packages/block-editor/src/components/border-radius-control/linked-button.js index 5d8443e57629e2..82752c91de6470 100644 --- a/packages/block-editor/src/components/border-radius-control/linked-button.js +++ b/packages/block-editor/src/components/border-radius-control/linked-button.js @@ -13,7 +13,7 @@ export default function LinkedButton( { isLinked, ...props } ) { <Button { ...props } className="component-border-radius-control__linked-button" - isSmall + size="small" icon={ isLinked ? link : linkOff } iconSize={ 24 } aria-label={ label } diff --git a/packages/block-editor/src/components/image-size-control/index.js b/packages/block-editor/src/components/image-size-control/index.js index 46e87de60f2fc8..7a333e98f795a1 100644 --- a/packages/block-editor/src/components/image-size-control/index.js +++ b/packages/block-editor/src/components/image-size-control/index.js @@ -87,7 +87,7 @@ export default function ImageSizeControl( { return ( <Button key={ scale } - isSmall + size="small" variant={ isCurrent ? 'primary' : undefined } @@ -104,7 +104,10 @@ export default function ImageSizeControl( { ); } ) } </ButtonGroup> - <Button isSmall onClick={ () => updateDimensions() }> + <Button + size="small" + onClick={ () => updateDimensions() } + > { __( 'Reset' ) } </Button> </HStack> diff --git a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js index 7ef5c17f82943c..c2b6ae30de7ac9 100644 --- a/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js +++ b/packages/block-editor/src/components/spacing-sizes-control/input-controls/spacing-input-control.js @@ -317,7 +317,7 @@ export default function SpacingInputControl( { setShowCustomValueControl( ! showCustomValueControl ); } } isPressed={ showCustomValueControl } - isSmall + size="small" className="spacing-sizes-control__custom-toggle" iconSize={ 24 } /> diff --git a/packages/block-library/src/cover/edit/inspector-controls.js b/packages/block-library/src/cover/edit/inspector-controls.js index 38757f90c2deec..4aa655b7229e50 100644 --- a/packages/block-library/src/cover/edit/inspector-controls.js +++ b/packages/block-library/src/cover/edit/inspector-controls.js @@ -220,7 +220,7 @@ export default function CoverInspectorControls( { <PanelRow> <Button variant="secondary" - isSmall + size="small" className="block-library-cover__reset-button" onClick={ onClearMedia } > diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 52f89d344cdf02..2d39494c282392 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -453,7 +453,7 @@ export default function SearchEdit( { return ( <Button key={ widthValue } - isSmall + size="small" variant={ widthValue === width && widthUnit === '%' diff --git a/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx b/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx index ee76c39742d417..5388a381deb395 100644 --- a/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx +++ b/packages/components/src/border-box-control/border-box-control-linked-button/component.tsx @@ -29,7 +29,7 @@ const BorderBoxControlLinkedButton = ( <View className={ className }> <Button { ...buttonProps } - isSmall + size="small" icon={ isLinked ? link : linkOff } iconSize={ 24 } aria-label={ label } diff --git a/packages/components/src/border-control/border-control-style-picker/component.tsx b/packages/components/src/border-control/border-control-style-picker/component.tsx index ffb44855be9fa4..d0621dd8a7c4d4 100644 --- a/packages/components/src/border-control/border-control-style-picker/component.tsx +++ b/packages/components/src/border-control/border-control-style-picker/component.tsx @@ -63,7 +63,7 @@ const BorderControlStylePicker = ( key={ borderStyle.value } className={ buttonClassName } icon={ borderStyle.icon } - isSmall + size="small" isPressed={ borderStyle.value === value } onClick={ () => onChange( diff --git a/packages/components/src/box-control/index.tsx b/packages/components/src/box-control/index.tsx index cf267aff44352e..c7fcf066c545ce 100644 --- a/packages/components/src/box-control/index.tsx +++ b/packages/components/src/box-control/index.tsx @@ -167,7 +167,7 @@ function BoxControl( { <Button className="component-box-control__reset-button" variant="secondary" - isSmall + size="small" onClick={ handleOnReset } disabled={ ! isDirty } > diff --git a/packages/components/src/box-control/linked-button.tsx b/packages/components/src/box-control/linked-button.tsx index 600261379d5675..595a3c03436756 100644 --- a/packages/components/src/box-control/linked-button.tsx +++ b/packages/components/src/box-control/linked-button.tsx @@ -21,7 +21,7 @@ export default function LinkedButton( { <Button { ...props } className="component-box-control__linked-button" - isSmall + size="small" icon={ isLinked ? link : linkOff } iconSize={ 24 } aria-label={ label } diff --git a/packages/components/src/color-picker/color-copy-button.tsx b/packages/components/src/color-picker/color-copy-button.tsx index 99450b07628c21..11ff7ca52de788 100644 --- a/packages/components/src/color-picker/color-copy-button.tsx +++ b/packages/components/src/color-picker/color-copy-button.tsx @@ -62,7 +62,7 @@ export const ColorCopyButton = ( props: ColorCopyButtonProps ) => { } > <CopyButton - isSmall + size="small" ref={ copyRef } icon={ copy } showTooltip={ false } diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index 38488cf9fbb0e6..d79bc870d33588 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -153,7 +153,7 @@ const UnforwardedFontSizePicker = ( ); } } isPressed={ showCustomValueControl } - isSmall + size="small" /> ) } </Header> diff --git a/packages/components/src/navigation/menu/menu-title.tsx b/packages/components/src/navigation/menu/menu-title.tsx index 8a9c7fcd59963c..43ae0bd45cfb2b 100644 --- a/packages/components/src/navigation/menu/menu-title.tsx +++ b/packages/components/src/navigation/menu/menu-title.tsx @@ -66,7 +66,7 @@ export default function NavigationMenuTitle( { { hasSearch && ( <Button - isSmall + size="small" variant="tertiary" label={ searchButtonLabel } onClick={ () => setIsSearching( true ) } diff --git a/packages/components/src/number-control/index.tsx b/packages/components/src/number-control/index.tsx index fce0a7f76f49ed..320ef4cb87d1da 100644 --- a/packages/components/src/number-control/index.tsx +++ b/packages/components/src/number-control/index.tsx @@ -246,7 +246,7 @@ function UnforwardedNumberControl( <SpinButton className={ spinButtonClasses } icon={ plusIcon } - isSmall + size="small" aria-hidden="true" aria-label={ __( 'Increment' ) } tabIndex={ -1 } @@ -257,7 +257,7 @@ function UnforwardedNumberControl( <SpinButton className={ spinButtonClasses } icon={ resetIcon } - isSmall + size="small" aria-hidden="true" aria-label={ __( 'Decrement' ) } tabIndex={ -1 } diff --git a/packages/components/src/palette-edit/index.tsx b/packages/components/src/palette-edit/index.tsx index 91303471792ebd..ceadceca1a239d 100644 --- a/packages/components/src/palette-edit/index.tsx +++ b/packages/components/src/palette-edit/index.tsx @@ -241,7 +241,7 @@ function Option< T extends Color | Gradient >( { { isEditing && ! canOnlyChangeValues && ( <FlexItem> <RemoveButton - isSmall + size="small" icon={ lineSolid } label={ __( 'Remove color' ) } onClick={ onRemove } @@ -454,7 +454,7 @@ export function PaletteEdit( { <PaletteActionsContainer> { hasElements && isEditing && ( <DoneButton - isSmall + size="small" onClick={ () => { setIsEditing( false ); setEditingElement( null ); @@ -465,7 +465,7 @@ export function PaletteEdit( { ) } { ! canOnlyChangeValues && ( <Button - isSmall + size="small" isPressed={ isAdding } icon={ plus } label={ diff --git a/packages/components/src/range-control/index.tsx b/packages/components/src/range-control/index.tsx index 05b162a492e849..37a5a048becced 100644 --- a/packages/components/src/range-control/index.tsx +++ b/packages/components/src/range-control/index.tsx @@ -328,7 +328,7 @@ function UnforwardedRangeControl( className="components-range-control__reset" disabled={ disabled || value === undefined } variant="secondary" - isSmall + size="small" onClick={ handleOnReset } > { __( 'Reset' ) } From aa246d4119dcf4e23c340840eb68e05adbbe2665 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr <jsnajdr@gmail.com> Date: Mon, 18 Dec 2023 10:42:51 +0100 Subject: [PATCH 229/325] Block Editor: several little refactors (#57107) * Block Editor: several little refactors * Remove check for reusable blocks as this tab is not shown in the block editor since this were merged with patterns. * Flip the no-categories condition for uncategorized * Tidy up the uncategorized condition --------- Co-authored-by: Glen Davies <glen.davies@automattic.com> --- .../pattern-category-previews.js | 26 ++++++++--------- .../use-pattern-categories.js | 29 +++++++++---------- .../src/components/inserter/menu.js | 19 +++--------- .../src/components/inserter/tabs.js | 23 +++++---------- 4 files changed, 37 insertions(+), 60 deletions(-) diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js index 9e5b6373e54d8c..071a9c479003fa 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-previews.js @@ -70,27 +70,27 @@ export function PatternCategoryPreviews( { if ( category.name === allPatternsCategory.name ) { return true; } + if ( category.name === myPatternsCategory.name && pattern.type === PATTERN_TYPES.user ) { return true; } - if ( category.name !== 'uncategorized' ) { - return pattern.categories?.includes( category.name ); - } - // The uncategorized category should show all the patterns without any category - // or with no available category. - const availablePatternCategories = - pattern.categories?.filter( ( cat ) => - availableCategories.find( - ( availableCategory ) => - availableCategory.name === cat - ) - ) ?? []; + if ( category.name === 'uncategorized' ) { + // The uncategorized category should show all the patterns without any category... + if ( ! pattern.categories ) { + return true; + } + + // ...or with no available category. + return ! pattern.categories.some( ( catName ) => + availableCategories.some( ( c ) => c.name === catName ) + ); + } - return availablePatternCategories.length === 0; + return pattern.categories?.includes( category.name ); } ), [ allPatterns, diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js index 12e885954f4bf3..9f4d598ce37cbf 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/use-pattern-categories.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { useMemo, useCallback } from '@wordpress/element'; +import { useMemo } from '@wordpress/element'; import { _x, _n, sprintf } from '@wordpress/i18n'; import { speak } from '@wordpress/a11y'; @@ -17,6 +17,16 @@ import { PATTERN_TYPES, } from './utils'; +function hasRegisteredCategory( pattern, allCategories ) { + if ( ! pattern.categories || ! pattern.categories.length ) { + return false; + } + + return pattern.categories.some( ( cat ) => + allCategories.some( ( category ) => category.name === cat ) + ); +} + export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { const [ patterns, allCategories ] = usePatternsState( undefined, @@ -34,19 +44,6 @@ export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { [ sourceFilter, patterns ] ); - const hasRegisteredCategory = useCallback( - ( pattern ) => { - if ( ! pattern.categories || ! pattern.categories.length ) { - return false; - } - - return pattern.categories.some( ( cat ) => - allCategories.some( ( category ) => category.name === cat ) - ); - }, - [ allCategories ] - ); - // Remove any empty categories. const populatedCategories = useMemo( () => { const categories = allCategories @@ -59,7 +56,7 @@ export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { if ( filteredPatterns.some( - ( pattern ) => ! hasRegisteredCategory( pattern ) + ( pattern ) => ! hasRegisteredCategory( pattern, allCategories ) ) && ! categories.find( ( category ) => category.name === 'uncategorized' @@ -95,7 +92,7 @@ export function usePatternCategories( rootClientId, sourceFilter = 'all' ) { ) ); return categories; - }, [ allCategories, filteredPatterns, hasRegisteredCategory ] ); + }, [ allCategories, filteredPatterns ] ); return populatedCategories; } diff --git a/packages/block-editor/src/components/inserter/menu.js b/packages/block-editor/src/components/inserter/menu.js index cd44b902f491a5..24c099869ae0d6 100644 --- a/packages/block-editor/src/components/inserter/menu.js +++ b/packages/block-editor/src/components/inserter/menu.js @@ -67,26 +67,18 @@ function InserterMenu( insertionIndex: __experimentalInsertionIndex, shouldFocusBlock, } ); - const { showPatterns, inserterItems } = useSelect( + const { showPatterns } = useSelect( ( select ) => { - const { hasAllowedPatterns, getInserterItems } = unlock( - select( blockEditorStore ) - ); + const { hasAllowedPatterns } = unlock( select( blockEditorStore ) ); return { showPatterns: hasAllowedPatterns( destinationRootClientId ), - inserterItems: getInserterItems( destinationRootClientId ), }; }, [ destinationRootClientId ] ); - const hasReusableBlocks = useMemo( () => { - return inserterItems.some( - ( { category } ) => category === 'reusable' - ); - }, [ inserterItems ] ); const mediaCategories = useMediaCategories( destinationRootClientId ); - const showMedia = !! mediaCategories.length; + const showMedia = mediaCategories.length > 0; const onInsert = useCallback( ( blocks, meta, shouldForceFocusBlock ) => { @@ -211,9 +203,7 @@ function InserterMenu( selectedTab === 'patterns' && ! delayedFilterValue && selectedPatternCategory; - const showAsTabs = - ! delayedFilterValue && - ( showPatterns || hasReusableBlocks || showMedia ); + const showAsTabs = ! delayedFilterValue && ( showPatterns || showMedia ); const showMediaPanel = selectedTab === 'media' && ! delayedFilterValue && @@ -267,7 +257,6 @@ function InserterMenu( { showAsTabs && ( <InserterTabs showPatterns={ showPatterns } - showReusableBlocks={ hasReusableBlocks } showMedia={ showMedia } prioritizePatterns={ prioritizePatterns } onSelect={ handleSetSelectedTab } diff --git a/packages/block-editor/src/components/inserter/tabs.js b/packages/block-editor/src/components/inserter/tabs.js index 72b13425bbbe79..27a1c3f944f662 100644 --- a/packages/block-editor/src/components/inserter/tabs.js +++ b/packages/block-editor/src/components/inserter/tabs.js @@ -1,7 +1,6 @@ /** * WordPress dependencies */ -import { useMemo } from '@wordpress/element'; import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; @@ -33,23 +32,15 @@ function InserterTabs( { showPatterns = false, showMedia = false, onSelect, - prioritizePatterns, + prioritizePatterns = false, tabsContents, } ) { - const tabs = useMemo( () => { - const tempTabs = []; - if ( prioritizePatterns && showPatterns ) { - tempTabs.push( patternsTab ); - } - tempTabs.push( blocksTab ); - if ( ! prioritizePatterns && showPatterns ) { - tempTabs.push( patternsTab ); - } - if ( showMedia ) { - tempTabs.push( mediaTab ); - } - return tempTabs; - }, [ prioritizePatterns, showPatterns, showMedia ] ); + const tabs = [ + prioritizePatterns && showPatterns && patternsTab, + blocksTab, + ! prioritizePatterns && showPatterns && patternsTab, + showMedia && mediaTab, + ].filter( Boolean ); return ( <div className="block-editor-inserter__tabs"> From db617ee58dcb9d7b87028b39f9a9bbd7c30433e8 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 18 Dec 2023 19:16:18 +0900 Subject: [PATCH 230/325] Platform Docs: Fix missing link (#57145) * Platform Docs: Fix missing link * Remove link --- platform-docs/docs/basic-concepts/data.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platform-docs/docs/basic-concepts/data.md b/platform-docs/docs/basic-concepts/data.md index bc298073a3a5c0..3bcb06841a95d2 100644 --- a/platform-docs/docs/basic-concepts/data.md +++ b/platform-docs/docs/basic-concepts/data.md @@ -100,7 +100,7 @@ After running this through the parser, we're left with a simple object we can ma This has dramatic implications for how simple and performant we can make our parser. These explicit boundaries also protect damage to a single block from bleeding into other blocks or tarnishing the entire document. It also allows the system to identify unrecognized blocks before rendering them. -_N.B.:_ The defining aspects of blocks are their semantics and the isolation mechanism they provide: in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](/docs/reference-guides/block-api/block-attributes.md) for details. +_N.B.:_ The defining aspects of blocks are their semantics and the isolation mechanism they provide: in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. ### The Anatomy of a Serialized Block From d7eeb320f6b342c2f86afbe882d565589b670701 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:28:38 +0900 Subject: [PATCH 231/325] Image Block: Get lightbox trigger button ref via data-wp-init (#57089) --- packages/block-library/src/image/index.php | 1 + packages/block-library/src/image/view.js | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php index 48e6e0585b96ec..85cf3de57b1275 100644 --- a/packages/block-library/src/image/index.php +++ b/packages/block-library/src/image/index.php @@ -239,6 +239,7 @@ class="lightbox-trigger" type="button" aria-haspopup="dialog" aria-label="' . esc_attr( $aria_label ) . '" + data-wp-init="callbacks.initTriggerButton" data-wp-on--click="actions.showLightbox" data-wp-style--right="context.imageButtonRight" data-wp-style--top="context.imageButtonTop" diff --git a/packages/block-library/src/image/view.js b/packages/block-library/src/image/view.js index 315ed995f26cfc..2d5268e4836cb7 100644 --- a/packages/block-library/src/image/view.js +++ b/packages/block-library/src/image/view.js @@ -230,13 +230,16 @@ const { state, actions, callbacks } = store( 'core/image', { const ctx = getContext(); const { ref } = getElement(); ctx.imageRef = ref; - ctx.lightboxTriggerRef = - ref.parentElement.querySelector( '.lightbox-trigger' ); if ( ref.complete ) { ctx.imageLoaded = true; ctx.imageCurrentSrc = ref.currentSrc; } }, + initTriggerButton() { + const ctx = getContext(); + const { ref } = getElement(); + ctx.lightboxTriggerRef = ref; + }, initLightbox() { const ctx = getContext(); const { ref } = getElement(); From bea46cbe967bb481b12b9f20a6024d3ede609daf Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 18 Dec 2023 13:29:34 +0100 Subject: [PATCH 232/325] Site Editor: Add the Discussion panel (#57150) --- .../sidebar/settings-sidebar/index.js | 4 ++-- .../sidebar-edit-mode/page-panels/index.js | 2 ++ .../sidebar-edit-mode/template-panel/index.js | 2 ++ packages/editor/src/components/index.js | 1 + .../src/components/post-discussion/panel.js} | 17 ++++++++--------- 5 files changed, 15 insertions(+), 11 deletions(-) rename packages/{edit-post/src/components/sidebar/discussion-panel/index.js => editor/src/components/post-discussion/panel.js} (79%) diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 76d1f1b63ad636..8f71b3908d584d 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -13,6 +13,7 @@ import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editorStore, + PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, @@ -24,7 +25,6 @@ import { */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import DiscussionPanel from '../discussion-panel'; import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; @@ -85,7 +85,7 @@ const SidebarContent = ( { <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> <PostExcerptPanel /> - <DiscussionPanel /> + <PostDiscussionPanel /> <PageAttributes /> <MetaBoxes location="side" /> </> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index e350225a1212aa..87be48220ec95e 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -13,6 +13,7 @@ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { + PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, @@ -104,6 +105,7 @@ export default function PagePanels() { <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> <PostExcerptPanel /> + <PostDiscussionPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 157a56b2461712..21903f0066767f 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -4,6 +4,7 @@ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; import { + PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, PostLastRevisionPanel, @@ -68,6 +69,7 @@ export default function TemplatePanel() { <PostTaxonomiesPanel /> <PostFeaturedImagePanel /> <PostExcerptPanel /> + <PostDiscussionPanel /> </> ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index 0ae7ac0824a7fd..d20ba59215b9b1 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -30,6 +30,7 @@ export { default as PostAuthor } from './post-author'; export { default as PostAuthorCheck } from './post-author/check'; export { default as PostAuthorPanel } from './post-author/panel'; export { default as PostComments } from './post-comments'; +export { default as PostDiscussionPanel } from './post-discussion/panel'; export { default as PostExcerpt } from './post-excerpt'; export { default as PostExcerptCheck } from './post-excerpt/check'; export { default as PostExcerptPanel } from './post-excerpt/panel'; diff --git a/packages/edit-post/src/components/sidebar/discussion-panel/index.js b/packages/editor/src/components/post-discussion/panel.js similarity index 79% rename from packages/edit-post/src/components/sidebar/discussion-panel/index.js rename to packages/editor/src/components/post-discussion/panel.js index 3ed175ca66e1e6..8d9a6a691ac901 100644 --- a/packages/edit-post/src/components/sidebar/discussion-panel/index.js +++ b/packages/editor/src/components/post-discussion/panel.js @@ -3,20 +3,19 @@ */ import { __ } from '@wordpress/i18n'; import { PanelBody, PanelRow } from '@wordpress/components'; -import { - PostComments, - PostPingbacks, - PostTypeSupportCheck, - store as editorStore, -} from '@wordpress/editor'; import { useDispatch, useSelect } from '@wordpress/data'; /** - * Module Constants + * Internal dependencies */ +import { store as editorStore } from '../../store'; +import PostTypeSupportCheck from '../post-type-support-check'; +import PostComments from '../post-comments'; +import PostPingbacks from '../post-pingbacks'; + const PANEL_NAME = 'discussion-panel'; -function DiscussionPanel() { +function PostDiscussionPanel() { const { isEnabled, isOpened } = useSelect( ( select ) => { const { isEditorPanelEnabled, isEditorPanelOpened } = select( editorStore ); @@ -55,4 +54,4 @@ function DiscussionPanel() { ); } -export default DiscussionPanel; +export default PostDiscussionPanel; From 7ef77340cdf88a299fbd637b141768adf11da656 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Mon, 18 Dec 2023 21:44:51 +0900 Subject: [PATCH 233/325] e2e: Try to fix flaky font-library test (#57092) --- test/e2e/specs/site-editor/font-library.spec.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/e2e/specs/site-editor/font-library.spec.js b/test/e2e/specs/site-editor/font-library.spec.js index 531398fb495906..8bc7cfb17ea629 100644 --- a/test/e2e/specs/site-editor/font-library.spec.js +++ b/test/e2e/specs/site-editor/font-library.spec.js @@ -10,10 +10,7 @@ test.describe( 'Font Library', () => { } ); test.beforeEach( async ( { admin, editor } ) => { - await admin.visitSiteEditor( { - postId: 'emptytheme//index', - postType: 'wp_template', - } ); + await admin.visitSiteEditor(); await editor.canvas.locator( 'body' ).click(); } ); @@ -35,10 +32,7 @@ test.describe( 'Font Library', () => { } ); test.beforeEach( async ( { admin, editor } ) => { - await admin.visitSiteEditor( { - postId: 'twentytwentythree//index', - postType: 'wp_template', - } ); + await admin.visitSiteEditor(); await editor.canvas.locator( 'body' ).click(); } ); From 8cd528beafc813e192c13d28c2d39289f8e8b433 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 18 Dec 2023 13:57:03 +0100 Subject: [PATCH 234/325] Swap Template: Show the right templates for the right post type (#57149) --- .../editor/src/components/post-template/hooks.js | 12 ++++++------ .../components/post-template/swap-template-button.js | 11 +++++++---- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/editor/src/components/post-template/hooks.js b/packages/editor/src/components/post-template/hooks.js index e676bf66cf2fbd..1529228fe95151 100644 --- a/packages/editor/src/components/post-template/hooks.js +++ b/packages/editor/src/components/post-template/hooks.js @@ -41,21 +41,21 @@ export function useAllowSwitchingTemplates() { ); } -function useTemplates() { +function useTemplates( postType ) { return useSelect( ( select ) => select( coreStore ).getEntityRecords( 'postType', 'wp_template', { per_page: -1, - post_type: 'page', + post_type: postType, } ), - [] + [ postType ] ); } -export function useAvailableTemplates() { +export function useAvailableTemplates( postType ) { const currentTemplateSlug = useCurrentTemplateSlug(); const allowSwitchingTemplate = useAllowSwitchingTemplates(); - const templates = useTemplates(); + const templates = useTemplates( postType ); return useMemo( () => allowSwitchingTemplate && @@ -71,7 +71,7 @@ export function useAvailableTemplates() { export function useCurrentTemplateSlug() { const { postType, postId } = useEditedPostContext(); - const templates = useTemplates(); + const templates = useTemplates( postType ); const entityTemplate = useSelect( ( select ) => { const post = select( coreStore ).getEditedEntityRecord( diff --git a/packages/editor/src/components/post-template/swap-template-button.js b/packages/editor/src/components/post-template/swap-template-button.js index 240dee42214d56..1e9562970f6828 100644 --- a/packages/editor/src/components/post-template/swap-template-button.js +++ b/packages/editor/src/components/post-template/swap-template-button.js @@ -18,11 +18,11 @@ import { useAvailableTemplates, useEditedPostContext } from './hooks'; export default function SwapTemplateButton( { onClick } ) { const [ showModal, setShowModal ] = useState( false ); - const availableTemplates = useAvailableTemplates(); const onClose = useCallback( () => { setShowModal( false ); }, [] ); const { postType, postId } = useEditedPostContext(); + const availableTemplates = useAvailableTemplates( postType ); const { editEntityRecord } = useDispatch( coreStore ); if ( ! availableTemplates?.length ) { return null; @@ -51,7 +51,10 @@ export default function SwapTemplateButton( { onClick } ) { isFullScreen > <div className="editor-post-template__swap-template-modal-content"> - <TemplatesList onSelect={ onTemplateSelect } /> + <TemplatesList + postType={ postType } + onSelect={ onTemplateSelect } + /> </div> </Modal> ) } @@ -59,8 +62,8 @@ export default function SwapTemplateButton( { onClick } ) { ); } -function TemplatesList( { onSelect } ) { - const availableTemplates = useAvailableTemplates(); +function TemplatesList( { postType, onSelect } ) { + const availableTemplates = useAvailableTemplates( postType ); const templatesAsPatterns = useMemo( () => availableTemplates.map( ( template ) => ( { From 1b3f48b17c1dafbb03348a211949e9e9665cc2e7 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Mon, 18 Dec 2023 18:37:23 +0400 Subject: [PATCH 235/325] Block Editor: Try removing extra memoization for individual style panels (#57160) --- packages/block-editor/src/hooks/background.js | 8 +------- packages/block-editor/src/hooks/border.js | 8 +------- packages/block-editor/src/hooks/color.js | 8 +------- packages/block-editor/src/hooks/dimensions.js | 8 +------- packages/block-editor/src/hooks/typography.js | 8 +------- 5 files changed, 5 insertions(+), 35 deletions(-) diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index b75dc95b75241f..e8518bcbc419f2 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -24,7 +24,6 @@ import { Platform, useCallback, useRef } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { getFilename } from '@wordpress/url'; -import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -302,7 +301,7 @@ function BackgroundImagePanelItem( { clientId, setAttributes } ) { ); } -function BackgroundImagePanelPure( props ) { +export function BackgroundImagePanel( props ) { const [ backgroundImage ] = useSettings( 'background.backgroundImage' ); if ( ! backgroundImage || @@ -317,8 +316,3 @@ function BackgroundImagePanelPure( props ) { </InspectorControls> ); } - -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -export const BackgroundImagePanel = pure( BackgroundImagePanelPure ); diff --git a/packages/block-editor/src/hooks/border.js b/packages/block-editor/src/hooks/border.js index a11fdc4b97e48b..e67105969df81c 100644 --- a/packages/block-editor/src/hooks/border.js +++ b/packages/block-editor/src/hooks/border.js @@ -8,7 +8,6 @@ import classnames from 'classnames'; */ import { getBlockSupport } from '@wordpress/blocks'; import { __experimentalHasSplitBorders as hasSplitBorders } from '@wordpress/components'; -import { pure } from '@wordpress/compose'; import { Platform, useCallback, useMemo } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { useSelect } from '@wordpress/data'; @@ -133,7 +132,7 @@ function BordersInspectorControl( { children, resetAllFilter } ) { ); } -function BorderPanelPure( { clientId, name, setAttributes, settings } ) { +export function BorderPanel( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasBorderPanel( settings ); function selector( select ) { const { style, borderColor } = @@ -170,11 +169,6 @@ function BorderPanelPure( { clientId, name, setAttributes, settings } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -export const BorderPanel = pure( BorderPanelPure ); - /** * Determine whether there is block support for border properties. * diff --git a/packages/block-editor/src/hooks/color.js b/packages/block-editor/src/hooks/color.js index 267bafe1201739..5767db829d1b37 100644 --- a/packages/block-editor/src/hooks/color.js +++ b/packages/block-editor/src/hooks/color.js @@ -9,7 +9,6 @@ import classnames from 'classnames'; import { addFilter } from '@wordpress/hooks'; import { getBlockSupport } from '@wordpress/blocks'; import { useMemo, Platform, useCallback } from '@wordpress/element'; -import { pure } from '@wordpress/compose'; import { useSelect } from '@wordpress/data'; /** @@ -267,7 +266,7 @@ function ColorInspectorControl( { children, resetAllFilter } ) { ); } -function ColorEditPure( { clientId, name, setAttributes, settings } ) { +export function ColorEdit( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasColorPanel( settings ); function selector( select ) { const { style, textColor, backgroundColor, gradient } = @@ -336,11 +335,6 @@ function ColorEditPure( { clientId, name, setAttributes, settings } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -export const ColorEdit = pure( ColorEditPure ); - function useBlockProps( { name, backgroundColor, diff --git a/packages/block-editor/src/hooks/dimensions.js b/packages/block-editor/src/hooks/dimensions.js index 4dcba5c4abef68..bbf5b12ca27cf8 100644 --- a/packages/block-editor/src/hooks/dimensions.js +++ b/packages/block-editor/src/hooks/dimensions.js @@ -5,7 +5,6 @@ import { useState, useEffect, useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { getBlockSupport } from '@wordpress/blocks'; import deprecated from '@wordpress/deprecated'; -import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -66,7 +65,7 @@ function DimensionsInspectorControl( { children, resetAllFilter } ) { ); } -function DimensionsPanelPure( { clientId, name, setAttributes, settings } ) { +export function DimensionsPanel( { clientId, name, setAttributes, settings } ) { const isEnabled = useHasDimensionsPanel( settings ); const value = useSelect( ( select ) => @@ -126,11 +125,6 @@ function DimensionsPanelPure( { clientId, name, setAttributes, settings } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -export const DimensionsPanel = pure( DimensionsPanelPure ); - /** * @deprecated */ diff --git a/packages/block-editor/src/hooks/typography.js b/packages/block-editor/src/hooks/typography.js index 7b2fdc9ca28fb2..12d0075527bec5 100644 --- a/packages/block-editor/src/hooks/typography.js +++ b/packages/block-editor/src/hooks/typography.js @@ -4,7 +4,6 @@ import { getBlockSupport, hasBlockSupport } from '@wordpress/blocks'; import { useMemo, useCallback } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; -import { pure } from '@wordpress/compose'; /** * Internal dependencies @@ -109,7 +108,7 @@ function TypographyInspectorControl( { children, resetAllFilter } ) { ); } -function TypographyPanelPure( { clientId, name, setAttributes, settings } ) { +export function TypographyPanel( { clientId, name, setAttributes, settings } ) { function selector( select ) { const { style, fontFamily, fontSize } = select( blockEditorStore ).getBlockAttributes( clientId ) || {}; @@ -147,11 +146,6 @@ function TypographyPanelPure( { clientId, name, setAttributes, settings } ) { ); } -// We don't want block controls to re-render when typing inside a block. `pure` -// will prevent re-renders unless props change, so only pass the needed props -// and not the whole attributes object. -export const TypographyPanel = pure( TypographyPanelPure ); - export const hasTypographySupport = ( blockName ) => { return TYPOGRAPHY_SUPPORT_KEYS.some( ( key ) => hasBlockSupport( blockName, key ) From 44d1e21842b905e8c3e03fc9ee2793ab2fea5c49 Mon Sep 17 00:00:00 2001 From: Carlos Garcia <fluiddot@gmail.com> Date: Mon, 18 Dec 2023 15:52:40 +0100 Subject: [PATCH 236/325] [RNMobile] Fix pasting HTML into the post title (#57118) * Update paste handler of post title This logic is a duplicate of the web implementation. * Allow set initial title in integration tests * Add `getEditorTitle` test helper The file has been renamed to a proper name now that provides different getters for the editor content. * Update index of integration test helpers Now it exports modules using wildcard, instead of needing to specify every single item. * Add integration tests of `PostTitle` component --- .../src/components/post-title/index.native.js | 49 ++++++++---- .../test/__snapshots__/index.native.js.snap | 25 ++++++ .../post-title/test/index.native.js | 78 +++++++++++++++++++ ...t-editor-html.js => get-editor-content.js} | 25 ++++-- test/native/integration-test-helpers/index.js | 46 +++++------ .../initialize-editor.js | 3 +- 6 files changed, 180 insertions(+), 46 deletions(-) create mode 100644 packages/editor/src/components/post-title/test/__snapshots__/index.native.js.snap create mode 100644 packages/editor/src/components/post-title/test/index.native.js rename test/native/integration-test-helpers/{get-editor-html.js => get-editor-content.js} (59%) diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index 6d905e743581e9..d82206303314a3 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -7,7 +7,7 @@ import { View } from 'react-native'; * WordPress dependencies */ import { Component } from '@wordpress/element'; -import { create, insert } from '@wordpress/rich-text'; +import { create, toHTMLString, insert } from '@wordpress/rich-text'; import { decodeEntities } from '@wordpress/html-entities'; import { withDispatch, withSelect } from '@wordpress/data'; import { withFocusOutside } from '@wordpress/components'; @@ -16,6 +16,7 @@ import { __, sprintf } from '@wordpress/i18n'; import { pasteHandler } from '@wordpress/blocks'; import { store as blockEditorStore, RichText } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; /** * Internal dependencies @@ -57,7 +58,7 @@ class PostTitle extends Component { this.props.onSelect(); } - onPaste( { value, onChange, plainText, html } ) { + onPaste( { value, plainText, html } ) { const { title, onInsertBlockAfter, onUpdate } = this.props; const content = pasteHandler( { @@ -65,23 +66,37 @@ class PostTitle extends Component { plainText, } ); - if ( content.length ) { - if ( typeof content === 'string' ) { - const valueToInsert = create( { html: content } ); - onChange( insert( value, valueToInsert ) ); + if ( ! content.length ) { + return; + } + + if ( typeof content !== 'string' ) { + const [ firstBlock ] = content; + + if ( + ! title && + ( firstBlock.name === 'core/heading' || + firstBlock.name === 'core/paragraph' ) + ) { + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( + firstBlock.attributes.content + ); + onUpdate( contentNoHTML ); + onInsertBlockAfter( content.slice( 1 ) ); } else { - const [ firstBlock ] = content; - if ( - ! title && - ( firstBlock.name === 'core/heading' || - firstBlock.name === 'core/paragraph' ) - ) { - onUpdate( firstBlock.attributes.content ); - onInsertBlockAfter( content.slice( 1 ) ); - } else { - onInsertBlockAfter( content ); - } + onInsertBlockAfter( content ); } + } else { + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( content ); + + const newValue = insert( value, create( { html: contentNoHTML } ) ); + onUpdate( toHTMLString( { value: newValue } ) ); } } diff --git a/packages/editor/src/components/post-title/test/__snapshots__/index.native.js.snap b/packages/editor/src/components/post-title/test/__snapshots__/index.native.js.snap new file mode 100644 index 00000000000000..d595171b880785 --- /dev/null +++ b/packages/editor/src/components/post-title/test/__snapshots__/index.native.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PostTitle does not update title with existing content when pasting HTML 1`] = ` +"<!-- wp:heading --> +<h2 class="wp-block-heading">Howdy</h2> +<!-- /wp:heading --> + +<!-- wp:heading --> +<h2 class="wp-block-heading">This is a heading.</h2> +<!-- /wp:heading --> + +<!-- wp:paragraph --> +<p>This is a paragraph.</p> +<!-- /wp:paragraph -->" +`; + +exports[`PostTitle populates empty title with first block content when pasting HTML 1`] = ` +"<!-- wp:heading --> +<h2 class="wp-block-heading">This is a heading.</h2> +<!-- /wp:heading --> + +<!-- wp:paragraph --> +<p>This is a paragraph.</p> +<!-- /wp:paragraph -->" +`; diff --git a/packages/editor/src/components/post-title/test/index.native.js b/packages/editor/src/components/post-title/test/index.native.js new file mode 100644 index 00000000000000..1d7cc492f44b8b --- /dev/null +++ b/packages/editor/src/components/post-title/test/index.native.js @@ -0,0 +1,78 @@ +/** + * External dependencies + */ +import { + getEditorHtml, + getEditorTitle, + initializeEditor, + pasteIntoRichText, + selectRangeInRichText, + screen, + setupCoreBlocks, + within, +} from 'test/helpers'; + +setupCoreBlocks(); + +const HTML_MULTIPLE_TAGS = `<h2>Howdy</h2> +<h2>This is a heading.</h2> +<p>This is a paragraph.</p>`; + +describe( 'PostTitle', () => { + it( 'populates empty title with first block content when pasting HTML', async () => { + await initializeEditor( { initialTitle: '' } ); + + const postTitle = within( + screen.getByTestId( 'post-title' ) + ).getByPlaceholderText( 'Add title' ); + pasteIntoRichText( postTitle, { html: HTML_MULTIPLE_TAGS } ); + + expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( 'Howdy' ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'does not update title with existing content when pasting HTML', async () => { + const initialTitle = 'Hello'; + await initializeEditor( { initialTitle } ); + + const postTitle = within( + screen.getByTestId( 'post-title' ) + ).getByPlaceholderText( 'Add title' ); + selectRangeInRichText( postTitle, 0 ); + pasteIntoRichText( postTitle, { html: HTML_MULTIPLE_TAGS } ); + + expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( initialTitle ); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'updates title with existing content when pasting text', async () => { + await initializeEditor( { initialTitle: 'World' } ); + + const postTitle = within( + screen.getByTestId( 'post-title' ) + ).getByPlaceholderText( 'Add title' ); + selectRangeInRichText( postTitle, 0 ); + pasteIntoRichText( postTitle, { text: 'Hello' } ); + + expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( 'HelloWorld' ); + expect( getEditorHtml() ).toBe( '' ); + } ); + + it( 'does not add HTML to title when pasting span tag', async () => { + const pasteHTML = `<span style="border: 1px solid black">l</span>`; + await initializeEditor( { initialTitle: 'Helo' } ); + + const postTitle = within( + screen.getByTestId( 'post-title' ) + ).getByPlaceholderText( 'Add title' ); + selectRangeInRichText( postTitle, 2 ); + pasteIntoRichText( postTitle, { html: pasteHTML } ); + + expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( 'Hello' ); + expect( getEditorHtml() ).toBe( '' ); + } ); +} ); diff --git a/test/native/integration-test-helpers/get-editor-html.js b/test/native/integration-test-helpers/get-editor-content.js similarity index 59% rename from test/native/integration-test-helpers/get-editor-html.js rename to test/native/integration-test-helpers/get-editor-content.js index eb1a9909895d2c..2594382d8ce686 100644 --- a/test/native/integration-test-helpers/get-editor-html.js +++ b/test/native/integration-test-helpers/get-editor-content.js @@ -8,7 +8,7 @@ import { // Set up the mocks for getting the HTML output of the editor let triggerHtmlSerialization; -let serializedHtml; +let serializedContent = {}; subscribeParentGetHtml.mockImplementation( ( callback ) => { if ( ! triggerHtmlSerialization ) { triggerHtmlSerialization = callback; @@ -19,9 +19,11 @@ subscribeParentGetHtml.mockImplementation( ( callback ) => { }; } } ); -provideToNativeHtml.mockImplementation( ( html ) => { - serializedHtml = html; -} ); +provideToNativeHtml.mockImplementation( + ( html, title, hasChanges, contentInfo ) => { + serializedContent = { html, title, hasChanges, contentInfo }; + } +); /** * Gets the current HTML output of the editor. @@ -33,5 +35,18 @@ export function getEditorHtml() { throw new Error( 'HTML serialization trigger is not defined.' ); } triggerHtmlSerialization(); - return serializedHtml; + return serializedContent.html; +} + +/** + * Gets the current title of the editor. + * + * @return {string} Title + */ +export function getEditorTitle() { + if ( ! triggerHtmlSerialization ) { + throw new Error( 'HTML serialization trigger is not defined.' ); + } + triggerHtmlSerialization(); + return serializedContent.title; } diff --git a/test/native/integration-test-helpers/index.js b/test/native/integration-test-helpers/index.js index 5aca46049715cd..43bbb768081c67 100644 --- a/test/native/integration-test-helpers/index.js +++ b/test/native/integration-test-helpers/index.js @@ -3,26 +3,26 @@ export { advanceAnimationByTime, advanceAnimationByFrames, } from './advance-animation'; -export { dismissModal } from './dismiss-modal'; -export { getBlock } from './get-block'; -export { getBlockTransformOptions } from './get-block-transform-options'; -export { getEditorHtml } from './get-editor-html'; -export { getInnerBlock } from './get-inner-block'; -export { initializeEditor } from './initialize-editor'; -export { openBlockActionsMenu } from './open-block-actions-menu'; -export { openBlockSettings } from './open-block-settings'; -export { selectRangeInRichText } from './rich-text-select-range'; -export { typeInRichText } from './rich-text-type'; -export { pasteIntoRichText } from './rich-text-paste'; -export { setupApiFetch } from './setup-api-fetch'; -export { setupCoreBlocks } from './setup-core-blocks'; -export { setupMediaPicker } from './setup-media-picker'; -export { setupMediaUpload } from './setup-media-upload'; -export { setupPicker } from './setup-picker'; -export { changeTextOfTextInput } from './text-input-change-text'; -export { transformBlock } from './transform-block'; -export { triggerBlockListLayout } from './trigger-block-list-layout'; -export { waitForModalVisible } from './wait-for-modal-visible'; -export { waitForStoreResolvers } from './wait-for-store-resolvers'; -export { withFakeTimers } from './with-fake-timers'; -export { withReanimatedTimer } from './with-reanimated-timer'; +export * from './dismiss-modal'; +export * from './get-block'; +export * from './get-block-transform-options'; +export * from './get-editor-content'; +export * from './get-inner-block'; +export * from './initialize-editor'; +export * from './open-block-actions-menu'; +export * from './open-block-settings'; +export * from './rich-text-select-range'; +export * from './rich-text-type'; +export * from './rich-text-paste'; +export * from './setup-api-fetch'; +export * from './setup-core-blocks'; +export * from './setup-media-picker'; +export * from './setup-media-upload'; +export * from './setup-picker'; +export * from './text-input-change-text'; +export * from './transform-block'; +export * from './trigger-block-list-layout'; +export * from './wait-for-modal-visible'; +export * from './wait-for-store-resolvers'; +export * from './with-fake-timers'; +export * from './with-reanimated-timer'; diff --git a/test/native/integration-test-helpers/initialize-editor.js b/test/native/integration-test-helpers/initialize-editor.js index 54ece9d347a07c..511f0223e11356 100644 --- a/test/native/integration-test-helpers/initialize-editor.js +++ b/test/native/integration-test-helpers/initialize-editor.js @@ -36,6 +36,7 @@ export async function initializeEditor( props, { component } = {} ) { const { screenWidth = 320, withGlobalStyles = false, + initialTitle = 'test', ...rest } = props || {}; const editorElement = component @@ -44,7 +45,7 @@ export async function initializeEditor( props, { component } = {} ) { const screen = render( cloneElement( editorElement, { - initialTitle: 'test', + initialTitle, ...( withGlobalStyles ? getGlobalStyles() : {} ), ...rest, } ) From 34dbbe4226d1d2bcc99c0eb33a48e42a5064edcd Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Mon, 18 Dec 2023 09:11:43 -0600 Subject: [PATCH 237/325] Remove "How to use JavaScript" docs (#57166) --- docs/how-to-guides/javascript/README.md | 20 -- docs/how-to-guides/javascript/esnext-js.md | 123 ------------ .../javascript/extending-the-block-editor.md | 64 ------ .../javascript/js-build-setup.md | 182 ------------------ .../javascript/loading-javascript.md | 49 ----- .../javascript/plugins-background.md | 22 --- .../javascript/scope-your-code.md | 117 ----------- .../javascript/troubleshooting.md | 76 -------- .../javascript/versions-and-building.md | 13 -- docs/manifest.json | 54 ------ docs/toc.json | 20 -- 11 files changed, 740 deletions(-) delete mode 100644 docs/how-to-guides/javascript/README.md delete mode 100644 docs/how-to-guides/javascript/esnext-js.md delete mode 100644 docs/how-to-guides/javascript/extending-the-block-editor.md delete mode 100644 docs/how-to-guides/javascript/js-build-setup.md delete mode 100644 docs/how-to-guides/javascript/loading-javascript.md delete mode 100644 docs/how-to-guides/javascript/plugins-background.md delete mode 100644 docs/how-to-guides/javascript/scope-your-code.md delete mode 100644 docs/how-to-guides/javascript/troubleshooting.md delete mode 100644 docs/how-to-guides/javascript/versions-and-building.md diff --git a/docs/how-to-guides/javascript/README.md b/docs/how-to-guides/javascript/README.md deleted file mode 100644 index 4b2d123c8ab136..00000000000000 --- a/docs/how-to-guides/javascript/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# How to use JavaScript with the Block Editor - -The Block Editor Handbook contains information on the APIs available for working with this new setup. The goal of this tutorial is to get you comfortable using the API reference and snippets of code found within. - -### What is JavaScript - -JavaScript is a programming language which is loaded and executed in your web browser; compared to PHP which is run by a web server with the results sent to the browser, typically as HTML. - -The block editor introduced in WordPress 5.0 is written in JavaScript, with the code run in the browser, and not on the server, this allows for a richer and more dynamic user experience. It also requires you to learn how to use JavaScript to extend and enhance the block editor. - -### Table of Contents - -1. [Plugins Background](/docs/how-to-guides/javascript/plugins-background.md) -2. [Loading JavaScript](/docs/how-to-guides/javascript/loading-javascript.md) -3. [Extending the Block Editor](/docs/how-to-guides/javascript/extending-the-block-editor.md) -4. [Troubleshooting](/docs/how-to-guides/javascript/troubleshooting.md) -5. [JavaScript Versions and Building](/docs/how-to-guides/javascript/versions-and-building.md) -6. [Scope your code](/docs/how-to-guides/javascript/scope-your-code.md) -7. [JavaScript Build Step](/docs/how-to-guides/javascript/js-build-setup.md) -8. [ESNext Syntax](/docs/how-to-guides/javascript/esnext-js.md) diff --git a/docs/how-to-guides/javascript/esnext-js.md b/docs/how-to-guides/javascript/esnext-js.md deleted file mode 100644 index f478d1fd596eaa..00000000000000 --- a/docs/how-to-guides/javascript/esnext-js.md +++ /dev/null @@ -1,123 +0,0 @@ -# ESNext Syntax - -The JavaScript language continues to evolve, the syntax used to write JavaScript code is not fixed but changes over time. [Ecma International](https://en.wikipedia.org/wiki/Ecma_International) is the organization that sets the standard for the language, officially called [ECMAScript](https://en.wikipedia.org/wiki/ECMAScript). A new standard for JavaScript is published each year, the 6th edition published in 2015 is often referred to as ES6. Our usage would more appropriately be **ESNext** referring to the latest standard. The build step is what converts this latest syntax of JavaScript to a version understood by browsers. - -Here are some common ESNext syntax patterns used throughout the Gutenberg project. - -## Destructuring Assignments - -The [destructuring assignment](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment) syntax allows you to pull apart arrays, or properties from objects into their own variable. - -For the object `const obj = { foo: "bar" }` - -Creating and assigning a new variable `foo` can be done in a single step: `const { foo } = obj;` - -The curly brackets on the left side tells JavaScript to inspect the object `obj` for the property `foo` and assign its value to the new variable of the same name. - -## Arrow Functions - -Arrow functions provide a shorter syntax for defining a function; this is such a common task in JavaScript that having a syntax a bit shorter is quite helpful. - -Before you might define a function like: - -```js -const f = function ( param ) { - console.log( param ); -}; -``` - -Using arrow function, you can define the same using: - -```js -const g = ( param ) => { - console.log( param ); -}; -``` - -Or even shorter, if the function is only a single-line you can omit the -curly braces: - -```js -const g2 = ( param ) => console.log( param ); -``` - -In the examples above, using `console.log` we aren't too concerned about the return values. However, when using arrow functions in this way, the return value is set whatever the line returns. - -For example, our save function could be shortened from: - -```js -save: ( { attributes } ) => { - return <div className="theurl">{ attributes.url }</div>; -}; -``` - -To: - -```js -save: ( { attributes } ) => <div className="theurl">{ attributes.url }</div>; -``` - -There are even more ways to shorten code, but you don't want to take it too far and make it harder to read what is going on. - -## Imports - -The [import statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import) is used to import variables or functions from an exported file. You can use destructuring on imports, for example: - -```js -import { TextControl } from '@wordpress/components'; -``` - -This will look in the `@wordpress/components` package for the exported `TextControl` variable. - -A package or file can also set a `default` export, this is imported without using the curly brackets. For example - -```js -const edit = ( { attributes, setAttributes } ) => { - return ( - <div> - <TextControl - label="URL" - value={ attributes.url } - onChange={ ... } - /> - </div> - ); -}; - -export default edit; -``` - -To import, you would use: - -```js -import edit from './edit'; - -registerBlockType( 'mkaz/qrcode-block', { - title: 'QRCode Block', - icon: 'visibility', - category: 'widgets', - attributes: { - url: { - type: 'string', - source: 'text', - selector: '.theurl', - }, - }, - edit, - save: ( { attributes } ) => { - return <div> ... </div>; - }, -} ); -``` - -Note, you can also shorten `edit: edit` to just `edit` as shown above. JavaScript will automatically assign the property `edit` to the value of `edit`. This is another form of destructuring. - -## Summary - -It helps to become familiar with the ESNext syntax and the common shorter forms. It will give you a greater understanding of reading code examples and what is going on. - -Here are a few more resources that may help - -- [ES5 vs ES6 with example code](https://medium.com/recraftrelic/es5-vs-es6-with-example-code-9901fa0136fc) -- [Top 10 ES6 Features by Example](https://blog.pragmatists.com/top-10-es6-features-by-example-80ac878794bb) -- [ES6 Syntax and Feature Overview](https://www.taniarascia.com/es6-syntax-and-feature-overview/) diff --git a/docs/how-to-guides/javascript/extending-the-block-editor.md b/docs/how-to-guides/javascript/extending-the-block-editor.md deleted file mode 100644 index 5944b9208407f5..00000000000000 --- a/docs/how-to-guides/javascript/extending-the-block-editor.md +++ /dev/null @@ -1,64 +0,0 @@ -# Extending the Block Editor - -Let's look at using the [Block Style example](/docs/reference-guides/block-api/block-styles.md) to extend the editor. This example allows you to add your own custom CSS class name to any core block type. - -Replace the existing `console.log()` code in your `myguten.js` file with: - -```js -wp.blocks.registerBlockStyle( 'core/quote', { - name: 'fancy-quote', - label: 'Fancy Quote', -} ); -``` - -**Important:** Notice that you are using a function from `wp.blocks` package. This means you must specify it as a dependency when you enqueue the script. Update the `myguten-plugin.php` file to: - -```php -<?php -/* -Plugin Name: Fancy Quote -*/ - -function myguten_enqueue() { - wp_enqueue_script( 'myguten-script', - plugins_url( 'myguten.js', __FILE__ ), - array( 'wp-blocks' ) - ); -} -add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); -``` - -The last argument in the `wp_enqueue_script()` function is an array of dependencies. WordPress makes packages available under the `wp` namespace. In the example, you use `wp.blocks` to access the items that the blocks package exports (in this case the `registerBlockStyle()` function). - -See [Packages](/docs/reference-guides/packages.md) for list of available packages and what objects they export. - -After you have updated both JavaScript and PHP files, go to the block editor and create a new post. - -Add a quote block, and in the right sidebar under Styles, you will see your new Fancy Quote style listed. - -Click the Fancy Quote to select and apply that style to your quote block: - -![Fancy Quote Style in Inspector](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/fancy-quote-in-inspector.png) - -Even if you Preview or Publish the post you will not see a visible change. However, if you look at the source, you will see the `is-style-fancy-quote` class name is now attached to your quote block. - -Let's add some style. In your plugin folder, create a `style.css` file with: - -```css -.is-style-fancy-quote { - color: tomato; -} -``` - -You enqueue the CSS file by adding the following to your `myguten-plugin.php`: - -```php -function myguten_stylesheet() { - wp_enqueue_style( 'myguten-style', plugins_url( 'style.css', __FILE__ ) ); -} -add_action( 'enqueue_block_assets', 'myguten_stylesheet' ); -``` - -Now when you view in the editor and publish, you will see your Fancy Quote style, a delicious tomato color text: - -![Fancy Quote with Style](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/fancy-quote-with-style.png) diff --git a/docs/how-to-guides/javascript/js-build-setup.md b/docs/how-to-guides/javascript/js-build-setup.md deleted file mode 100644 index cf49154b590a96..00000000000000 --- a/docs/how-to-guides/javascript/js-build-setup.md +++ /dev/null @@ -1,182 +0,0 @@ -# JavaScript Build Setup - -ESNext is JavaScript written using syntax and features only available in a version newer than browser support—the support browser versions is referred to as ECMAScript 5 (ES5). [JSX](https://reactjs.org/docs/introducing-jsx.html) is a custom syntax extension to JavaScript, created by React project, that allows you to write JavaScript using a familiar HTML tag-like syntax. - -See the [ESNext syntax documentation](/docs/how-to-guides/javascript/esnext-js.md) for explanation and examples about common code differences between standard JavaScript and ESNext. - -Let's set up your development environment to use these syntaxes, we'll cover development for your plugin to work with the Gutenberg project (ie: the block editor). If you want to develop on Gutenberg itself, see the [Getting Started](/docs/contributors/code/getting-started-with-code-contribution.md) documentation. - -Browsers cannot interpret or run ESNext and JSX syntaxes, so we must use a transformation step to convert these syntaxes to code that browsers can understand. - -There are a few reasons to use ESNext and this extra step of transformation: - -- It makes for simpler code that is easier to read and write. -- Using a transformation step allows for tools to optimize the code to work on the widest variety of browsers. -- By using a build step you can organize your code into smaller modules and files that can be bundled together into a single download. - -There are different tools that can perform this transformation or build step; WordPress uses webpack and Babel. - -[webpack](https://webpack.js.org/) is a pluggable tool that processes JavaScript and creates a compiled bundle that runs in a browser. [Babel](https://babeljs.io/) transforms JavaScript from one format to another. You use Babel as a plugin to webpack to transform both ESNext and JSX to JavaScript. - -The [@wordpress/scripts](https://www.npmjs.com/package/@wordpress/scripts) package abstracts these libraries away to standardize and simplify development, so you won't need to handle the details for configuring webpack or babel. See the [@wordpress/scripts package documentation](https://developer.wordpress.org/block-editor/packages/packages-scripts/) for configuration details. - -## Quick Start - -If you prefer a quick start, you can use one of the examples from the [Block Development Examples repository](https://github.com/wordpress/block-development-examples/) and skip below. Each one of the `-esnext` directories in the examples repository contain the necessary files for working with ESNext and JSX. - -## Setup - -Both webpack and Babel are tools written in JavaScript and run using [Node.js](https://nodejs.org/) (node). Node.js is a runtime environment for JavaScript outside of a browser. Simply put, node allows you to run JavaScript code on the command-line. - -First, you need to set up Node.js for your development environment. The steps required depend on your operating system, if you have a package manager installed, setup can be as straightforward as: - -- Ubuntu: `apt install nodejs npm` -- macOS: `brew install node` -- Windows: `choco install node` - -If you are not using a package manager, see the [developer environment setup documentation](/docs/getting-started/devenv/README.md) for setting up Node using nvm, or see the official [Node.js download page](https://nodejs.org/en/download/) for installers and binaries. - -**Note:** The build tools and process occur on the command-line, so basic familiarity using a terminal application is required. Some text editors have a terminal built-in that is fine to use; Visual Studio Code and PhpStorm are two popular options. - -### Node Package Manager (npm) - -The Node Package Manager (npm) is a tool included with node. npm allows you to install and manage JavaScript packages. npm can also generate and process a special file called `package.json`, that contains information about your project and the packages your project uses. - -To start a new node project, first create a directory to work in: - -```sh -mkdir myguten-block -cd myguten-block -``` - -You create a new package.json running `npm init` in your terminal. This will walk you through creating your package.json file: - -```sh -npm init - -package name: (myguten-block) myguten-block -version: (1.0.0) -description: Test block -entry point: (index.js) build/index.js -test command: -git repository: -keywords: -author: mkaz -license: (ISC) GPL-2.0-only -About to write to /home/mkaz/src/wp/scratch/package.json: - -{ - "name": "myguten-block", - "version": "1.0.0", - "description": "Test block", - "main": "block.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "mkaz", - "license": "GPL-2.0-only" -} - - -Is this OK? (yes) yes -``` - -### Using npm to install packages - -The next step is to install the packages required. You can install packages using the npm command `npm install`. If you pass the `--save-dev` parameter, npm will write the package as a dev dependency in the package.json file. The `--save-exact` parameter instructs npm to save an exact version of a dependency, not a range of valid versions. See [npm install documentation](https://docs.npmjs.com/cli/install) for more details. - -Run `npm install --save-dev --save-exact @wordpress/scripts` - -After installing, a `node_modules` directory is created with the modules and their dependencies. - -Also, if you look at package.json file it will include a new section: - -```json -"devDependencies": { - "@wordpress/scripts": "6.0.0" -} -``` - -## Setting Up wp-scripts build - -The `@wordpress/scripts` package handles the dependencies and default configuration for webpack and Babel. The scripts package expects the source file to compile to be found at `src/index.js`, and will save the compiled output to `build/index.js`. - -With that in mind, let's set up a basic block. Create a file at `src/index.js` with the following content: - -```js -import { registerBlockType } from '@wordpress/blocks'; - -registerBlockType( 'myguten/test-block', { - title: 'Basic Example', - icon: 'smiley', - category: 'design', - edit: () => <div>Hola, mundo!</div>, - save: () => <div>Hola, mundo!</div>, -} ); -``` - -To configure npm to run a script, you use the scripts section in `package.json` webpack: - -```json - "scripts": { - "build": "wp-scripts build" - }, -``` - -You can then run the build using: `npm run build`. - -After the build finishes, you will see the built file created at `build/index.js`. Enqueue this file in the admin screen as you would any JavaScript in WordPress, see [loading JavaScript step in this tutorial](/docs/how-to-guides/javascript/loading-javascript.md), and the block will load in the editor. - -## Development Mode - -The **build** command in `@wordpress/scripts` runs in "production" mode. This shrinks the code down so it downloads faster, but makes it difficult to read in the process. You can use the **start** command which runs in development mode that does not shrink the code, and additionally continues a running process to watch the source file for more changes and rebuilds as you develop. - -The start command can be added to the same scripts section of `package.json`: - -```json - "scripts": { - "start": "wp-scripts start", - "build": "wp-scripts build" - }, -``` - -Now, when you run `npm start` a watcher will run in the terminal. You can then edit away in your text editor; after each save, it will automatically build. You can then use the familiar edit/save/reload development process. - -**Note:** keep an eye on your terminal for any errors. If you make a typo or syntax error, the build will fail and the error will be in the terminal. - -## Source Control - -Because a typical `node_modules` folder will contain thousands of files that change with every software update, you should exclude `node_modules/` from your source control. If you ever start from a fresh clone, simply run `npm install` in the same folder your `package.json` is located to pull your required packages. - -Likewise, you do not need to include `node_modules` or any of the above configuration files in your plugin because they will be bundled inside the file that webpack builds. **Be sure to enqueue the `build/index.js` file** in your plugin PHP. This is the main JavaScript file needed for your block to run. - -## Dependency Management - -Using `wp-scripts` ver 5.0.0+ build step will also produce an `index.asset.php` file that contains an array of dependencies and a version number for your block. For our simple example above, it is something like: -`array('dependencies' => array('react', 'wp-polyfill'), 'version' => 'fc93c4a9675c108725227db345898bcc');` - -Here is how to use this asset file to automatically set the dependency list for enqueuing the script. This prevents having to manually update the dependencies, it will be created based on the package imports used within your block. - -```php -$asset_file = include( plugin_dir_path( __FILE__ ) . 'build/index.asset.php'); - -wp_register_script( - 'myguten-block', - plugins_url( 'build/index.js', __FILE__ ), - $asset_file['dependencies'], - $asset_file['version'] -); -``` - -See [blocks in the block-development-examples repo](https://github.com/WordPress/block-development-examples) for full examples. - -## Summary - -Yes, the initial setup is a bit more involved, but the additional features and benefits are usually worth the trade off in setup time. - -With a setup in place, the standard workflow is: - -1. Install dependencies: `npm install` -2. Start development builds: `npm start` -3. Develop. Test. Repeat. -4. Create production build: `npm run build` diff --git a/docs/how-to-guides/javascript/loading-javascript.md b/docs/how-to-guides/javascript/loading-javascript.md deleted file mode 100644 index 80150f14445a1d..00000000000000 --- a/docs/how-to-guides/javascript/loading-javascript.md +++ /dev/null @@ -1,49 +0,0 @@ -# Loading JavaScript - -With the plugin in place, you can add the code that loads the JavaScript. This methodology follows the standard WordPress procedure of enqueuing scripts, see [enqueuing section of the Plugin Handbook](https://developer.wordpress.org/plugins/javascript/enqueuing/). - -Add the following code to your `myguten-plugin.php` file: - -```php -function myguten_enqueue() { - wp_enqueue_script( - 'myguten-script', - plugins_url( 'myguten.js', __FILE__ ) - ); -} -add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); -``` - -The `enqueue_block_editor_assets` hook is used, which is called when the block editor loads, and will enqueue the JavaScript file `myguten.js`. - -Create a file called `myguten.js` and add: - -```js -console.log( "I'm loaded!" ); -``` - -Next, create a new post in the block editor. - -We'll check the JavaScript console in your browser's Developer Tools, to see if the message is displayed. If you're not sure what developer tools are, Mozilla's ["What are browser developer tools?"](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools) documentation provides more information, including more background on the [JavaScript console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#The_JavaScript_console). - -If your code is registered and enqueued correctly, you should see a message in your console: - -![Console Log Message Success](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/js-tutorial-console-log-success.png) - -**Note for Theme Developers:** The above method of enqueuing is used for plugins. If you are extending the block editor for your theme there is a minor difference, you will use the `get_template_directory_uri()` function instead of `plugins_url()`. So for a theme, the enqueue example is: - -```php -function myguten_enqueue() { - wp_enqueue_script( - 'myguten-script', - get_template_directory_uri() . '/myguten.js' - ); -} -add_action( 'enqueue_block_editor_assets', 'myguten_enqueue' ); -``` - -### Recap - -At this point, you have a plugin in the directory `wp-content/plugins/myguten-plugin` with two files: the PHP server-side code in `myguten-plugin.php`, and the JavaScript which runs in the browser in `myguten.js`. - -This puts all the initial pieces in place for you to start extending the block editor. diff --git a/docs/how-to-guides/javascript/plugins-background.md b/docs/how-to-guides/javascript/plugins-background.md deleted file mode 100644 index 617cbe64e05ede..00000000000000 --- a/docs/how-to-guides/javascript/plugins-background.md +++ /dev/null @@ -1,22 +0,0 @@ -# Plugins Background - -The primary means of extending WordPress is the plugin. The WordPress [Plugin Basics](https://developer.wordpress.org/plugins/plugin-basics/) documentation provides details on building a plugin. - -The quickest way to start is to create a new directory in `wp-content/plugins/` to contain your plugin code. For this example, call it `myguten-plugin`. - -Inside this new directory, create a file called `myguten-plugin.php`. This is the server-side code that runs when your plugin is active. - -For now, add the following code in the file: - -```php -<?php -/* -Plugin Name: Fancy Quote -*/ -``` - -To summarize, you should have a directory `wp-content/plugins/myguten-plugin/` which has the single file `myguten-plugin.php`. - -Once that is in place, go to your plugins list in `wp-admin` and you should see your plugin listed. - -Click **Activate** and your plugin will load with WordPress. diff --git a/docs/how-to-guides/javascript/scope-your-code.md b/docs/how-to-guides/javascript/scope-your-code.md deleted file mode 100644 index 39d52cd6ab934b..00000000000000 --- a/docs/how-to-guides/javascript/scope-your-code.md +++ /dev/null @@ -1,117 +0,0 @@ -# Scope Your Code - -Historically, JavaScript files loaded in a web page share the same scope. This means that a global variable declared in one file will be seen by the code in other files. - -To see how this works, create a web page that loads three JavaScript files. The `first.js` file will be: - -```js -var pluginName = 'MyPlugin'; -console.log( 'Plugin name is ', pluginName ); -``` - -Let's create `second.js` as: - -```js -var pluginName = 'DifferentPlugin'; -console.log( 'Plugin name is ', pluginName ); -``` - -And, finally, `third.js`: - -```js -console.log( 'Plugin name is ', pluginName ); -``` - -When loaded on the same page, `first.js` and `second.js` will output the plugin name declared within itself. They will override the value of the global `pluginName` variable if one was already declared. It's not known what gets printed in the console when `third.js` is executed, though - it depends on the value of the global `pluginName` variable when `third.js` is executed, which will depend on the order the files are loaded. - -This behavior can be problematic, and is the reason we need to scope the code. By scoping the code—ensuring each file is isolated from each other—we can prevent values unexpectedly changing. - -## Scoping Code Within a Function - -In JavaScript, you can scope your code by writing it within a function. Functions have "local scope", or a scope that is specific only to that function. Additionally, in JavaScript you can write anonymous functions, functions without a name, which will also prevent your function name from being overridden in the global scope. - -Taking advantage of these two JavaScript features, `first.js` could be scoped as: - -```js -function() { - var pluginName = 'MyPlugin'; - console.log( 'Plugin name is ', pluginName ); -} -``` - -`second.js` as: - -```js -function() { - var pluginName = 'DifferentPlugin'; - console.log( 'Plugin name is ', pluginName ); -} -``` - -And `third.js`: - -```js -function() { - console.log( 'Plugin name is ', pluginName ); -} -``` - -With this trick, the different files won't override each other's variables. Unfortunately, they also won't work as expected, because these functions are being called by no one. We've only _defined_ the functions; we haven't _executed_ them yet. - -## Automatically Execute Anonymous Functions - -It turns out there are a few ways to execute anonymous functions in JavaScript, but the most popular is this: - -```js -( function () { - // your code goes here -} )(); -``` - -You wrap your function between parentheses, and then call it like any other named function. This pattern is known as [Immediately-Invoked Function Expression](http://benalman.com/news/2010/11/immediately-invoked-function-expression/), or IIFE for short. - -This is `first.js` written as an IIFE: - -```js -( function () { - var pluginName = 'MyPlugin'; - console.log( 'Plugin name is ', pluginName ); -} )(); -``` - -And this is `second.js`: - -```js -( function () { - var pluginName = 'DifferentPlugin'; - console.log( 'Plugin name is ', pluginName ); -} )(); -``` - -And this is `third.js`: - -```js -( function () { - console.log( 'Plugin name is ', pluginName ); -} )(); -``` - -The code in `first.js` and `second.js` is unaffected by other variables in the global scope, so it's safe and deterministic. - -On the other hand, `third.js` doesn't declare a `pluginName` variable, but needs to be provided one. IIFEs still allow you to take a variable from the global scope and pass it into your function. Provided that there was a global `window.pluginName` variable, we could rewrite `third.js` as: - -```js -( function ( name ) { - console.log( 'Plugin name is ', name ); -} )( window.pluginName ); -``` - -## Future Changes - -At the beginning we mentioned that: - -> Historically, JavaScript files loaded in a web page share the same scope. - -Notice the _historically_. - -JavaScript has evolved quite a bit since its creation. As of 2015, the language supports modules, also known as _ES6 modules_, that introduce separate scope per file: a global variable in `first.js` wouldn't be exposed to `second.js`. This feature is already [supported by modern browsers](https://caniuse.com/#feat=es6-module), but not all of them do. If your code needs to run in browsers that don't support modules, your last resort is using IIFEs. diff --git a/docs/how-to-guides/javascript/troubleshooting.md b/docs/how-to-guides/javascript/troubleshooting.md deleted file mode 100644 index 99484054d7b012..00000000000000 --- a/docs/how-to-guides/javascript/troubleshooting.md +++ /dev/null @@ -1,76 +0,0 @@ -# Troubleshooting - -If you're having trouble getting your JavaScript code to work, here are a few tips on how to find errors to help you troubleshoot. - -## Console Log - -The console log is a JavaScript developer's best friend. It is a good practice to work with it open, as it displays errors and notices in one place. See Mozilla's [JavaScript console](https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_are_browser_developer_tools#The_JavaScript_console) documentation for more. - -To open the JavaScript console, find the correct key combination for your browser and OS: - -| Browser | Windows | Linux | Mac | -| ------- | ------------ | ------------ | --------- | -| Firefox | Ctrl+Shift+K | Ctrl+Shift+K | Cmd+Opt+K | -| Chrome | Ctrl+Shift+J | Ctrl+Shift+J | Cmd+Opt+J | -| Edge | Ctrl+Shift+J | Ctrl+Shift+J | Cmd+Opt+J | -| Safari | | | Cmd+Opt+C | - -### First Step - -Your first step in debugging should be to check the JavaScript console for any errors. Here is an example, which shows a syntax error on line 6: - -![console error](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/js-tutorial-console-log-error.png) - -### Display your message in console log - -You can also write directly to the console from your JavaScript code for debugging and checking variable values. Use the `console.log` function like so: - -```js -console.log( 'My message' ); -``` - -Or if you want to include a message and variable, in this case display the contents of settings variable: - -```js -console.log( 'Settings value:', settings ); -``` - -### Using console log - -You can also write JavaScript directly in the console if you want to test a short command. The commands you run apply to the open browser window. Try this example with the [wp.data package](/packages/data/README.md) to count how many blocks are in the editor. Play with it and also try to use the console to browse available functions. - -```js -wp.data.select( 'core/block-editor' ).getBlockCount(); -``` - -![JavaScript example command](https://developer.wordpress.org/files/2020/07/js-console-cmd.gif) - -### Using the `debugger` statement - -If you would like to pause code execution at a certain line of code, you can write `debugger;` anywhere in your code. Once the browser sees the statement `debugger;`, it will pause execution of your code. This allows you to inspect all variables around the `debugger` statement, which is very useful. [See this MDN page for more information](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger). - -## Confirm JavaScript is loading - -If you are not seeing your changes, and no errors, check that your JavaScript file is being enqueued. Open the page source in your browser's web inspector (some browsers may allow you to view the page source by right clicking on the page and selecting "View Page Source"), and look for the `<script>` tag that loads your file. In the JavaScript tutorial example, you would search for `myguten.js` and confirm it is being loaded. - -If you do not see the file being loaded, double check the enqueue function is correct. You can also check your server logs to see if there is an error messages. - -Add a test message to confirm your JavaScript is loading, add a `console.log("Here");` at the top of your code, and confirm the message is shown. If not, it is likely the file is not loading properly, [review the loading JavaScript page](/docs/how-to-guides/javascript/loading-javascript.md) for details on enqueuing JavaScript properly. - -## Confirm all dependencies are loading - -The console log will show an error if a dependency your JavaScript code uses has not been declared and loaded in the browser. In the JavaScript tutorial example, if `myguten.js` script is enqueued without declaring the `wp-blocks` dependency, the console log will show: - -<img src="https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/assets/js-tutorial-error-blocks-undefined.png" width=448 title="error wp.blocks is undefined"/> - -You can correct by checking your `wp_enqueue_script` function includes all packages listed that are used: - -```js -wp_enqueue_script( - 'myguten-script', - plugins_url( 'myguten.js', __FILE__ ), - array( 'wp-blocks' ) -); -``` - -For automated dependency management, it is recommended to [use wp-scripts to build step your JavaScript](/docs/how-to-guides/javascript/js-build-setup.md#dependency-management). diff --git a/docs/how-to-guides/javascript/versions-and-building.md b/docs/how-to-guides/javascript/versions-and-building.md deleted file mode 100644 index ec9f98368ff024..00000000000000 --- a/docs/how-to-guides/javascript/versions-and-building.md +++ /dev/null @@ -1,13 +0,0 @@ -# JavaScript Versions and Build Step - -The Block Editor Handbook shows JavaScript examples in two syntaxes: JSX and Plain. - -Plain refers to JavaScript code compatible with WordPress's minimum [target for browser support](https://make.wordpress.org/core/handbook/best-practices/browser-support/) without requiring a transpilation step. This step is commonly referred to as a build process. - -"JSX" doesn't refer to a specific version of JavaScript, but refers to the latest language definition plus [JSX syntax](https://reactjs.org/docs/introducing-jsx.html), a syntax that blends HTML and JavaScript. JSX makes it easier to read and write markup code, but does require a build step to transpile into code compatible with browsers. Webpack and babel are the tools that perform this transpilation step. - -For simplicity, the JavaScript tutorial uses the Plain definition, without JSX. This code can run straight in your browser and does not require an additional build step. In many cases, it is perfectly fine to follow the same approach for simple plugins or experimenting. As your codebase grows in complexity it might be a good idea to switch to JSX. You will find the majority of code and documentation across the block editor uses JSX. - -See the [JavaScript Build Setup documentation](/docs/how-to-guides/javascript/js-build-setup.md) for setting up a development environment using JSX syntax. - -See the [ESNext syntax documentation](/docs/how-to-guides/javascript/esnext-js.md) for explanation and examples about common code differences between standard JavaScript and more modern approaches. diff --git a/docs/manifest.json b/docs/manifest.json index 6b5ee58d4d6a50..fb2ce08aa8b910 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -227,60 +227,6 @@ "markdown_source": "../docs/how-to-guides/format-api.md", "parent": "how-to-guides" }, - { - "title": "How to use JavaScript with the Block Editor", - "slug": "javascript", - "markdown_source": "../docs/how-to-guides/javascript/README.md", - "parent": "how-to-guides" - }, - { - "title": "Plugins Background", - "slug": "plugins-background", - "markdown_source": "../docs/how-to-guides/javascript/plugins-background.md", - "parent": "javascript" - }, - { - "title": "Loading JavaScript", - "slug": "loading-javascript", - "markdown_source": "../docs/how-to-guides/javascript/loading-javascript.md", - "parent": "javascript" - }, - { - "title": "Extending the Block Editor", - "slug": "extending-the-block-editor", - "markdown_source": "../docs/how-to-guides/javascript/extending-the-block-editor.md", - "parent": "javascript" - }, - { - "title": "Troubleshooting", - "slug": "troubleshooting", - "markdown_source": "../docs/how-to-guides/javascript/troubleshooting.md", - "parent": "javascript" - }, - { - "title": "JavaScript Versions and Build Step", - "slug": "versions-and-building", - "markdown_source": "../docs/how-to-guides/javascript/versions-and-building.md", - "parent": "javascript" - }, - { - "title": "Scope Your Code", - "slug": "scope-your-code", - "markdown_source": "../docs/how-to-guides/javascript/scope-your-code.md", - "parent": "javascript" - }, - { - "title": "JavaScript Build Setup", - "slug": "js-build-setup", - "markdown_source": "../docs/how-to-guides/javascript/js-build-setup.md", - "parent": "javascript" - }, - { - "title": "ESNext Syntax", - "slug": "esnext-js", - "markdown_source": "../docs/how-to-guides/javascript/esnext-js.md", - "parent": "javascript" - }, { "title": "Internationalization", "slug": "internationalization", diff --git a/docs/toc.json b/docs/toc.json index fec39245761c94..6179915e62ae3a 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -100,26 +100,6 @@ { "docs/how-to-guides/enqueueing-assets-in-the-editor.md": [] }, { "docs/how-to-guides/feature-flags.md": [] }, { "docs/how-to-guides/format-api.md": [] }, - { - "docs/how-to-guides/javascript/README.md": [ - { - "docs/how-to-guides/javascript/plugins-background.md": [] - }, - { - "docs/how-to-guides/javascript/loading-javascript.md": [] - }, - { - "docs/how-to-guides/javascript/extending-the-block-editor.md": [] - }, - { "docs/how-to-guides/javascript/troubleshooting.md": [] }, - { - "docs/how-to-guides/javascript/versions-and-building.md": [] - }, - { "docs/how-to-guides/javascript/scope-your-code.md": [] }, - { "docs/how-to-guides/javascript/js-build-setup.md": [] }, - { "docs/how-to-guides/javascript/esnext-js.md": [] } - ] - }, { "docs/how-to-guides/internationalization.md": [] }, { "docs/how-to-guides/metabox.md": [] From 0e4c6a463e4b2300044dafe3cd107f50a7459ef7 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr <jsnajdr@gmail.com> Date: Mon, 18 Dec 2023 16:29:28 +0100 Subject: [PATCH 238/325] InserterListItem: use item.isDisabled to detect disabled item (#57161) --- .../src/components/inserter-list-item/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) 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 dc755e87676233..5cc59dfbac0054 100644 --- a/packages/block-editor/src/components/inserter-list-item/index.js +++ b/packages/block-editor/src/components/inserter-list-item/index.js @@ -39,15 +39,16 @@ function InserterListItem( { color: item.icon.foreground, } : {}; - const blocks = useMemo( () => { - return [ + const blocks = useMemo( + () => [ createBlock( item.name, item.initialAttributes, createBlocksFromInnerBlocksTemplate( item.innerBlocks ) ), - ]; - }, [ item.name, item.initialAttributes, item.initialAttributes ] ); + ], + [ item.name, item.initialAttributes, item.innerBlocks ] + ); const isSynced = ( isReusableBlock( item ) && item.syncStatus !== 'unsynced' ) || @@ -55,7 +56,7 @@ function InserterListItem( { return ( <InserterDraggableBlocks - isEnabled={ isDraggable && ! item.disabled } + isEnabled={ isDraggable && ! item.isDisabled } blocks={ blocks } icon={ item.icon } > @@ -63,7 +64,6 @@ function InserterListItem( { <div className={ classnames( 'block-editor-block-types-list__list-item', - { 'is-synced': isSynced, } From 22bad555b5232d8697e9e3a4c3198f8f356535b3 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr <jsnajdr@gmail.com> Date: Mon, 18 Dec 2023 16:33:46 +0100 Subject: [PATCH 239/325] useBlockTypesState: divide useSelect call into two (#57163) --- .../inserter/hooks/use-block-types-state.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js index 2faa5036327831..566d0476fbd0f5 100644 --- a/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js +++ b/packages/block-editor/src/components/inserter/hooks/use-block-types-state.js @@ -23,20 +23,18 @@ import { store as blockEditorStore } from '../../../store'; * @return {Array} Returns the block types state. (block types, categories, collections, onSelect handler) */ const useBlockTypesState = ( rootClientId, onInsert ) => { - const { categories, collections, items } = useSelect( - ( select ) => { - const { getInserterItems } = select( blockEditorStore ); - const { getCategories, getCollections } = select( blocksStore ); - - return { - categories: getCategories(), - collections: getCollections(), - items: getInserterItems( rootClientId ), - }; - }, + const [ items ] = useSelect( + ( select ) => [ + select( blockEditorStore ).getInserterItems( rootClientId ), + ], [ rootClientId ] ); + const [ categories, collections ] = useSelect( ( select ) => { + const { getCategories, getCollections } = select( blocksStore ); + return [ getCategories(), getCollections() ]; + }, [] ); + const onSelectItem = useCallback( ( { name, initialAttributes, innerBlocks, syncStatus, content }, From 1f42ec7e64c76ac9518bee14341cd55bcc530cb9 Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Mon, 18 Dec 2023 09:46:25 -0600 Subject: [PATCH 240/325] Enforce heading sentence case throughout the BEH (#57143) * Enforce heading sentence case. * Linting * Update title and remove unnecessary TOC * Update docs/how-to-guides/README.md Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> * Update docs/reference-guides/richtext.md Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> * Update docs/explanations/user-interface/block-design.md Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> * Update docs/how-to-guides/internationalization.md Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --------- Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- docs/contributors/README.md | 2 +- docs/contributors/accessibility-testing.md | 6 +- docs/contributors/code/README.md | 2 +- .../code/backward-compatibility.md | 4 +- docs/contributors/code/coding-guidelines.md | 34 ++++----- .../getting-started-with-code-contribution.md | 4 +- docs/contributors/code/git-workflow.md | 8 +- .../getting-started-react-native.md | 10 +-- docs/contributors/code/testing-overview.md | 16 ++-- docs/contributors/design/README.md | 2 +- docs/contributors/design/the-block.md | 2 +- docs/contributors/documentation/README.md | 2 +- docs/contributors/repository-management.md | 12 +-- docs/contributors/triage.md | 6 +- docs/explanations/architecture/README.md | 2 +- docs/explanations/architecture/data-flow.md | 8 +- .../explanations/architecture/key-concepts.md | 10 +-- docs/explanations/architecture/modularity.md | 2 +- docs/explanations/architecture/performance.md | 2 +- docs/explanations/architecture/styles.md | 74 +++++++------------ .../user-interface/block-design.md | 24 +++--- .../devenv/get-started-with-create-block.md | 2 +- .../devenv/get-started-with-wp-scripts.md | 2 +- .../javascript-in-the-block-editor.md | 2 +- docs/how-to-guides/README.md | 10 +-- docs/how-to-guides/accessibility.md | 2 +- .../nested-blocks-inner-blocks.md | 14 ++-- docs/how-to-guides/feature-flags.md | 8 +- docs/how-to-guides/format-api.md | 4 +- docs/how-to-guides/internationalization.md | 12 +-- docs/how-to-guides/metabox.md | 26 +++---- docs/how-to-guides/notices/README.md | 2 +- docs/how-to-guides/platform/README.md | 4 +- docs/how-to-guides/plugin-sidebar-0.md | 8 +- docs/how-to-guides/propagating-updates.md | 2 +- docs/manifest.json | 2 +- .../block-api/block-attributes.md | 8 +- .../block-api/block-context.md | 8 +- .../block-api/block-edit-save.md | 2 +- .../block-api/block-metadata.md | 14 ++-- .../block-api/block-patterns.md | 6 +- .../block-api/block-registration.md | 4 +- .../block-api/block-selectors.md | 6 +- .../block-api/block-templates.md | 6 +- .../block-api/block-transforms.md | 2 +- docs/reference-guides/packages.md | 4 +- docs/reference-guides/richtext.md | 12 +-- .../theme-json-reference/README.md | 2 +- .../theme-json-reference/styles-versions.md | 2 +- 49 files changed, 193 insertions(+), 215 deletions(-) diff --git a/docs/contributors/README.md b/docs/contributors/README.md index 33255d778526f6..ccf56119917329 100644 --- a/docs/contributors/README.md +++ b/docs/contributors/README.md @@ -18,7 +18,7 @@ Find the section below based on what you are looking to contribute: - **Internationalization?** See the [localizing and translating section](/docs/contributors/localizing.md) -### Repository Management +### Repository management The Gutenberg project uses GitHub for managing code and tracking issues. Please see the following sections for the project methodologies using GitHub. diff --git a/docs/contributors/accessibility-testing.md b/docs/contributors/accessibility-testing.md index 8012b5214764c2..ded036e19cb5a5 100644 --- a/docs/contributors/accessibility-testing.md +++ b/docs/contributors/accessibility-testing.md @@ -2,11 +2,11 @@ This is a guide on how to test accessibility on Gutenberg. This is a living document that can be improved over time with new approaches and techniques. -## Getting Started +## Getting started Make sure you have set up your local environment following the instructions on [Getting Started](/docs/contributors/code/getting-started-with-code-contribution.md). -## Keyboard Testing +## Keyboard testing In addition to mouse, make sure the interface is fully accessible for keyboard-only users. Try to interact with your changes using only the keyboard: @@ -18,7 +18,7 @@ If the elements can be focused using arrow keys, but not <kbd>Tab</kbd> or <kbd> If the interaction is complex or confusing to you, consider that it's also going to impact keyboard-only users. -## Screen Reader Testing +## Screen reader testing According to the [WebAIM: Screen Reader User Survey #8 Results](https://webaim.org/projects/screenreadersurvey8/#usage), these are the most common screen reader and browser combinations: diff --git a/docs/contributors/code/README.md b/docs/contributors/code/README.md index caaa9221f240fe..848aa8bc26bbd1 100644 --- a/docs/contributors/code/README.md +++ b/docs/contributors/code/README.md @@ -14,7 +14,7 @@ The Gutenberg project uses GitHub for managing code and tracking issues. The mai Browse [the issues list](https://github.com/wordpress/gutenberg/issues) to find issues to work on. The [good first issue](https://github.com/wordpress/gutenberg/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22) and [good first review](https://github.com/WordPress/gutenberg/pulls?q=is%3Aopen+is%3Apr+label%3A%22Good+First+Review%22) labels are good starting points. -## Contributor Resources +## Contributor resources - [Getting Started](/docs/contributors/code/getting-started-with-code-contribution.md) documents getting your development environment setup, this includes your test site and developer tools suggestions. - [Git Workflow](/docs/contributors/code/git-workflow.md) documents the git process for deploying changes using pull requests. diff --git a/docs/contributors/code/backward-compatibility.md b/docs/contributors/code/backward-compatibility.md index 96615fe793a505..ce0f58a079c759 100644 --- a/docs/contributors/code/backward-compatibility.md +++ b/docs/contributors/code/backward-compatibility.md @@ -61,7 +61,7 @@ deprecated( 'wp.components.ClipboardButton', { } ); ``` -## Dev Notes +## Dev notes Dev notes are [posts published on the make/core site](https://make.wordpress.org/core/tag/dev-notes/) prior to WordPress releases to inform third-party developers about important changes to the developer APIs, these changes can include: @@ -70,7 +70,7 @@ Dev notes are [posts published on the make/core site](https://make.wordpress.org - Unavoidable backward compatibility breakage, with reasoning and migration flows. - Important deprecations (even without breakage), with reasoning and migration flows. -### Dev Note Workflow +### Dev note workflow - When working on a pull request and the need for a dev note is discovered, add the **Needs Dev Note** label to the PR. - If possible, add a comment to the PR explaining why the dev note is needed. diff --git a/docs/contributors/code/coding-guidelines.md b/docs/contributors/code/coding-guidelines.md index 53f0a0f8d10002..52fa92440b2653 100644 --- a/docs/contributors/code/coding-guidelines.md +++ b/docs/contributors/code/coding-guidelines.md @@ -55,7 +55,7 @@ export default function Notice( { children, onRemove, isDismissible } ) { A component's class name should **never** be used outside its own folder (with rare exceptions such as [`_z-index.scss`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/base-styles/_z-index.scss)). If you need to inherit styles of another component in your own components, you should render an instance of that other component. At worst, you should duplicate the styles within your own component's stylesheet. This is intended to improve maintainability by isolating shared components as a reusable interface, reducing the surface area of similar UI elements by adapting a limited set of common components to support a varied set of use-cases. -#### SCSS File Naming Conventions for Blocks +#### SCSS file naming conventions for blocks The build process will split SCSS from within the blocks library directory into two separate CSS files when Webpack runs. @@ -75,7 +75,7 @@ In the Gutenberg project, we use [the ES2015 import syntax](https://developer.mo These separations are identified by multi-line comments at the top of a file which imports code from another file or source. -#### External Dependencies +#### External dependencies An external dependency is third-party code that is not maintained by WordPress contributors, but instead [included in WordPress as a default script](https://developer.wordpress.org/reference/functions/wp_enqueue_script/#default-scripts-included-and-registered-by-wordpress) or referenced from an outside package manager like [npm](https://www.npmjs.com/). @@ -88,7 +88,7 @@ Example: import moment from 'moment'; ``` -#### WordPress Dependencies +#### WordPress dependencies To encourage reusability between features, our JavaScript is split into domain-specific modules which [`export`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export) one or more functions or objects. In the Gutenberg project, we've distinguished these modules under top-level directories. Each module serve an independent purpose, and often code is shared between them. For example, in order to localize its text, editor code will need to include functions from the `i18n` module. @@ -101,7 +101,7 @@ Example: import { __ } from '@wordpress/i18n'; ``` -#### Internal Dependencies +#### Internal dependencies Within a specific feature, code is organized into separate files and folders. As is the case with external and WordPress dependencies, you can bring this code into scope by using the `import` keyword. The main distinction here is that when importing internal files, you should use relative paths specific to top-level directory you're working in. @@ -114,9 +114,9 @@ Example: import VisualEditor from '../visual-editor'; ``` -### Legacy Experimental APIs, Plugin-only APIs, and Private APIs +### Legacy experimental APIs, plugin-only APIs, and private APIs -#### Legacy Experimental APIs +#### Legacy experimental APIs Historically, Gutenberg has used the `__experimental` and `__unstable` prefixes to indicate that a given API is not yet stable and may be subject to change. This is a legacy convention which should be avoided in favor of the plugin-only API pattern or a private API pattern described below. @@ -352,7 +352,7 @@ const { privateValidateBlocks } = unlock( package1PrivateApis ); privateValidateBlocks( blocks, true ); ``` -#### Private React Component properties +#### Private React component properties To add an private argument to a stable component you'll need to prepare a stable and an private version of that component. @@ -517,7 +517,7 @@ alert( 'My name is ' + name + '.' ); alert( `My name is ${ name }.` ); ``` -### Optional Chaining +### Optional chaining [Optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining) is a new language feature introduced in version 2020 of the ECMAScript specification. While the feature can be very convenient for property access on objects which are potentially null-ish (`null` or `undefined`), there are a number of common pitfalls to be aware of when using optional chaining. These may be issues that linting and/or type-checking can help protect against at some point in the future. In the meantime, you will want to be cautious of the following items: @@ -529,11 +529,11 @@ alert( `My name is ${ name }.` ); - Example: `document.body.classList.toggle( 'has-focus', nodeRef.current?.contains( document.activeElement ) );` may wrongly _add_ the class, since [the second argument is optional](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList/toggle). If `undefined` is passed, it would not unset the class as it would when `false` is passed. - Example: `<input value={ state.selected?.value.trim() } />` may inadvertently cause warnings in React by toggling between [controlled and uncontrolled inputs](https://reactjs.org/docs/uncontrolled-components.html). This is an easy trap to fall into when eagerly assuming that a result of `trim()` will always return a string value, overlooking the fact the optional chaining may have caused evaluation to abort earlier with a value of `undefined`. -### React Components +### React components It is preferred to implement all components as [function components](https://reactjs.org/docs/components-and-props.html), using [hooks](https://reactjs.org/docs/hooks-reference.html) to manage component state and lifecycle. With the exception of [error boundaries](https://reactjs.org/docs/error-boundaries.html), you should never encounter a situation where you must use a class component. Note that the [WordPress guidance on Code Refactoring](https://make.wordpress.org/core/handbook/contribute/code-refactoring/) applies here: There needn't be a concentrated effort to update class components in bulk. Instead, consider it as a good refactoring opportunity in combination with some other change. -## JavaScript Documentation using JSDoc +## JavaScript documentation using JSDoc Gutenberg follows the [WordPress JavaScript Documentation Standards](https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/javascript/), with additional guidelines relevant for its distinct use of [import semantics](/docs/contributors/code/coding-guidelines.md#imports) in organizing files, the [use of TypeScript tooling](/docs/contributors/code/testing-overview.md#javascript-testing) for types validation, and automated documentation generation using [`@wordpress/docgen`](https://github.com/WordPress/gutenberg/tree/HEAD/packages/docgen). @@ -542,7 +542,7 @@ For additional guidance, consult the following resources: - [JSDoc Official Documentation](https://jsdoc.app/index.html) - [TypeScript Supported JSDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) -### Custom Types +### Custom types Define custom types using the [JSDoc `@typedef` tag](https://jsdoc.app/tags-typedef.html). @@ -577,7 +577,7 @@ Custom types can also be used to describe a set of predefined options. While the Note the use of quotes when defining a set of string literals. As in the [JavaScript Coding Standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/javascript/), single quotes should be used when assigning a string literal either as the type or as a [default function parameter](#nullable-undefined-and-void-types), or when [specifying the path](#importing-and-exporting-types) of an imported type. -### Importing and Exporting Types +### Importing and exporting types Use the [TypeScript `import` function](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html#import-types) to import type declarations from other files or third-party dependencies. @@ -599,7 +599,7 @@ When considering which types should be made available from a WordPress package, In this snippet, the `@typedef` will support the usage of the previous example's `import('@wordpress/data')`. -#### External Dependencies +#### External dependencies Many third-party dependencies will distribute their own TypeScript typings. For these, the `import` semantics should "just work". @@ -609,7 +609,7 @@ If you use a [TypeScript integration](https://github.com/Microsoft/TypeScript/wi For packages which do not distribute their own TypeScript types, you are welcomed to install and use the [DefinitelyTyped](http://definitelytyped.org/) community-maintained types definitions, if one exists. -### Generic Types +### Generic types When documenting a generic type such as `Object`, `Function`, `Promise`, etc., always include details about the expected record types. @@ -659,7 +659,7 @@ Similar to the "Custom Types" advice concerning type unions and with literal val const BREAKPOINTS = { huge: 1440 /* , ... */ }; ``` -### Nullable, Undefined, and Void Types +### Nullable, undefined, and void types You can express a nullable type using a leading `?`. Use the nullable form of a type only if you're describing either the type or an explicit `null` value. Do not use the nullable form as an indicator of an optional parameter. @@ -731,7 +731,7 @@ When documenting a [function type](https://github.com/WordPress/gutenberg/blob/a */ ``` -### Documenting Examples +### Documenting examples Because the documentation generated using the `@wordpress/docgen` tool will include `@example` tags if they are defined, it is considered a best practice to include usage examples for functions and components. This is especially important for documented members of a package's public API. @@ -756,7 +756,7 @@ When documenting an example, use the markdown <code>\`\`\`</code> code block to */ ```` -### Documenting React Components +### Documenting React components When possible, all components should be implemented as [function components](https://reactjs.org/docs/components-and-props.html#function-and-class-components), using [hooks](https://reactjs.org/docs/hooks-intro.html) for managing component lifecycle and state. diff --git a/docs/contributors/code/getting-started-with-code-contribution.md b/docs/contributors/code/getting-started-with-code-contribution.md index 3de83d50268d8e..c3282c0f8003da 100644 --- a/docs/contributors/code/getting-started-with-code-contribution.md +++ b/docs/contributors/code/getting-started-with-code-contribution.md @@ -183,7 +183,7 @@ If so, you need to instruct Apache to allow following such links: Tools like MAMP tend to configure MySQL to use ports other than the default 3306, often preferring 8889. This may throw off WP-CLI, which will fail after trying to connect to the database. To remedy this, edit `wp-config.php` and change the `DB_HOST` constant from `define( 'DB_HOST', 'localhost' )` to `define( 'DB_HOST', '127.0.0.1:8889' )`. -### On A Remote Server +### On a remote server You can use a remote server in development by building locally and then uploading the built files as a plugin to the remote server. @@ -203,7 +203,7 @@ You can launch Storybook by running `npm run storybook:dev` locally. It will ope You can also test Storybook for the current `trunk` branch on GitHub Pages: [https://wordpress.github.io/gutenberg/](https://wordpress.github.io/gutenberg/) -## Developer Tools +## Developer tools We recommend configuring your editor to automatically check for syntax and lint errors. This will help you save time as you develop by automatically fixing minor formatting issues. Here are some directions for setting up Visual Studio Code, a popular editor used by many of the core developers, these tools are also available for other editors. diff --git a/docs/contributors/code/git-workflow.md b/docs/contributors/code/git-workflow.md index eeeb68282c1cdf..f2ad1345355deb 100644 --- a/docs/contributors/code/git-workflow.md +++ b/docs/contributors/code/git-workflow.md @@ -73,7 +73,7 @@ Do not make a new pull request for updates; by pushing your change to your repos That’s it! Once approved and merged, your change will be incorporated into the main repository. 🎉 -## Branch Naming +## Branch naming You should name your branches using a prefixes and short description, like this: `[type]/[change]`. @@ -87,7 +87,7 @@ Suggested prefixes: For example, `add/gallery-block` means you're working on adding a new gallery block. -## Keeping Your Branch Up To Date +## Keeping your branch up to date When many different people are working on a project simultaneously, pull requests can go stale quickly. A "stale" pull request is one that is no longer up to date with the main line of development, and it needs to be updated before it can be merged into the project. @@ -105,7 +105,7 @@ git rebase trunk git push --force-with-lease origin your-branch-name ``` -## Keeping Your Fork Up To Date +## Keeping your fork up to date Working on pull request starts with forking the Gutenberg repository, your separate working copy. Which can easily go out of sync as new pull requests are merged into the main repository. Here your working repository is a `fork` and the main Gutenberg repository is `upstream`. When working on new pull request you should always update your fork before you do `git checkout -b my-new-branch` to work on a feature or fix. @@ -138,7 +138,7 @@ The above commands will update your `trunk` branch from _upstream_. To update an ## Miscellaneous -### Git Archeology +### Git archeology When looking for a commit that introduced a specific change, it might be helpful to ignore revisions that only contain styling or formatting changes. diff --git a/docs/contributors/code/react-native/getting-started-react-native.md b/docs/contributors/code/react-native/getting-started-react-native.md index 7b4dcca98027d0..48dc0e81f260eb 100644 --- a/docs/contributors/code/react-native/getting-started-react-native.md +++ b/docs/contributors/code/react-native/getting-started-react-native.md @@ -55,7 +55,7 @@ npm run native ios which will attempt to open your app in the iOS Simulator if you're on a Mac and have it installed. -### Running on Other iOS Device Simulators +### Running on other iOS device simulators To compile and run the app using a different device simulator, use the following, noting the double sets of `--` to pass the simulator option down to the `react-native` CLI. @@ -71,7 +71,7 @@ npm run native ios -- -- --simulator="iPhone Xs Max" To see a list of all of your available iOS devices, use `xcrun simctl list devices`. -### Customizing the Demo Editor +### Customizing the demo Editor By default, the Demo editor renders most of the supported core blocks. This is helpful to showcase the editor's capabilities, but can be distracting when focusing on a specific block or feature. One can customize the editor's initial state by leveraging the `native.block_editor_props` hook in a `packages/react-native-editor/src/setup-local.js` file. @@ -119,7 +119,7 @@ When you first open the project in Visual Studio, you will be prompted to instal One of the extensions we are using is the [React Native Tools](https://marketplace.visualstudio.com/items?itemName=vsmobile.vscode-react-native). This allows you to run the packager from VSCode or launch the application on iOS or Android. It also adds some debug configurations so you can set breakpoints and debug the application directly from VSCode. Take a look at the [extension documentation](https://marketplace.visualstudio.com/items?itemName=vsmobile.vscode-react-native) for more details. -## Unit Tests +## Unit tests Use the following command to run the test suite: @@ -137,11 +137,11 @@ npm run test:native:debug Then, open `chrome://inspect` in Chrome to attach the debugger (look into the "Remote Target" section). While testing/developing, feel free to sprinkle `debugger` statements anywhere in the code that you'd like the debugger to break. -## Writing and Running Unit Tests +## Writing and running unit tests This project is set up to use [jest](https://jestjs.io/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test.js` extension to have the files loaded by jest. See an example test [here](https://github.com/WordPress/gutenberg/blob/HEAD/packages/react-native-editor/src/test/api-fetch-setup.test.js). The [jest documentation](https://jestjs.io/docs/getting-started) is also a wonderful resource, as is the [React Native testing tutorial](https://jestjs.io/docs/tutorial-react-native). -## End-to-End Tests +## End-to-end tests In addition to unit tests, the Mobile Gutenberg (MG) project relies upon end-to-end (E2E) tests to automate testing critical flows in an environment similar to that of an end user. We generally prefer unit tests due to their speed and ease of maintenance. However, assertions that require OS-level features (e.g. complex gestures, text selection) or visual regression testing (e.g. dark mode, contrast levels) we use E2E tests. diff --git a/docs/contributors/code/testing-overview.md b/docs/contributors/code/testing-overview.md index 8a2f6079b2419c..3be1b6b935bff2 100644 --- a/docs/contributors/code/testing-overview.md +++ b/docs/contributors/code/testing-overview.md @@ -17,7 +17,7 @@ When writing tests consider the following: - Does the test test what we think it is testing? Or are we introducing false positives/negatives? - Is it readable? Will other contributors be able to understand how our code behaves by looking at its corresponding test? -## JavaScript Testing +## JavaScript testing Tests for JavaScript use [Jest](https://jestjs.io/) as the test runner and its API for [globals](https://jestjs.io/docs/en/api.html) (`describe`, `test`, `beforeEach` and so on) [assertions](https://jestjs.io/docs/en/expect.html), [mocks](https://jestjs.io/docs/en/mock-functions.html), [spies](https://jestjs.io/docs/en/jest-object.html#jestspyonobject-methodname) and [mock functions](https://jestjs.io/docs/en/mock-function-api.html). If needed, you can also use [React Testing Library](https://testing-library.com/docs/react-testing-library/intro) for React component testing. @@ -90,7 +90,7 @@ describe( 'CheckboxWithLabel', () => { } ); ``` -### Setup and Teardown methods +### Setup and teardown methods The Jest API includes some nifty [setup and teardown methods](https://jestjs.io/docs/en/setup-teardown.html) that allow you to perform tasks _before_ and _after_ each or all of your tests, or tests within a specific `describe` block. @@ -507,7 +507,7 @@ Contributors to Gutenberg will note that PRs include continuous integration E2E There is an ongoing effort to add integration tests to the native mobile project using the [`react-native-testing-library`](https://testing-library.com/docs/react-native-testing-library/intro/) library. A guide to writing integration tests can be found [here](/docs/contributors/code/react-native/integration-test-guide.md). -## End-to-end Testing +## End-to-end testing Most existing End-to-end tests currently use [Puppeteer](https://github.com/puppeteer/puppeteer) as a headless Chromium driver to run the tests in `packages/e2e-tests`, and are otherwise still run by a [Jest](https://jestjs.io/) test runner. @@ -559,7 +559,7 @@ Then to run the tests, specify the base URL, username, and passwords for your si WP_BASE_URL=http://wp.test npm run test:e2e -- --wordpress-username=admin --wordpress-password=password ``` -### Scenario Testing +### Scenario testing If you find that end-to-end tests pass when run locally, but fail in GitHub Actions, you may be able to isolate a CPU- or network-bound race condition by simulating a slow CPU or network: @@ -587,15 +587,15 @@ OFFLINE=true npm run test:e2e See [Chrome docs: emulateNetworkConditions](https://chromedevtools.github.io/devtools-protocol/tot/Network#method-emulateNetworkConditions) -### Core Block Testing +### Core block testing Every core block is required to have at least one set of fixture files for its main save function and one for each deprecation. These fixtures test the parsing and serialization of the block. See [the integration tests fixtures readme](https://github.com/wordpress/gutenberg/blob/HEAD/test/integration/fixtures/blocks/README.md) for more information and instructions. -### Flaky Tests +### Flaky tests A test is considered to be **flaky** when it can pass and fail across multiple retry attempts without any code changes. We auto retry failed tests at most **twice** on CI to detect and report them to GitHub issues automatically under the [`[Type] Flaky Test`](https://github.com/WordPress/gutenberg/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22%5BType%5D+Flaky+Test%22) label via [`report-flaky-tests`](https://github.com/WordPress/gutenberg/tree/trunk/packages/report-flaky-tests) GitHub action. Note that a test that failed three times in a row is not counted as a flaky test and will not be reported to an issue. -## PHP Testing +## PHP testing Tests for PHP use [PHPUnit](https://phpunit.de/) as the testing framework. If you're using the built-in [local environment](/docs/contributors/code/getting-started-with-code-contribution.md#local-environment), you can run the PHP tests locally using this command: @@ -620,7 +620,7 @@ To run unit tests only, without the linter, use `npm run test:unit:php` instead. [snapshot testing]: https://jestjs.io/docs/en/snapshot-testing.html [update snapshots]: https://jestjs.io/docs/en/snapshot-testing.html#updating-snapshots -## Performance Testing +## Performance testing To ensure that the editor stays performant as we add features, we monitor the impact pull requests and releases can have on some key metrics: diff --git a/docs/contributors/design/README.md b/docs/contributors/design/README.md index de390937244ed2..abc077b8fdc072 100644 --- a/docs/contributors/design/README.md +++ b/docs/contributors/design/README.md @@ -8,7 +8,7 @@ The [Make WordPress Design blog](https://make.wordpress.org/design/) is the prim Real-time discussions for design take place in the `#design` channel in [Make WordPress Slack](https://make.wordpress.org/chat) (registration required). Weekly meetings for the Design team are on Wednesdays at 19:00UTC. -## How Can Designers Contribute? +## How can designers contribute? The Gutenberg project uses GitHub for managing code and tracking issues. The main repository is at: [https://github.com/WordPress/gutenberg](https://github.com/WordPress/gutenberg). diff --git a/docs/contributors/design/the-block.md b/docs/contributors/design/the-block.md index f35e17d0195928..463d3eb9b9e1a0 100644 --- a/docs/contributors/design/the-block.md +++ b/docs/contributors/design/the-block.md @@ -11,7 +11,7 @@ So, for example, a user can add an image, write its caption, change its width an - Users only need to learn one interface — the block — to add and edit everything on their site. Users shouldn’t have to write shortcodes, custom HTML, or understand hidden mechanisms to embed content. - Gutenberg makes core features more discoverable, reducing hard-to-find “Mystery meat.” WordPress supports a large number of blocks and 30+ embeds. Let’s increase their visibility. -## Building Blocks +## Building blocks What does this mean for designers and developers? The block structure plus the principle of direct manipulation mean thinking differently about how to design and develop WordPress components. Let’s take another look at the architecture of a block: diff --git a/docs/contributors/documentation/README.md b/docs/contributors/documentation/README.md index 397e7ad1e140c9..7089bf57141687 100644 --- a/docs/contributors/documentation/README.md +++ b/docs/contributors/documentation/README.md @@ -64,7 +64,7 @@ To add a new document requires a working JavaScript development environment to b If you forget to run, `npm run docs:build` your PR will fail the static analysis check, since the `manifest.json` file is an uncommitted local change that must be committed. -### Documenting Packages +### Documenting packages Package documentation is generated automatically by the documentation tool by pulling the contents of the README.md file located in the root of the package. Sometimes however, it is preferable to split the contents of the README out into smaller, easier to read portions. diff --git a/docs/contributors/repository-management.md b/docs/contributors/repository-management.md index 9bce2d06fd1566..e57f762a605394 100644 --- a/docs/contributors/repository-management.md +++ b/docs/contributors/repository-management.md @@ -55,7 +55,7 @@ Here are some milestones you might see: - [X.Y (Gutenberg)](https://github.com/WordPress/gutenberg/milestone/85): PRs targeted for the Gutenberg Plugin X.Y release. - [Future](https://github.com/WordPress/gutenberg/milestone/35): this is something that is confirmed by everyone as a good thing but doesn’t fall into other criteria. -### Triaging Issues +### Triaging issues To keep the issue list healthy, it needs to be triaged regularly. _Triage_ is the practice of reviewing existing issues to make sure they’re relevant, actionable, and have all the information they need. @@ -63,7 +63,7 @@ Anyone can help triage, although you’ll need contributor permission on the Gut See the [Triage Contributors guide](/docs/contributors/triage.md) for details. -## Pull Requests +## Pull requests Gutenberg follows a feature branch pull request workflow for all code and documentation changes. At a high-level, the process looks like this: @@ -86,7 +86,7 @@ Along with this process, there are a few important points to mention: - To make it far easier to merge your code, each pull request should only contain one conceptual change. Keeping contributions atomic keeps the pull request discussion focused on one topic and makes it possible to approve changes on a case-by-case basis. - Separate pull requests can address different items or todos from their linked issue, there’s no need for a single pull request to cover a single issue if the issue is non-trivial. -### Code Review +### Code review Every pull request goes through a manual code review, in addition to automated tests. The objectives for the code review are best thought of as: @@ -106,7 +106,7 @@ If you are not yet comfortable leaving a full review, try commenting on a PR. Qu If you struggle with getting a review, see: [How To Get Your Pull Request Reviewed?](/docs/contributors/code/how-to-get-your-pull-request-reviewed.md) -### Design Review +### Design review If your pull request impacts the design/UI, you need to label appropriately to alert design. To request a design review, add the [Needs Design Feedback](https://github.com/WordPress/gutenberg/labels/Needs%20Design%20Feedback) label to your PR. If there are any PRs that require an update to the design/UI, please use the [Figma Library Update](https://github.com/WordPress/gutenberg/labels/Figma%20Library%20Update) label. @@ -116,7 +116,7 @@ As a guide, changes that should be reviewed: - Anything that changes something visually. - If you just want design feedback on an idea or exploration. -### Merging Pull Requests +### Merging pull requests A pull request can generally be merged once it is: @@ -134,7 +134,7 @@ All members of the WordPress organization on GitHub have the ability to review a Most pull requests will be automatically assigned a release milestone, but please make sure your merged pull request was assigned one. Doing so creates the historical legacy of what code landed when, and makes it possible for all project contributors (even non-technical ones) to access this information. -### Closing Pull Requests +### Closing pull requests Sometimes, a pull request may not be mergeable, no matter how much additional effort is applied to it (e.g. out of scope). In these cases, it’s best to communicate with the contributor graciously while describing why the pull request was closed, this encourages productive future involvement. diff --git a/docs/contributors/triage.md b/docs/contributors/triage.md index 477c7d7dc81703..33275b8d3df014 100644 --- a/docs/contributors/triage.md +++ b/docs/contributors/triage.md @@ -48,7 +48,7 @@ When triaging, either one of the lists above or issues in general, work through - Check if the issue is missing some detail and see if you can fill in those details. For instance, if a bug report is missing visual detail, it’s helpful to reproduce the issue locally and upload a screenshot or GIF. - Consider adding the Good First Issue label if you believe this is a relatively easy issue for a first-time contributor to try to solve. -**Commonly Used Labels** +**Commonly used labels** Generally speaking, the following labels are very useful for triaging issues and will likely be the ones you use the most consistently. You can view all possible labels [here](https://github.com/WordPress/gutenberg/labels). @@ -61,7 +61,7 @@ Generally speaking, the following labels are very useful for triaging issues and | `Needs More Info` | When it’s not clear what the issue is or it would help to provide additional details. | | `Needs Testing` | When a new issue needs to be confirmed or old bugs seem like they are no longer relevant. | -**Determining Priority Labels** +**Determining priority labels** If you have enough knowledge about the report at hand and feel confident in doing so, you can consider adding priority. Note that it’s on purpose that no priority label infers a normal level. @@ -81,7 +81,7 @@ Issues are closed for the following reasons: - An issue that needs more information that the author of the issue hasn't responded to for 2+ weeks. - An item that is determined as unable to be fixed or is working as intended. -## Specific Triages +## Specific triages ### Release specific triage diff --git a/docs/explanations/architecture/README.md b/docs/explanations/architecture/README.md index 2cecdfd70e2d6f..ec49318bb38cfa 100644 --- a/docs/explanations/architecture/README.md +++ b/docs/explanations/architecture/README.md @@ -11,7 +11,7 @@ Let’s look at the big picture and the architectural and UX principles of the b - [Styles in the editor](/docs/explanations/architecture/styles.md). - [Performance](/docs/explanations/architecture/performance.md). -## Gutenberg Repository +## Gutenberg repository - [Modularity and WordPress Packages](/docs/explanations/architecture/modularity.md). - [Understand the repository folder structure](/docs/contributors/folder-structure.md). diff --git a/docs/explanations/architecture/data-flow.md b/docs/explanations/architecture/data-flow.md index e000cd33b46317..c594ceda704431 100644 --- a/docs/explanations/architecture/data-flow.md +++ b/docs/explanations/architecture/data-flow.md @@ -74,7 +74,7 @@ const columnsBlock = { }; ``` -## Serialization and Parsing +## Serialization and parsing ![Diagram](https://docs.google.com/drawings/d/1iuownt5etcih7rMMvPvh0Mny8zUA1Z28saxjxaWmfJ0/pub?w=1234&h=453) @@ -88,7 +88,7 @@ This is one end of the process. The other is how to recreate the collection of b They just happen, incidentally, to be stored inside of `post_content` in a way in which they require no transformation in order to be viewable by any legacy system. It's true that loading the stored HTML into a browser without the corresponding machinery might degrade the experience, and if it included dynamic blocks of content, the dynamic elements may not load, server-generated content may not appear, and interactive content may remain static. However, it at least protects against not being able to view block editor posts on themes and installations that are blocks-unaware, and it provides the most accessible way to the content. In other words, the post remains mostly intact even if the saved HTML is rendered as is. -### Delimiters and Parsing Expression Grammar +### Delimiters and parsing expression grammar We chose instead to try to find a way to keep the formality, explicitness, and unambiguity in the existing HTML syntax. Within the HTML there were a number of options. @@ -102,7 +102,7 @@ This has dramatic implications for how simple and performant we can make our par _N.B.:_ The defining aspects of blocks are their semantics and the isolation mechanism they provide: in other words, their identity. On the other hand, where their data is stored is a more liberal aspect. Blocks support more than just static local data (via JSON literals inside the HTML comment or within the block's HTML), and more mechanisms (_e.g._, global blocks or otherwise resorting to storage in complementary `WP_Post` objects) are expected. See [attributes](/docs/reference-guides/block-api/block-attributes.md) for details. -### The Anatomy of a Serialized Block +### The anatomy of a serialized block When blocks are saved to the content after the editing session, its attributes—depending on the nature of the block—are serialized to these explicit comment delimiters. @@ -118,7 +118,7 @@ A purely dynamic block that is to be server-rendered before display could look l <!-- wp:latest-posts {"postsToShow":4,"displayPostDate":true} /--> ``` -## The Data Lifecycle +## The data lifecycle In summary, the block editor workflow parses the saved document to an in-memory tree of blocks, using token delimiters to help. During editing, all manipulations happen within the block tree. The process ends by serializing the blocks back to the `post_content`. diff --git a/docs/explanations/architecture/key-concepts.md b/docs/explanations/architecture/key-concepts.md index 3af2d6d87a077d..1ba009f7823140 100644 --- a/docs/explanations/architecture/key-concepts.md +++ b/docs/explanations/architecture/key-concepts.md @@ -12,7 +12,7 @@ The settings and content of a block can be customized in three main places: the Blocks are meant to be combined in different ways. Blocks are hierarchical in that a block can be nested within another block. Nested blocks and its container are also called _children_ and _parent_ respectively. For example, a _Columns_ block can be the parent block to multiple child blocks in each of its columns. The API that governs child block usage is named `InnerBlocks`. -### Data & Attributes +### Data and attributes Blocks understand content as attributes and are serializable to HTML. To this point, there is a new Block Grammar. Distilled, the block grammar is an HTML comment, either a self-closing tag or with a beginning tag and ending tag. In the main tag, depending on the block type and user customizations, there can be a JSON object. This raw form of the block is referred to as serialized. @@ -28,20 +28,20 @@ Each block contains Attributes or configuration settings, which can be sourced f More on [Data format and data flow](/docs/explanations/architecture/data-flow.md). -### Block Transforms +### Block transforms Blocks have the ability to be transformed into other block types. This allows basic operations like converting a paragraph into a heading, but also more intricate ones like multiple images becoming a gallery. Block transforms work for single blocks and for multi-block selections. Internal block variations are also possible transformation targets. -### Block Variations +### Block variations Given a block type, a block variation is a predefined set of its initial attributes. This API allows creating a single block from which multiple configurations are possible. Variations provide different possible interfaces, including showing up as entirely new blocks in the library, or as presets when inserting a new block. Read [the API documentation](/docs/reference-guides/block-api/block-registration.md#variations-optional) for more details. -**More on Blocks** +**More on blocks** - **[Block API](/docs/reference-guides/block-api/README.md)** - **[Tutorial: Building A Custom Block](/docs/getting-started/create-block/README.md)** -## Reusable Blocks +## Reusable blocks A reusable blocks is **an instance** of a block (or multiple blocks) that can be inserted and edited in multiples places, remaining in sync everywhere. If a reusable block is edited in one place, those changes are reflected across all posts and pages that block is used. Examples of reusable blocks include a block consisting of a heading whose content and a custom color that would be appear on multiple pages of the site and sidebar widgets that would appear on every page. diff --git a/docs/explanations/architecture/modularity.md b/docs/explanations/architecture/modularity.md index dc815d92174dde..f94f8ec7b9472e 100644 --- a/docs/explanations/architecture/modularity.md +++ b/docs/explanations/architecture/modularity.md @@ -76,7 +76,7 @@ If you're using one of these stores to access and manipulate WordPress data in y These are packages used in development mode to help developers with daily tasks to develop, build and ship JavaScript applications, WordPress plugins and themes. They include tools for linting your codebase, building it, testing it... -## Editor Packages +## Editor packages ![Post Editor Modules Architecture](https://raw.githubusercontent.com/WordPress/gutenberg/HEAD/docs/explanations/architecture/assets/modules.png) diff --git a/docs/explanations/architecture/performance.md b/docs/explanations/architecture/performance.md index b57a2bba1db615..e631ceb01fa705 100644 --- a/docs/explanations/architecture/performance.md +++ b/docs/explanations/architecture/performance.md @@ -10,7 +10,7 @@ To ensure the block editor stays performant across releases and development, we - **Typing Time:** The time it takes for the browser to respond while typing on the editor. - **Block Selection Time:** The time it takes for the browser to respond after a user selects block. (Inserting a block is also equivalent to selecting a block. Monitoring the selection is sufficient to cover both metrics). -## Key Performance Decisions and Solutions +## Key performance decisions and solutions **Data Module Async Mode** diff --git a/docs/explanations/architecture/styles.md b/docs/explanations/architecture/styles.md index a8a5af72fec76b..d62171a0622055 100644 --- a/docs/explanations/architecture/styles.md +++ b/docs/explanations/architecture/styles.md @@ -1,30 +1,8 @@ -## Styles in the editor +# Styles in the Editor This document introduces the main concepts related to styles that affect the user content in the block editor. It points to the relevant reference guides and tutorials for readers to dig deeper into each one of the ideas presented. It's aimed to block authors and people working in the block editor project. -1. [HTML and CSS](#html-and-css) -2. [Block styles](#block-styles) - -- [From UI controls to HTML markup](#from-ui-controls-to-html-markup) -- [Block Supports API](#block-supports-api) -- [Current limitations of the Block Supports API](#current-limitations-of-the-block-supports-api) - -3. [Global styles](#global-styles) - -- [Gather data](#gather-data) -- [Consolidate data](#consolidate-data) -- [From data to styles](#from-data-to-styles) -- [Current limitations of the Global Styles API](#current-limitations-of-the-global-styles-api) - -4. [Layout styles](#layout-styles) - -- [Base layout styles](#base-layout-styles) -- [Individual layout styles](#individual-layout-styles) -- [Available layout types](#available-layout-types) -- [Targeting layout or container blocks from themes](#targeting-layout-or-container-blocks-from-themes) -- [Opting out of generated layout styles](#opting-out-of-generated-layout-styles) - -### HTML and CSS +## HTML and CSS By creating a post in the block editor the user is creating a number of artifacts: a HTML document plus a number of CSS stylesheets, either embedded in the document or external. @@ -42,7 +20,7 @@ The stylesheets loaded in the front end include: - **User**. Some of the user actions in the editor will generate style content. This is the case for features such as duotone, layout, or link color. - **Other**. WordPress and plugins can also enqueue stylesheets. -### Block styles +## Block styles Since the introduction of the block editor in WordPress 5.0, there were tools for the users to "add styles" to specific blocks. By using these tools, the user would attach new classes or inline styles to the blocks, modifying their visual aspect. @@ -67,7 +45,7 @@ This is what we refer to as "user-provided block styles", also know as "local st The ability to modify a block state coupled with the fact that a block can live within any other block (think of a paragraph within a group), creates a vast amount of potential states and style possibilities. -#### From UI controls to HTML markup +### From UI controls to HTML markup If you follow the [block tutorial](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/) you can learn up about the different parts of the [block API](https://developer.wordpress.org/block-editor/reference-guides/block-api/) presented here in more detail and also build your own block. This is an introduction to the general concepts of how a block can let users edit its state. @@ -80,7 +58,7 @@ To build an experience like the one described above a block author needs a few p In essence, these are the essential mechanics a block author needs to care about for their block to be able to be styled by the user. While this can be done completely manually, there's an API that automates this process for common style needs: block supports. -#### Block Supports API +### Block Supports API [Block Supports](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-supports/) is an API that allows a block to declare what features it supports. By adding some info to their [block.json file](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/), the block tells the system what kind of actions a user can do to it. @@ -108,7 +86,7 @@ Besides the benefit of having to do less work to achieve the same results, there - the block will use the UI controls other blocks use for the same styles, creating a more coherent user experience - the UI controls in use by the block will be automatically updated as they are improved, without the block author having to do anything -#### Current limitations of the Block Supports API +### Current limitations of the Block Supports API While the Block Supports API provides value, it also comes with some limitations a block author needs to be aware of. To better visualize what they are, let's run with the following example of a table block: @@ -182,7 +160,7 @@ To enable for a _single_ property only, you may use an array to declare which pr Support for this feature was [added in this PR](https://github.com/WordPress/gutenberg/pull/36293). -### Global styles +## Global styles Global Styles refers to a mechanism that generates site-wide styles. Unlike the block styles described in the previous section, these are not serialized into the post content and are not attached to the block HTML. Instead, the output of this system is a new stylesheet with id `global-styles-inline-css`. @@ -198,19 +176,19 @@ The process of generating the stylesheet has, in essence, three steps: 2. Consolidate data: the structured information from different origins -WordPress defaults, theme, and user- is normalized and merged into a single structure. 3. Convert data into a stylesheet: convert the internal representation into CSS style rules and enqueue them as a stylesheet. -#### Gather data +### Gather data The data can come from three different origins: WordPress defaults, the active theme, or the user. All three of them use the same [`theme.json` format](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/). Data from WordPress and the active theme is retrieved from the corresponding `theme.json` file. Data from the user is pulled from the database, where it's stored after the user saves the changes they did via the global styles sidebar in the site editor. -#### Consolidate data +### Consolidate data The goal of this phase is to build a consolidated structure. There are two important processes going on in this phase. First, the system needs to normalize all the incoming data, as different origins may be using different versions of the `theme.json` format. For example, a theme may be using [v1](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-v1/) while the WordPress base is using [the latest version](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/theme-json-living/). Second, the system needs to decide how to merge the input into a single structure. This will be the focus of the following sections. -##### Styles +#### Styles Different parts of the incoming `theme.json` structure are treated differently. The data present in the `styles` section is blended together following this logic: user data overrides theme data, and theme data overrides WordPress data. @@ -266,7 +244,7 @@ The result after the consolidation would be: } ``` -##### Settings +#### Settings The `settings` section works differently than styles. Most of the settings are only used to configure the editor and have no effect on the global styles. Only a few of them are part of the resulting stylesheet: the presets. @@ -332,11 +310,11 @@ The result after the consolidation would be: } ``` -#### From data to styles +### From data to styles The last phase of generating the stylesheet is converting the consolidated data into CSS style rules. -##### Styles to CSS rules +#### Styles to CSS rules The `styles` section can be thought of as a structured representation of CSS rules, each chunk representing a CSS rule: @@ -405,7 +383,7 @@ p { } ``` -##### Settings to CSS rules +#### Settings to CSS rules From the `settings` section, all the values of any given presets will be converted to a CSS Custom Property that follows this naming structure: `--wp--preset--<category>-<slug>`. The selectors follow the same rules described in the styles section above. @@ -483,29 +461,29 @@ In addition to the CSS Custom Properties, all presets but duotone generate CSS c .wp-block-site-title .has-foreground-border-color { border-color: var(--wp--preset--color--foreground) !important; } ``` -#### Current limitations of the Global Styles API +### Current limitations of the Global Styles API -##### 1. **Setting a different CSS selector for blocks requires server-registration** +#### 1. **Setting a different CSS selector for blocks requires server-registration** By default, the selector assigned to a block is `.wp-block-<block-name>`. However, blocks can change this should they need. They can provide a CSS selector via the `__experimentalSelector` property in its `block.json`. If blocks do this, they need to be registered in the server using the `block.json`, otherwise, the global styles code doesn't have access to that information and will use the default CSS selector for the block. -##### 2. **Can't target different HTML nodes for different styles** +#### 2. **Can't target different HTML nodes for different styles** Every chunk of styles can only use a single selector. This is particularly relevant if the block is using `__experimentalSkipSerialization` to serialize the different style properties to different nodes other than the wrapper. See "Current limitations of blocks supports" for more. -##### 3. **Only a single property per block** +#### 3. **Only a single property per block** Similarly to block supports, there can be only one instance of any style in use by the block. For example, the block can only have a single font size. See related "Current limitations of block supports". -##### 4. **Only blocks using block supports are shown in the Global Styles UI** +#### 4. **Only blocks using block supports are shown in the Global Styles UI** The global styles UI in the site editor has a screen for per-block styles. The list of blocks is generated dynamically using the block supports from the `block.json` of blocks. If a block wants to be listed there, it needs to use the block supports mechanism. -### Layout styles +## Layout styles In addition to styles at the individual block level and in global styles, there is the concept of layout styles that are output for both blocks-based and classic themes. @@ -513,7 +491,7 @@ The layout block support outputs common layout styles that are shared between bl There are two primary places where Layout styles are output: -#### Base layout styles +### Base layout styles Base layout styles are those styles that are common to all blocks that opt in to a particular layout type. Examples of common base layout styling include setting `display: flex` for blocks that use the Flex layout type (such as Buttons and Social Icons), and providing default max-width for constrained layouts. @@ -521,14 +499,14 @@ Base layout styles are output from within [the main PHP class](https://github.co Common layout definitions are stored in [the core layout block support file](https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-supports/layout.php). -#### Individual layout styles +### Individual layout styles When a block that opts in to layout support is rendered, two things are processed and added to the output via [`layout.php`](https://github.com/WordPress/wordpress-develop/blob/trunk/src/wp-includes/block-supports/layout.php): - Semantic class names are added to the block markup to indicate which layout settings are in use. For example, `is-layout-flow` is for blocks (such as Group) that use the default/flow layout, and `is-content-justification-right` is added when a user sets a block to use right justification. - Individual styles are generated for non-default layout values that are set on the individual block being rendered. These styles are attached to the block via a container class name using the form `wp-container-$id` where the `$id` is a [unique number](https://developer.wordpress.org/reference/functions/wp_unique_id/). -#### Available layout types +### Available layout types There are currently four layout types in use: @@ -539,7 +517,7 @@ There are currently four layout types in use: For controlling spacing between blocks, and enabling block spacing controls see: [What is blockGap and how can I use it?](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-json/#what-is-blockgap-and-how-can-i-use-it). -#### Targeting layout or container blocks from themes +### Targeting layout or container blocks from themes The layout block support is designed to enable control over layout features from within the block and site editors. Where possible, try to use the features of the blocks to determine particular layout requirements rather than relying upon additional stylesheets. @@ -547,7 +525,7 @@ For themes that wish to target container blocks in order to add or adjust partic For targeting a block that uses a particular layout type, avoid targeting `wp-container-` as container classes may not always be present in the rendered markup. -##### Semantic class names +#### Semantic class names Work is currently underway to expand stable semantic classnames in Layout block support output. The task is being discussed in [this issue](https://github.com/WordPress/gutenberg/issues/38719). @@ -566,6 +544,6 @@ The current semantic class names that can be output by the Layout block support - `is-content-justification-space-between`: When a block explicitly sets `justifyContent` to `space-between`. - `is-nowrap`: When a block explicitly sets `flexWrap` to `nowrap`. -#### Opting out of generated layout styles +### Opting out of generated layout styles Layout styles output is switched on by default because the styles are required by core structural blocks. However, themes can opt out of generated block layout styles while retaining semantic class name output by using the `disable-layout-styles` block support. Such themes will be responsible for providing all their own layout styles. See [the entry under Theme Support](https://developer.wordpress.org/block-editor/how-to-guides/themes/theme-support/#disabling-base-layout-styles). diff --git a/docs/explanations/user-interface/block-design.md b/docs/explanations/user-interface/block-design.md index 5df99b5398867f..e3a7b84bfa583e 100644 --- a/docs/explanations/user-interface/block-design.md +++ b/docs/explanations/user-interface/block-design.md @@ -64,7 +64,7 @@ Group toolbar controls in logical segments. Don't add a segment for each. ![A screenshot comparing a block toolbar with good vs. bad toolbar segment groupings.](https://make.wordpress.org/design/files/2021/03/docs__block-toolbar-do-dont.png) -### Block Identification +### Block identification A block should have a straightforward, short name so users can easily find it in the block library. A block named "YouTube" is easy to find and understand. The same block, named "Embedded Video (YouTube)", would be less clear and harder to find in the block library. @@ -84,7 +84,7 @@ Use concise block names. **Don't:** Avoid long, multi-line block names. -### Block Description +### Block description Every block should include a description that clearly explains the block's function. The description will display in the Settings Sidebar. @@ -116,7 +116,7 @@ Provide an instructive placeholder state. **Don't:** Avoid branding and relying on the title alone to convey instructions. -### Selected and Unselected States +### Selected and unselected states When unselected, your block should preview its content as closely to the front-end output as possible. @@ -130,7 +130,7 @@ For controls that are essential for the operation of the block, provide them dir **Don't:** Do not put controls that are essential to the block in the sidebar, otherwise the block will appear non-functional to mobile users or desktop users who have dismissed the sidebar. -### Advanced Block Settings +### Advanced block settings The “Block” tab of the Settings Sidebar can contain additional block options and configuration. Keep in mind that a user can dismiss the sidebar and never use it. You should not put critical options in the Sidebar. @@ -156,11 +156,11 @@ The most basic unit of the editor. The Paragraph block is a simple input field. ![Paragraph block](https://cldup.com/HVJe5bGZ8H-3000x3000.png) -### Placeholder: +#### Placeholder - Simple placeholder text that reads “Type / to choose a block”. The placeholder disappears when the block is selected. -### Selected state: +#### Selected state - Block Toolbar: Has a switcher to perform transformations to headings, etc. - Block Toolbar: Has basic text alignments @@ -172,11 +172,11 @@ Basic image block. ![Image block placeholder](https://cldup.com/w6FNywNsj1-3000x3000.png) -### Placeholder: +#### Placeholder - A generic gray placeholder block with options to upload an image, drag and drop an image directly on it, or pick an image from the media library. -### Selected state: +#### Selected state - Block Toolbar: Alignments, including wide and full-width if the theme supports it. - Block Toolbar: Edit Image, to open the Media Library @@ -185,7 +185,7 @@ Basic image block. ![Image Block](https://cldup.com/6YYXstl_xX-3000x3000.png) -### Block settings: +#### Block settings - Has description: “They're worth 1,000 words! Insert a single image.” - Has options for changing or adding alt text and adding additional custom CSS classes. @@ -196,18 +196,18 @@ _Future improvements to the Image block could include getting rid of the media m ![Latest Post Block](https://cldup.com/8lyAByDpy_-3000x3000.png) -### Placeholder: +#### Placeholder Has no placeholder as it works immediately upon insertion. The default inserted state shows the last 5 posts. -### Selected state: +#### Selected state - Block Toolbar: Alignments - Block Toolbar: Options for picking list view or grid view _Note that the Block Toolbar does not include the Block Chip in this case, since there are no similar blocks to switch to._ -### Block settings: +#### Block settings - Has description: “Display a list of your most recent posts.” - Has options for post order, narrowing the list by category, changing the default number of posts to show, and showing the post date. diff --git a/docs/getting-started/devenv/get-started-with-create-block.md b/docs/getting-started/devenv/get-started-with-create-block.md index 3a2c6607b82cff..2d46dd18cffe55 100644 --- a/docs/getting-started/devenv/get-started-with-create-block.md +++ b/docs/getting-started/devenv/get-started-with-create-block.md @@ -4,7 +4,7 @@ Custom blocks for the Block Editor in WordPress are typically registered using p The package is designed to help developers quickly set up a block development environment following WordPress best practices. -## Quick Start +## Quick start ### Installation diff --git a/docs/getting-started/devenv/get-started-with-wp-scripts.md b/docs/getting-started/devenv/get-started-with-wp-scripts.md index 6416adc081e70a..b6271620514df4 100644 --- a/docs/getting-started/devenv/get-started-with-wp-scripts.md +++ b/docs/getting-started/devenv/get-started-with-wp-scripts.md @@ -123,7 +123,7 @@ To help developers improve the quality of their code, `wp-scripts` comes pre-con Regularly linting and formatting your code ensures it's functional, clear, and maintainable for yourself and other developers. -### Running Tests +### Running tests Beyond just writing code, verifying its functionality is crucial. `wp-scripts` includes [Jest](https://jestjs.io/), a JavaScript testing framework, and both end-to-end and unit testing scripts: diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index 9dc542a5a24c9d..daaddd707c3156 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -2,7 +2,7 @@ A JavaScript Build Process is recommended for most cases when working with Javascript for the Block Editor. With a build process, you'll be able to work with ESNext and JSX (among others) syntaxes and features in your code while producing code ready for the majority of the browsers. -## JavaScript Build Process +## JavaScript build process ["ESNext"](https://developer.mozilla.org/en-US/docs/Web/JavaScript/JavaScript_technologies_overview#standardization_process) is a dynamic name that refers to Javascript's latest syntax and features. ["JSX"](https://react.dev/learn/writing-markup-with-jsx) is a custom syntax extension to JavaScript, created by React project, that allows you to write JavaScript using a familiar HTML tag-like syntax. diff --git a/docs/how-to-guides/README.md b/docs/how-to-guides/README.md index c0a2bd7f1fe6f8..152f8ce6184ae2 100644 --- a/docs/how-to-guides/README.md +++ b/docs/how-to-guides/README.md @@ -2,13 +2,13 @@ The new editor is highly flexible, like most of WordPress. You can build custom blocks, modify the editor's appearance, add special plugins, and much more. -## Creating Blocks +## Creating blocks The editor is about blocks, and the main extensibility API is the Block API. It allows you to create your own static blocks, [Dynamic Blocks](/docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md) ( rendered on the server ) and also blocks capable of saving data to Post Meta for more structured content. If you want to learn more about block creation, see the [Create a Block tutorial](/docs/getting-started/create-block/README.md) for the best place to start. -## Extending Blocks +## Extending blocks It is also possible to modify the behavior of existing blocks or even remove them completely using filters. @@ -24,11 +24,11 @@ Refer to the [Plugins](/packages/plugins/README.md) and [Edit Post](/packages/ed You can also filter certain aspects of the editor; this is documented on the [Editor Filters](/docs/reference-guides/filters/editor-filters.md) page. -## Meta Boxes +## Meta boxes Porting PHP meta boxes to blocks or sidebar plugins is highly encouraged, learn how in the [meta box](/docs/how-to-guides/metabox.md) and [sidebar plugin](/docs/how-to-guides/plugin-sidebar-0.md) guides. -## Theme Support +## Theme support By default, blocks provide their styles to enable basic support for blocks in themes without any change. Themes can add/override these styles, or rely on defaults. @@ -38,7 +38,7 @@ There are some advanced block features which require opt-in support in the theme Autocompleters within blocks may be extended and overridden. Learn more about the [autocomplete](/docs/reference-guides/filters/autocomplete-filters.md) filters. -## Block Parsing and Serialization +## Block parsing and serialization Posts in the editor move through a couple of different stages between being stored in `post_content` and appearing in the editor. Since the blocks themselves are data structures that live in memory it takes a parsing and serialization step to transform out from and into the stored format in the database. diff --git a/docs/how-to-guides/accessibility.md b/docs/how-to-guides/accessibility.md index 75458d6690cab4..bdf9c977b5cf44 100644 --- a/docs/how-to-guides/accessibility.md +++ b/docs/how-to-guides/accessibility.md @@ -4,7 +4,7 @@ Accessibility documentation for developers working on the Gutenberg Project. For more information on accessibility and WordPress see the [Make WordPress Accessibility Handbook](https://make.wordpress.org/accessibility/handbook/) and the [Accessibility Team section](https://make.wordpress.org/accessibility/). -## Landmark Regions +## Landmark regions It is a best practice to include ALL content on the page in landmarks, so that screen reader users who rely on them to navigate from section to section do not lose track of content. diff --git a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md index 9dc7f1f324743f..94d4ea67d8cf99 100644 --- a/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md +++ b/docs/how-to-guides/block-tutorial/nested-blocks-inner-blocks.md @@ -36,7 +36,7 @@ registerBlockType( 'gutenberg-examples/example-06', { } ); ``` -## Allowed Blocks +## Allowed blocks Using the `allowedBlocks` property, you can define the set of blocks allowed in your InnerBlock. This restricts the blocks that can be included only to those listed, all other blocks will not show in the inserter. @@ -56,7 +56,7 @@ By default, `InnerBlocks` expects its blocks to be shown in a vertical list. A v Specifying this prop does not affect the layout of the inner blocks, but results in the block mover icons in the child blocks being displayed horizontally, and also ensures that drag and drop works correctly. -## Default Block +## Default block By default `InnerBlocks` opens a list of permitted blocks via `allowedBlocks` when the block appender is clicked. You can modify the default block and its attributes that are inserted when the initial block appender is clicked by using the `defaultBlock` property. For example: @@ -93,7 +93,7 @@ const MY_TEMPLATE = [ Use the `templateLock` property to lock down the template. Using `all` locks the template completely so no changes can be made. Using `insert` prevents additional blocks from being inserted, but existing blocks can be reordered. See [templateLock documentation](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-editor/src/components/inner-blocks/README.md#templatelock) for additional information. -### Post Template +### Post template Unrelated to `InnerBlocks` but worth mentioning here, you can create a [post template](https://developer.wordpress.org/block-editor/developers/block-api/block-templates/) by post type, that preloads the block editor with a set of blocks. @@ -109,7 +109,7 @@ add_action( 'init', function() { } ); ``` -## Using Parent and Ancestor Relationships in Blocks +## Using parent and ancestor relationships in blocks A common pattern for using InnerBlocks is to create a custom block that will be only be available if its parent block is inserted. This allows builders to establish a relationship between blocks, while limiting a nested block's discoverability. Currently, there are two relationships builders can use: `parent` and `ancestor`. The differences are: @@ -118,7 +118,7 @@ A common pattern for using InnerBlocks is to create a custom block that will be The key difference between `parent` and `ancestor` is `parent` has finer specificity, while an `ancestor` has greater flexibility in its nested hierarchy. -### Defining Parent Block Relationship +### Defining parent block relationship An example of this is the Column block, which is assigned the `parent` block setting. This allows the Column block to only be available as a nested direct descendant in its parent Columns block. Otherwise, the Column block will not be available as an option within the block inserter. See [Column code for reference](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/column). @@ -133,7 +133,7 @@ When defining a direct descendent block, use the `parent` block setting to defin } ``` -### Defining Ancestor Block Relationship +### Defining an ancestor block relationship An example of this is the Comment Author Name block, which is assigned the `ancestor` block setting. This allows the Comment Author Name block to only be available as a nested descendant in its ancestral Comment Template block. Otherwise, the Comment Author Name block will not be available as an option within the block inserter. See [Comment Author Name code for reference](https://github.com/WordPress/gutenberg/tree/HEAD/packages/block-library/src/comment-author-name). @@ -150,7 +150,7 @@ When defining a descendent block, use the `ancestor` block setting. This prevent } ``` -## Using a React Hook +## Using a React hook You can use a react hook called `useInnerBlocksProps` instead of the `InnerBlocks` component. This hook allows you to take more control over the markup of inner blocks areas. diff --git a/docs/how-to-guides/feature-flags.md b/docs/how-to-guides/feature-flags.md index 49fb8ba487edf5..5855f93f8ed9fc 100644 --- a/docs/how-to-guides/feature-flags.md +++ b/docs/how-to-guides/feature-flags.md @@ -8,7 +8,7 @@ The `process.env.IS_GUTENBERG_PLUGIN` is an environment variable whose value 'fl When the codebase is built for the plugin, this variable will be set to `true`. When building for WordPress core, it will be set to `false` or `undefined`. -## Basic Use +## Basic usage ### Exporting features @@ -69,7 +69,7 @@ if ( undefined ) { // Wepack has replaced `process.env.IS_GUTENBERG_PLUGIN` with `undefined` evaluates to `false` so the plugin-only feature will not be executed. -### Dead Code Elimination +### Dead code elimination For production builds, webpack ['minifies'](https://en.wikipedia.org/wiki/Minification_(programming)) the code, removing as much unnecessary JavaScript as it can. @@ -97,8 +97,8 @@ if ( undefined ) { In this case, the minification process will remove the entire `if` statement including the body, ensuring plugin-only code is not included in WordPress core build. -## FAQ +## Frequently asked questions -#### Why shouldn't I assign the result of an expression involving `IS_GUTENBERG_PLUGIN` to a variable, e.g. `const isMyFeatureActive = process.env.IS_GUTENBERG_PLUGIN === 2`? +### Why shouldn't I assign the result of an expression involving `IS_GUTENBERG_PLUGIN` to a variable, e.g. `const isMyFeatureActive = process.env.IS_GUTENBERG_PLUGIN === 2`? Introducing complexity may prevent webpack's minifier from identifying and therefore eliminating dead code. Therefore it is recommended to use the examples in this document to ensure your feature flag functions as intended. For further details, see the [Dead Code Elimination](#dead-code-elimination) section. diff --git a/docs/how-to-guides/format-api.md b/docs/how-to-guides/format-api.md index 00e1b82675c006..fe989575f8b00e 100644 --- a/docs/how-to-guides/format-api.md +++ b/docs/how-to-guides/format-api.md @@ -173,7 +173,7 @@ registerFormatType( 'my-custom-format/sample-output', { } ); ``` -### Step5: Add a button outside of the dropdown (Optional) +### Step 5: Add a button outside of the dropdown (Optional) Using the `RichTextToolbarButton` component, the button is added to the default dropdown menu. You can add the button directly to the toolbar by using the `BlockControls` component. @@ -220,7 +220,7 @@ If you run into errors: - Confirm the JavaScript is loading in the editor. - Check for any console error messages. -## Additional Resources +## Additional resources Reference documentation used in this guide: diff --git a/docs/how-to-guides/internationalization.md b/docs/how-to-guides/internationalization.md index 08ce46edb3f581..cd341f1b9c6c5d 100644 --- a/docs/how-to-guides/internationalization.md +++ b/docs/how-to-guides/internationalization.md @@ -1,6 +1,6 @@ # Internationalization -## What is Internationalization? +## What is internationalization? Internationalization is the process to provide multiple language support to software, in this case WordPress. Internationalization is often abbreviated as **i18n**, where 18 stands for the number of letters between the first _i_ and the last _n_. @@ -87,11 +87,11 @@ This is all you need to make your plugin JavaScript code translatable. When you set script translations for a handle WordPress will automatically figure out if a translations file exists on translate.wordpress.org, and if so ensure that it's loaded into `wp.i18n` before your script runs. With translate.wordpress.org, plugin authors also do not need to worry about setting up their own infrastructure for translations and can rely on a global community with dozens of active locales. Read more about [WordPress Translations](https://make.wordpress.org/meta/handbook/documentation/translations/). -## Provide Your Own Translations +## Provide your own translations You can create and ship your own translations with your plugin, if you have sufficient knowledge of the language(s) you can ensure the translations are available. -### Create Translation File +### Create the translation file The translation files must be in the JED 1.x JSON format. @@ -204,7 +204,7 @@ This will generate the JSON file `myguten-eo-[md5].json` with the contents: } ``` -### Load Translation File +### Load the translation file The final part is to tell WordPress where it can look to find the translation file. The `wp_set_script_translations` function accepts an optional third argument that is the path it will first check for translations. For example: @@ -220,12 +220,12 @@ WordPress will check for a file in that path with the format `${domain}-${locale Using `make-json` automatically names the file with the md5 hash, so it is ready as-is. You could rename the file to use the handle instead, in which case the file name would be `myguten-eo-myguten-script.json`. -### Test Translations +### Test translations You will need to set your WordPress installation to Esperanto language. Go to Settings > General and change your site language to Esperanto. With the language set, create a new post, add the block, and you will see the translations used. -### Filtering Translations +### Filtering translations The outputs of the translation functions (`__()`, `_x()`, `_n()`, and `_nx()`) are filterable, see [i18n Filters](/docs/reference-guides/filters/i18n-filters.md) for full information. diff --git a/docs/how-to-guides/metabox.md b/docs/how-to-guides/metabox.md index e0402b1180c1c3..b1baac1f255855 100644 --- a/docs/how-to-guides/metabox.md +++ b/docs/how-to-guides/metabox.md @@ -8,7 +8,7 @@ The block editor does support most existing meta boxes, see [the backward compat If you are interested in working with the post meta outside the editor, check out the [Sidebar Tutorial](/docs/how-to-guides/sidebar-tutorial/plugin-sidebar-0.md). -### Use Blocks to Store Meta +### Use blocks to store meta Typically, blocks store attribute values in the serialized block HTML. However, you can also create a block that saves its attribute values as post meta, that can be accessed programmatically anywhere in your template. @@ -35,7 +35,7 @@ A [complete meta-block example](https://github.com/WordPress/block-development-e 3. [Use Post Meta Data](#step-3-use-post-meta-data) 4. [Finishing Touches](#step-4-use-block-templates-optional) -### Step 1: Register Meta Field +### Step 1: Register meta field A post meta field is a WordPress object used to store extra data about a post. You need to first register a new meta field prior to use. See Managing [Post Metadata](https://developer.wordpress.org/plugins/metadata/managing-post-metadata/) to learn more about post meta. @@ -58,7 +58,7 @@ function myguten_register_post_meta() { add_action( 'init', 'myguten_register_post_meta' ); ``` -### Step 2: Add Meta Block +### Step 2: Add meta block With the meta field registered in the previous step, next create a new block to display the field value to the user. @@ -114,11 +114,11 @@ You could also confirm the data is saved by checking the database table `wp_post **Troubleshooting**: Be sure to build your code between changes, you updated the PHP code from Step 1, and JavaScript files are enqueued. Check the build output and developer console for errors. -### Step 3: Use Post Meta Data +### Step 3: Use post meta data You can use the post meta data stored in the last step in multiple ways. -#### Use Post Meta in PHP +#### Use post meta in PHP The first example uses the value from the post meta field and appends it to the end of the post content wrapped in a `H4` tag. @@ -134,7 +134,7 @@ function myguten_content_filter( $content ) { add_filter( 'the_content', 'myguten_content_filter' ); ``` -#### Use Post Meta in Block +#### Use post meta in a block You can also use the post meta data in other blocks. For this example the data is loaded at the end of every Paragraph block when it is rendered, ie. shown to the user. You can replace this for any core or custom block types as needed. @@ -157,7 +157,7 @@ register_block_type( 'core/paragraph', array( ) ); ``` -### Step 4: Use Block Templates (optional) +### Step 4: Use block templates (optional) One problem using a meta block is the block is easy for an author to forget, since it requires being added to each post. You solve this by using [block templates](/docs/reference-guides/block-api/block-templates.md). A block template is a predefined list of block items per post type. Templates allow you to specify a default initial state for a post type. @@ -181,9 +181,9 @@ You can also add other block types in the array, including placeholders, or even This guide showed how using blocks you can read and write to post meta. See the section below for backward compatibility with existing meta boxes. -## Backward Compatibility +## Backward compatibility -### Testing, Converting, and Maintaining Existing Meta Boxes +### Testing, converting, and maintaining existing meta boxes Before converting meta boxes to blocks, it may be easier to test if a meta box works with the block editor, and explicitly mark it as such. @@ -213,7 +213,7 @@ add_meta_box( 'my-meta-box', 'My Meta Box', 'my_meta_box_callback', When the block editor is used, this meta box will no longer be displayed in the meta box area, as it now only exists for backward compatibility purposes. It will display as before in the classic editor. -### Meta Box Data Collection +### Meta box data collection On each block editor page load, we register an action that collects the meta box data to determine if an area is empty. The original global state is reset upon collection of meta box data. @@ -227,14 +227,14 @@ Then each location for this particular type of meta box is checked for whether i Ideally, this could be done at instantiation of the editor and help simplify this flow. However, it is not possible to know the meta box state before `admin_enqueue_scripts`, where we are calling `initializeEditor()`. This will have to do, unless we want to move `initializeEditor()` to fire in the footer or at some point after `admin_head`. With recent changes to editor bootstrapping this might now be possible. Test with ACF to make sure. -### Redux and React Meta Box Management +### Redux and React meta box management When rendering the block editor, the meta boxes are rendered to a hidden div `#metaboxes`. _The Redux store will hold all meta boxes as inactive by default_. When `INITIALIZE_META_BOX_STATE` comes in, the store will update any active meta box areas by setting the `isActive` flag to `true`. Once this happens React will check for the new props sent in by Redux on the `MetaBox` component. If that `MetaBox` is now active, instead of rendering null, a `MetaBoxArea` component will be rendered. The `MetaBox` component is the container component that mediates between the `MetaBoxArea` and the Redux Store. _If no meta boxes are active, nothing happens. This will be the default behavior, as all core meta boxes have been stripped._ -#### MetaBoxArea Component +#### MetaBoxArea component When the component renders it will store a reference to the meta boxes container and retrieve the meta boxes HTML from the prefetch location. @@ -250,7 +250,7 @@ This url is automatically passed into React via a `_wpMetaBoxUrl` global variabl This page mimics the `post.php` post form, so when it is submitted it will fire all of the normal hooks and actions, and have the proper global state to correctly fire any PHP meta box mumbo jumbo without needing to modify any existing code. On successful submission, React will signal a `handleMetaBoxReload` to remove the updating overlay. -### Common Compatibility Issues +### Common compatibility issues Most PHP meta boxes should continue to work in the block editor, but some meta boxes that include advanced functionality could break. Here are some common reasons why meta boxes might not work as expected in the block editor: diff --git a/docs/how-to-guides/notices/README.md b/docs/how-to-guides/notices/README.md index 4b9a32c664a96c..d52004b7e48452 100644 --- a/docs/how-to-guides/notices/README.md +++ b/docs/how-to-guides/notices/README.md @@ -73,7 +73,7 @@ To better understand the specific code example above: Check out the [_Loading JavaScript_](/docs/how-to-guides/javascript/loading-javascript.md) tutorial for a primer on how to load your custom JavaScript into the block editor. -## Learn More +## Learn more The block editor offers a complete API for generating notices. The official documentation is a great place to review what's possible. diff --git a/docs/how-to-guides/platform/README.md b/docs/how-to-guides/platform/README.md index f63f8b6a5cbb1c..2ed96d6c83d876 100644 --- a/docs/how-to-guides/platform/README.md +++ b/docs/how-to-guides/platform/README.md @@ -2,7 +2,7 @@ The Gutenberg Project is not only building a better editor for WordPress, but also creating a platform to build upon. This platform consists of a set of JavaScript packages and tools that you can use in your web application. [View the list of packages available on npm](https://www.npmjs.com/org/wordpress). -## UI Components +## UI components The [WordPress Components package](/packages/components/README.md) contains a set of UI components you can use in your project. See the [WordPress Storybook site](https://wordpress.github.io/gutenberg/) for an interactive guide to the available components and settings. @@ -26,7 +26,7 @@ function MyApp() { Many components include CSS to add style, you will need to include for the components to appear correctly. The component stylesheet can be found in `node_modules/@wordpress/components/build-style/style.css`, you can link directly or copy and include it in your project. -## Development Scripts +## Development scripts The [`@wordpress/scripts` package](/packages/scripts/README.md) is a collection of reusable scripts for JavaScript development — includes scripts for building, linting, and testing — all with no additional configuration files. diff --git a/docs/how-to-guides/plugin-sidebar-0.md b/docs/how-to-guides/plugin-sidebar-0.md index bf084680c3d1b7..95bd26f7ee1f85 100644 --- a/docs/how-to-guides/plugin-sidebar-0.md +++ b/docs/how-to-guides/plugin-sidebar-0.md @@ -14,7 +14,7 @@ The tutorial assumes you have an existing plugin setup and are ready to add PHP ## Step-by-step guide -### Step 1: Get a Sidebar up and Running +### Step 1: Get a sidebar up and running The first step is to tell the editor that there is a new plugin that will have its own sidebar. Use the [registerPlugin](/packages/plugins/README.md), [PluginSidebar](/packages/edit-post/README.md#pluginsidebar), and [createElement](/packages/element/README.md) utilities provided by the `@wordpress/plugins`, `@wordpress/edit-post`, and `react` packages, respectively. @@ -168,7 +168,7 @@ Reload the editor and open the sidebar: This code doesn't let users store or retrieve data just yet, so the next steps will focus on how to connect it to the meta block field. -### Step 3: Register the Meta Field +### Step 3: Register the meta field To work with fields in the `post_meta` table, use the [register_post_meta](https://developer.wordpress.org/reference/functions/register_post_meta/). function to create a new field called `sidebar_plugin_meta_block_field`. @@ -194,7 +194,7 @@ The function will return an object containing the registered meta field you regi If the code returns `undefined` make sure your post type supports `custom-fields`. Either when [registering the post](https://developer.wordpress.org/reference/functions/register_post_type/#supports) or with [add_post_type_support function](https://developer.wordpress.org/reference/functions/add_post_type_support/). -### Step 4: Initialize the Input Control +### Step 4: Initialize the input control With the field available in the editor store, it can now be surfaced to the UI. We extract the input control to a function to keep the code clean as we add functionality. @@ -297,7 +297,7 @@ wp.data You can observe the content changing in the input component. -### Step 5: Update the Meta Field When the Input's Content Changes +### Step 5: Update the meta field when the input's content changes The last step is to update the meta field when the input content changes. The `useDispatch` function takes a store name as its only argument and returns methods that you can use to update the store, in this case we'll use `editPost` diff --git a/docs/how-to-guides/propagating-updates.md b/docs/how-to-guides/propagating-updates.md index ae5cb5f3b2a9ac..5f2861a4e456c6 100644 --- a/docs/how-to-guides/propagating-updates.md +++ b/docs/how-to-guides/propagating-updates.md @@ -40,7 +40,7 @@ It is not fool-proof because users can modify the class via the editor UI. Howe As the name suggests, these blocks are inherently synced across your site. Keep in mind that there are currently limitations with relying on reusable blocks to handle certain updates since content, HTML structure, and styles will all stay in sync when updates happen. If you require more nuance than that, this is a key element to factor in and a dynamic block might be a better approach. -### Template Parts and Templates +### Template parts and templates Because block themes allow users to directly edit templates and template parts, how changes are managed need to be adjusted in light of the greater access given to users. For context, when templates or template parts are changed by the user, the updated templates from the theme update don’t show for the user. Only new users of the theme or users who have not yet edited a template are experiencing the updated template. If users haven’t modified the files then the changes you make on the file system will be reflected on the user’s sites – you just need to update the files and they’ll get the changes. However if they have made changes to their templates then the only way you can update their templates is to: diff --git a/docs/manifest.json b/docs/manifest.json index fb2ce08aa8b910..929b001fd2dc38 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -2076,7 +2076,7 @@ "parent": "architecture" }, { - "title": "Styles", + "title": "Styles in the Editor", "slug": "styles", "markdown_source": "../docs/explanations/architecture/styles.md", "parent": "architecture" diff --git a/docs/reference-guides/block-api/block-attributes.md b/docs/reference-guides/block-api/block-attributes.md index 35ec1c1e7c64e4..cef32e5bb42fe8 100644 --- a/docs/reference-guides/block-api/block-attributes.md +++ b/docs/reference-guides/block-api/block-attributes.md @@ -68,7 +68,7 @@ The saved HTML will contain the `title` and `size` in the comment delimiter, and If an attributes change over time then a [block deprecation](/docs/reference-guides/block-api/block-deprecation.md) can help migrate from an older attribute, or remove it entirely. -## Type Validation +## Type validation The `type` indicates the type of data that is stored by the attribute. It does not indicate where the data is stored, which is defined by the `source` field. @@ -86,7 +86,7 @@ The `type` field MUST be one of the following: Note that the validity of an `object` is determined by your `source`. For an example, see the `query` details below. -## Enum Validation +## Enum validation An attribute can be defined as one of a fixed set of values. This is specified by an `enum`, which contains an array of allowed values: @@ -100,7 +100,7 @@ _Example_: Example `enum`. } ``` -## Value Source +## Value source Attribute sources are used to define how the attribute values are extracted from saved post content. They provide a mechanism to map from the saved markup to a JavaScript representation of a block. @@ -430,7 +430,7 @@ function onChange( event ) { } ``` -## Default Value +## Default value A block attribute can contain a default value, which will be used if the `type` and `source` do not match anything within the block content. diff --git a/docs/reference-guides/block-api/block-context.md b/docs/reference-guides/block-api/block-context.md index 3ab3175a0d1b23..3541f632673d35 100644 --- a/docs/reference-guides/block-api/block-context.md +++ b/docs/reference-guides/block-api/block-context.md @@ -6,11 +6,11 @@ This is especially useful in full-site editing where, for example, the contents If you are familiar with [React Context](https://reactjs.org/docs/context.html), block context adopts many of the same ideas. In fact, the client-side block editor implementation of block context is a very simple application of React Context. Block context is also supported in server-side `render_callback` implementations, demonstrated in the examples below. -## Defining Block Context +## Defining block context Block context is defined in the registered settings of a block. A block can provide a context value, or consume a value it seeks to inherit. -### Providing Block Context +### Providing block context A block can provide a context value by assigning a `providesContext` property in its registered settings. This is an object which maps a context name to one of the block's own attribute. The value corresponding to that attribute value is made available to descendent blocks and can be referenced by the same context name. Currently, block context only supports values derived from the block's own attributes. This could be enhanced in the future to support additional sources of context values. @@ -32,7 +32,7 @@ For complete example, refer to the section below. As seen in the above example, it is recommended that you include a namespace as part of the name of the context key so as to avoid potential conflicts with other plugins or default context values provided by WordPress. The context namespace should be specific to your plugin, and in most cases can be the same as used in the name of the block itself. -### Consuming Block Context +### Consuming block context A block can inherit a context value from an ancestor provider by assigning a `usesContext` property in its registered settings. This should be assigned as an array of the context names the block seeks to inherit. @@ -45,7 +45,7 @@ registerBlockType('my-plugin/record-title', { ``` -## Using Block Context +## Using block context Once a block has defined the context it seeks to inherit, this can be accessed in the implementation of `edit` (JavaScript) and `render_callback` (PHP). It is provided as an object (JavaScript) or associative array (PHP) of the context values which have been defined for the block. Note that a context value will only be made available if the block explicitly defines a desire to inherit that value. diff --git a/docs/reference-guides/block-api/block-edit-save.md b/docs/reference-guides/block-api/block-edit-save.md index a8b6f9171bdef3..3a833e3d5ce346 100644 --- a/docs/reference-guides/block-api/block-edit-save.md +++ b/docs/reference-guides/block-api/block-edit-save.md @@ -24,7 +24,7 @@ const blockSettings = { }; ``` -### block wrapper props +### Block wrapper props The first thing to notice here is the use of the `useBlockProps` React hook on the block wrapper element. In the example above, the block wrapper renders a "div" in the editor, but in order for the Gutenberg editor to know how to manipulate the block, add any extra classNames that are needed for the block... the block wrapper element should apply props retrieved from the `useBlockProps` react hook call. The block wrapper element should be a native DOM element, like `<div>` and `<table>`, or a React component that forwards any additional props to native DOM elements. Using a `<Fragment>` or `<ServerSideRender>` component, for instance, would be invalid. diff --git a/docs/reference-guides/block-api/block-metadata.md b/docs/reference-guides/block-api/block-metadata.md index d023742092df1e..f89257f52d0446 100644 --- a/docs/reference-guides/block-api/block-metadata.md +++ b/docs/reference-guides/block-api/block-metadata.md @@ -61,7 +61,7 @@ Starting in WordPress 5.8 release, we recommend using the `block.json` metadata } ``` -## Benefits using the metadata file +## Benefits of using the metadata file The block definition allows code sharing between JavaScript, PHP, and other languages when processing block types stored as JSON, and registering blocks with the `block.json` metadata file provides multiple benefits on top of it. @@ -85,7 +85,7 @@ Check <a href="https://developer.wordpress.org/block-editor/getting-started/fund This section describes all the properties that can be added to the `block.json` file to define the behavior and metadata of block types. -### API Version +### API version - Type: `number` - Optional @@ -460,7 +460,7 @@ Block Hooks is an API that allows a block to automatically insert itself next to The key is the name of the block (`string`) to hook into, and the value is the position to hook into (`string`). Take a look at the [Block Hooks documentation](/docs/reference-guides/block-api/block-registration.md#block-hooks-optional) for more info about available configurations. -### Editor Script +### Editor script - Type: `WPDefinedAsset`|`WPDefinedAsset[]` ([learn more](#wpdefinedasset)) - Optional @@ -494,7 +494,7 @@ It's possible to pass a script handle registered with the [`wp_register_script`] _Note: An option to pass also an array of scripts exists since WordPress `6.1.0`._ -### View Script +### View script - Type: `WPDefinedAsset`|`WPDefinedAsset[]` ([learn more](#wpdefinedasset)) - Optional @@ -512,7 +512,7 @@ It's possible to pass a script handle registered with the [`wp_register_script`] _Note: An option to pass also an array of view scripts exists since WordPress `6.1.0`._ -### Editor Style +### Editor style - Type: `WPDefinedAsset`|`WPDefinedAsset[]` ([learn more](#wpdefinedasset)) - Optional @@ -649,7 +649,7 @@ return array( ); ``` -### Frontend Enqueueing +### Frontend enqueueing Starting in the WordPress 5.8 release, it is possible to instruct WordPress to enqueue scripts and styles for a block type only when rendered on the frontend. It applies to the following asset fields in the `block.json` file: @@ -706,7 +706,7 @@ registerBlockType( metadata, { } ); ``` -## Backward Compatibility +## Backward compatibility The existing registration mechanism (both server side and frontend) will continue to work, it will serve as low-level implementation detail for the `block.json` based registration. diff --git a/docs/reference-guides/block-api/block-patterns.md b/docs/reference-guides/block-api/block-patterns.md index a1da09d63a6c99..87e268a49f6f56 100644 --- a/docs/reference-guides/block-api/block-patterns.md +++ b/docs/reference-guides/block-api/block-patterns.md @@ -14,7 +14,7 @@ In this Document: - [Block patterns contextual to block types and pattern transformations](#block-patterns-contextual-to-block-types-and-pattern-transformations) - [Semantic block patterns](#semantic-block-patterns) -## Block Patterns +## Block patterns ### register_block_pattern @@ -63,7 +63,7 @@ function my_plugin_register_my_patterns() { add_action( 'init', 'my_plugin_register_my_patterns' ); ``` -## Unregistering Block Patterns +## Unregistering block patterns ### unregister_block_pattern @@ -88,7 +88,7 @@ function my_plugin_unregister_my_patterns() { add_action( 'init', 'my_plugin_unregister_my_patterns' ); ``` -## Block Pattern Categories +## Block pattern categories Block patterns can be grouped using categories. The block editor comes with bundled categories you can use on your custom block patterns. You can also register your own block pattern categories. diff --git a/docs/reference-guides/block-api/block-registration.md b/docs/reference-guides/block-api/block-registration.md index d2ff5f429c2675..bec9bbd871cbae 100644 --- a/docs/reference-guides/block-api/block-registration.md +++ b/docs/reference-guides/block-api/block-registration.md @@ -29,7 +29,7 @@ _Note:_ A block name can only contain lowercase alphanumeric characters and dash _Note:_ This name is used on the comment delimiters as `<!-- wp:my-plugin/book -->`. Those blocks provided by core don't include a namespace when serialized. -### Block Configuration +### Block configuration - **Type:** `Object` [ `{ key: value }` ] @@ -304,7 +304,7 @@ It’s crucial to emphasize that the Block Hooks feature is only designed to wor Block Hooks will not work with post content or patterns crafted by the user, such as synced patterns, or theme templates and template parts that have been modified by the user. -## Block Collections +## Block collections ## `registerBlockCollection` diff --git a/docs/reference-guides/block-api/block-selectors.md b/docs/reference-guides/block-api/block-selectors.md index 1771e54c33708b..b4b546bd9aae4e 100644 --- a/docs/reference-guides/block-api/block-selectors.md +++ b/docs/reference-guides/block-api/block-selectors.md @@ -12,7 +12,7 @@ when their styles are generated. A block may customize its CSS selectors at three levels: root, feature, and subfeature. -## Root Selector +## Root selector The root selector is the block's primary CSS selector. @@ -31,7 +31,7 @@ default is generated in the form of `.wp-block-<name>`. } ``` -## Feature Selectors +## Feature selectors Feature selectors relate to styles for a block support, e.g. border, color, typography, etc. @@ -53,7 +53,7 @@ but applying the typography styles to an inner heading only. } ``` -## Subfeature Selectors +## Subfeature selectors These selectors relate to individual styles provided by a block support e.g. `background-color` diff --git a/docs/reference-guides/block-api/block-templates.md b/docs/reference-guides/block-api/block-templates.md index 494a99bf73af73..b664867eb5b67f 100644 --- a/docs/reference-guides/block-api/block-templates.md +++ b/docs/reference-guides/block-api/block-templates.md @@ -61,7 +61,7 @@ registerBlockType( 'myplugin/template', { See the [Meta Block Tutorial](/docs/how-to-guides/metabox.md#step-4-finishing-touches) for a full example of a template in use. -## Block Attributes +## Block attributes To find a comprehensive list of all block attributes that you can define in a template, consult the block's `block.json` file, and look at the `attributes` and `supports` values. @@ -69,7 +69,7 @@ For example, [packages/block-library/src/heading/block.json](https://github.com/ If you don't have the Gutenberg plugin installed, you can find `block.json` files inside `wp-includes/blocks/heading/block.json`. -## Custom Post types +## Custom post types A custom post type can register its own template during registration: @@ -161,7 +161,7 @@ $template = array( ); ``` -## Nested Templates +## Nested templates Container blocks like the columns blocks also support templates. This is achieved by assigning a nested template to the block. diff --git a/docs/reference-guides/block-api/block-transforms.md b/docs/reference-guides/block-api/block-transforms.md index a91444981b76fb..c2c5ed49d1b19c 100644 --- a/docs/reference-guides/block-api/block-transforms.md +++ b/docs/reference-guides/block-api/block-transforms.md @@ -22,7 +22,7 @@ export const settings = { }; ``` -## Transformations Types +## Transformations types This section goes through the existing types of transformations blocks support: diff --git a/docs/reference-guides/packages.md b/docs/reference-guides/packages.md index 7dbe06769c42e9..ce9c4b7950c29e 100644 --- a/docs/reference-guides/packages.md +++ b/docs/reference-guides/packages.md @@ -2,7 +2,7 @@ WordPress exposes a list of JavaScript packages and tools for WordPress development. -## Using the Packages via WordPress Global +## Using the packages via WordPress global JavaScript packages are available as a registered script in WordPress and can be accessed using the `wp` global variable. @@ -22,7 +22,7 @@ After the dependency is declared, you can access the module in your JavaScript c const { PlainText } = wp.blockEditor; ``` -## Using the Packages via npm +## Using the packages via npm All the packages are also available on [npm](https://www.npmjs.com/org/wordpress) if you want to bundle them in your code. diff --git a/docs/reference-guides/richtext.md b/docs/reference-guides/richtext.md index 1a4509318b72b7..a203c4d54b7a3b 100644 --- a/docs/reference-guides/richtext.md +++ b/docs/reference-guides/richtext.md @@ -10,11 +10,11 @@ The RichText component is extremely powerful because it provides built-in functi Unlike other components that exist in the [Component Reference](/packages/components/README.md) section, RichText lives separately because it only makes sense within the block editor, and not within other areas of WordPress. -## Property Reference +## Property reference For a list of the possible properties to pass your RichText component, [check out the component documentation on GitHub](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md). -## Core Blocks Using the RichText Component +## Core blocks using the RichText component There are a number of core blocks using the RichText component. The JavaScript edit function linked below for each block can be used as a best practice reference while creating your own blocks. @@ -65,15 +65,15 @@ registerBlockType( /* ... */, { } ); ``` -## Common Issues & Solutions +## Common issues and solutions While using the RichText component a number of common issues tend to appear. -### HTML Formatting Tags Display in the Content +### HTML formatting tags display in the content If the HTML tags from text formatting such as `<strong>` or `<em>` are being escaped and displayed on the frontend of the site, this is likely due to an issue in your save function. Make sure your code looks something like `<RichText.Content tagName="h2" value={ heading } />` (JSX) within your save function instead of simply outputting the value with `<h2>{ heading }</h2>`. -### Unwanted Formatting Options Still Display +### Unwanted formatting options still display Before moving forward, consider if using the RichText component makes sense at all. Would it be better to use a basic `input` or `textarea` element? If you don't think any formatting should be possible, these HTML tags may make more sense. @@ -81,7 +81,7 @@ If you'd still like to use RichText, you can eliminate all of the formatting opt If you want to limit the formats allowed, you can specify using `allowedFormats` property in your code, see the example above or [the component documentation](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md#allowedformats-array) for details. -### Disable Specific Format Types in Editor +### Disable specific fromat types in Editor The RichText component uses formats to display inline elements, for example images within the paragraph block. If you just want to disable a format from the editor, you can use the `unregisterFormatType` function. For example to disable inline images, use: diff --git a/docs/reference-guides/theme-json-reference/README.md b/docs/reference-guides/theme-json-reference/README.md index 819e7d561cda00..92f6f77e298c00 100644 --- a/docs/reference-guides/theme-json-reference/README.md +++ b/docs/reference-guides/theme-json-reference/README.md @@ -4,7 +4,7 @@ This reference guide lists the settings and style properties defined in the them - [Version 2 (living reference)](/docs/reference-guides/theme-json-reference/theme-json-living.md) -## Older Versions +## Older versions - [Migrating to Newer Theme.json Versions](/docs/reference-guides/theme-json-reference/theme-json-migrations.md) - [Version 1](/docs/reference-guides/theme-json-reference/theme-json-v1.md) diff --git a/docs/reference-guides/theme-json-reference/styles-versions.md b/docs/reference-guides/theme-json-reference/styles-versions.md index 734c59c3d159ce..0cbffdc6ebfe07 100644 --- a/docs/reference-guides/theme-json-reference/styles-versions.md +++ b/docs/reference-guides/theme-json-reference/styles-versions.md @@ -2,7 +2,7 @@ New styles options are integrated into theme.json on a regular basis. Knowing the style options available through theme.json or the styles editor at any given time can be challenging. To clarify, the table below indicates the WordPress version when each theme.json styles option became available and when a corresponding control was added to the user interface to allow management of the style from the Styles editor. -## Styles Keys +## Styles keys | Key | theme.json Since| Style Editor Since | | --- | :---: | :---: | From e01e33b6e67ba3c3ba02652589ca77acd4d98442 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:51:35 -0500 Subject: [PATCH 241/325] Bump actions/upload-artifact from 3.1.3 to 4.0.0 (#57111) Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3.1.3 to 4.0.0. - [Release notes](https://github.com/actions/upload-artifact/releases) - [Commits](https://github.com/actions/upload-artifact/compare/a8a3f3ad30e3422c9c7b888a15615d19a852ae32...c7d193f32edcb7bfad88892161225aeda64e9392) --- updated-dependencies: - dependency-name: actions/upload-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-plugin-zip.yml | 4 ++-- .github/workflows/end2end-test.yml | 8 ++++---- .github/workflows/performance.yml | 4 ++-- .github/workflows/rnmobile-android-runner.yml | 4 ++-- .github/workflows/rnmobile-ios-runner.yml | 4 ++-- .github/workflows/upload-release-to-plugin-repo.yml | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index f0c704e10456c7..3ffb9f836d7217 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -182,7 +182,7 @@ jobs: NO_CHECKS: 'true' - name: Upload artifact - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 with: name: gutenberg-plugin path: ./gutenberg.zip @@ -205,7 +205,7 @@ jobs: - name: Upload release notes artifact if: ${{ needs.bump-version.outputs.new_version }} - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 with: name: release-notes path: ./release-notes.txt diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 5a9750c6bb0456..86be3fe8231c2f 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -43,7 +43,7 @@ jobs: npx wp-scripts test-e2e --config=./packages/e2e-tests/jest.config.js --cacheDirectory="$HOME/.jest-cache" - name: Archive debug artifacts (screenshots, HTML snapshots) - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: failures-artifacts @@ -51,7 +51,7 @@ jobs: if-no-files-found: ignore - name: Archive flaky tests report - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: flaky-tests-report @@ -94,7 +94,7 @@ jobs: xvfb-run --auto-servernum --server-args="-screen 0 1280x960x24" -- npm run test:e2e:playwright -- --shard=${{ matrix.part }}/${{ matrix.totalParts }} - name: Archive debug artifacts (screenshots, traces) - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: failures-artifacts @@ -102,7 +102,7 @@ jobs: if-no-files-found: ignore - name: Archive flaky tests report - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: flaky-tests-report diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml index 517febe9774a99..12063c0eb7d496 100644 --- a/.github/workflows/performance.yml +++ b/.github/workflows/performance.yml @@ -86,7 +86,7 @@ jobs: - name: Archive performance results if: success() - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 with: name: performance-results path: ${{ env.WP_ARTIFACTS_PATH }}/*.performance-results*.json @@ -100,7 +100,7 @@ jobs: ./bin/log-performance-results.js $CODEHEALTH_PROJECT_TOKEN trunk $GITHUB_SHA b61dde2e5ec29d98801e623de968bfb286c5be3f $COMMITTED_AT - name: Archive debug artifacts (screenshots, HTML snapshots) - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: failure() with: name: failures-artifacts diff --git a/.github/workflows/rnmobile-android-runner.yml b/.github/workflows/rnmobile-android-runner.yml index 5620e0f66abe5e..e1c51a62ed44e7 100644 --- a/.github/workflows/rnmobile-android-runner.yml +++ b/.github/workflows/rnmobile-android-runner.yml @@ -81,13 +81,13 @@ jobs: profile: Nexus 6 script: npm run native test:e2e:android:local ${{ matrix.native-test-name }} - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: android-screen-recordings path: packages/react-native-editor/android-screen-recordings - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: appium-logs diff --git a/.github/workflows/rnmobile-ios-runner.yml b/.github/workflows/rnmobile-ios-runner.yml index fe3b9b3a3c3a55..9ead788343b8dc 100644 --- a/.github/workflows/rnmobile-ios-runner.yml +++ b/.github/workflows/rnmobile-ios-runner.yml @@ -84,13 +84,13 @@ jobs: rm packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/main.jsbundle rm -rf packages/react-native-editor/ios/build/GutenbergDemo/Build/Products/Release-iphonesimulator/GutenbergDemo.app/assets - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: ios-screen-recordings path: packages/react-native-editor/ios-screen-recordings - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + - uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 if: always() with: name: appium-logs diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index c10020be057bcb..99043c8a21d200 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -147,7 +147,7 @@ jobs: fi - name: Upload Changelog artifact - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0 with: name: changelog ${{ matrix.label }} path: ./changelog.txt From 09cf2e8b6367909ae82bffd2c80d6d78f26c495d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:52:04 -0500 Subject: [PATCH 242/325] Bump actions/download-artifact from 3 to 4 (#57110) Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build-plugin-zip.yml | 4 ++-- .github/workflows/end2end-test.yml | 2 +- .github/workflows/upload-release-to-plugin-repo.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 3ffb9f836d7217..8f649f1e15889d 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -269,12 +269,12 @@ jobs: run: echo "version=$(echo $VERSION | cut -d / -f 3 | sed 's/-rc./ RC/' )" >> $GITHUB_OUTPUT - name: Download Plugin Zip Artifact - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: gutenberg-plugin - name: Download Release Notes Artifact - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: release-notes diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index 86be3fe8231c2f..ddbf714cb50232 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -122,7 +122,7 @@ jobs: ref: trunk show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 id: download_artifact # Don't fail the job if there isn't any flaky tests report. continue-on-error: true diff --git a/.github/workflows/upload-release-to-plugin-repo.yml b/.github/workflows/upload-release-to-plugin-repo.yml index 99043c8a21d200..02ba0e8cd50ff0 100644 --- a/.github/workflows/upload-release-to-plugin-repo.yml +++ b/.github/workflows/upload-release-to-plugin-repo.yml @@ -189,7 +189,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" ./trunk/readme.txt - name: Download Changelog Artifact - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: changelog trunk path: trunk @@ -247,7 +247,7 @@ jobs: sed -i "s/$STABLE_TAG_PLACEHOLDER/Stable tag: $VERSION/g" "$VERSION/readme.txt" - name: Download Changelog Artifact - uses: actions/download-artifact@9782bd6a9848b53b110e712e20e42d89988822b7 # v3.0.1 + uses: actions/download-artifact@7a1cd3216ca9260cd8022db641d960b1db4d1be4 # v4.0.0 with: name: changelog trunk path: ${{ github.event.release.name }} From c7d19cbdb9fa26c87ce455561a449c17069c2ee5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:53:21 -0500 Subject: [PATCH 243/325] Bump tj-actions/changed-files from 40.1.1 to 40.2.2 (#56947) Bumps [tj-actions/changed-files](https://github.com/tj-actions/changed-files) from 40.1.1 to 40.2.2. - [Release notes](https://github.com/tj-actions/changed-files/releases) - [Changelog](https://github.com/tj-actions/changed-files/blob/main/HISTORY.md) - [Commits](https://github.com/tj-actions/changed-files/compare/25ef3926d147cd02fc7e931c1ef50772bbb0d25d...94549999469dbfa032becf298d95c87a14c34394) --- updated-dependencies: - dependency-name: tj-actions/changed-files dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/php-changes-detection.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/php-changes-detection.yml b/.github/workflows/php-changes-detection.yml index 7e80157d0caf7c..cd3c2664548fd4 100644 --- a/.github/workflows/php-changes-detection.yml +++ b/.github/workflows/php-changes-detection.yml @@ -17,7 +17,7 @@ jobs: - name: Get changed PHP files id: changed-files-php - uses: tj-actions/changed-files@25ef3926d147cd02fc7e931c1ef50772bbb0d25d # v40.1.1 + uses: tj-actions/changed-files@94549999469dbfa032becf298d95c87a14c34394 # v40.2.2 with: files: | *.{php} From 20aaae7361d1885ee14188b0eee59b03d84443e0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:53:50 -0500 Subject: [PATCH 244/325] Bump actions/stale from 8.0.0 to 9.0.0 (#56864) Bumps [actions/stale](https://github.com/actions/stale) from 8.0.0 to 9.0.0. - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/1160a2240286f5da8ec72b1c0816ce2481aabf84...28ca1036281a5e5922ead5184a1bbf96e5fc984e) --- updated-dependencies: - dependency-name: actions/stale dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/stale-issue-gardening.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/stale-issue-gardening.yml b/.github/workflows/stale-issue-gardening.yml index cbeb04ead53214..c73fe7a19b24b3 100644 --- a/.github/workflows/stale-issue-gardening.yml +++ b/.github/workflows/stale-issue-gardening.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Update issues - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: ${{ matrix.message }} From 27a322e012a899ae2023f75124f7a0169afe9547 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Mon, 18 Dec 2023 18:36:43 +0100 Subject: [PATCH 245/325] InnerBlocks: overlay: remove viewport size condition (#57135) --- packages/block-editor/src/components/inner-blocks/index.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/inner-blocks/index.js b/packages/block-editor/src/components/inner-blocks/index.js index cdabb348cca05e..6461d57a7e604c 100644 --- a/packages/block-editor/src/components/inner-blocks/index.js +++ b/packages/block-editor/src/components/inner-blocks/index.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { useViewportMatch, useMergeRefs } from '@wordpress/compose'; +import { useMergeRefs } from '@wordpress/compose'; import { forwardRef, useMemo } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { @@ -188,7 +188,6 @@ export function useInnerBlocksProps( props = {}, options = {} ) { layout = null, __unstableLayoutClassNames: layoutClassNames = '', } = useBlockEditContext(); - const isSmallScreen = useViewportMatch( 'medium', '<' ); const { __experimentalCaptureToolbars, hasOverlay, @@ -219,7 +218,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { const { hasBlockSupport, getBlockType } = select( blocksStore ); const blockName = getBlockName( clientId ); const enableClickThrough = - __unstableGetEditorMode() === 'navigation' || isSmallScreen; + __unstableGetEditorMode() === 'navigation'; const blockEditingMode = getBlockEditingMode( clientId ); const _parentClientId = getBlockRootClientId( clientId ); return { @@ -244,7 +243,7 @@ export function useInnerBlocksProps( props = {}, options = {} ) { __unstableIsWithinBlockOverlay( clientId ), }; }, - [ clientId, isSmallScreen ] + [ clientId ] ); const blockDropZoneRef = useBlockDropZone( { From afa89acdeba11b814122017a91658023e19a3d49 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 18 Dec 2023 18:49:25 +0100 Subject: [PATCH 246/325] Editor: Move and unify the inserter and list view states (#57158) --- .../data/data-core-edit-post.md | 18 ++-- .../data/data-core-edit-site.md | 28 +++---- .../reference-guides/data/data-core-editor.md | 50 +++++++++++ .../components/header/header-toolbar/index.js | 9 +- .../components/header/writing-menu/index.js | 6 +- .../components/keyboard-shortcuts/index.js | 8 +- .../edit-post/src/components/layout/index.js | 7 +- .../src/components/preferences-modal/index.js | 6 +- .../secondary-sidebar/inserter-sidebar.js | 10 ++- .../secondary-sidebar/list-view-sidebar.js | 4 +- packages/edit-post/src/editor.js | 6 +- .../src/hooks/commands/use-common-commands.js | 8 +- packages/edit-post/src/index.js | 2 +- packages/edit-post/src/store/actions.js | 46 +++++----- packages/edit-post/src/store/reducer.js | 40 --------- packages/edit-post/src/store/selectors.js | 51 ++++++++---- packages/edit-post/src/store/test/actions.js | 20 +---- packages/edit-post/src/store/test/reducer.js | 83 +------------------ .../edit-post/src/store/test/selectors.js | 24 ------ .../block-editor/use-site-editor-settings.js | 5 +- .../editor-canvas-container/index.js | 5 +- .../edit-site/src/components/editor/index.js | 13 ++- .../header-edit-mode/document-tools/index.js | 11 ++- .../header-edit-mode/more-menu/index.js | 6 +- .../keyboard-shortcuts/edit-mode.js | 7 +- .../src/components/preferences-modal/index.js | 6 +- .../secondary-sidebar/inserter-sidebar.js | 7 +- .../secondary-sidebar/list-view-sidebar.js | 4 +- .../global-styles-sidebar.js | 3 +- .../index.js | 5 +- .../hooks/commands/use-edit-mode-commands.js | 3 +- packages/edit-site/src/store/actions.js | 66 +++++++-------- .../edit-site/src/store/private-actions.js | 6 +- packages/edit-site/src/store/reducer.js | 44 ---------- packages/edit-site/src/store/selectors.js | 70 +++++++--------- packages/edit-site/src/store/test/actions.js | 34 +------- packages/edit-site/src/store/test/reducer.js | 83 +------------------ .../edit-site/src/store/test/selectors.js | 24 ------ .../provider/use-block-editor-settings.js | 5 +- packages/editor/src/store/actions.js | 32 +++++++ packages/editor/src/store/index.js | 2 + .../editor/src/store/private-selectors.js | 47 +++++++++++ packages/editor/src/store/reducer.js | 40 +++++++++ packages/editor/src/store/selectors.js | 22 +++++ packages/editor/src/store/test/reducer.js | 77 +++++++++++++++++ packages/editor/src/store/test/selectors.js | 24 ++++++ 46 files changed, 517 insertions(+), 560 deletions(-) create mode 100644 packages/editor/src/store/private-selectors.js diff --git a/docs/reference-guides/data/data-core-edit-post.md b/docs/reference-guides/data/data-core-edit-post.md index 24b69c7853750d..1716482d5178c0 100644 --- a/docs/reference-guides/data/data-core-edit-post.md +++ b/docs/reference-guides/data/data-core-edit-post.md @@ -214,6 +214,8 @@ _Returns_ ### isInserterOpened +> **Deprecated** + Returns true if the inserter is opened. _Parameters_ @@ -446,30 +448,24 @@ Returns an action object used to switch to template editing. ### setIsInserterOpened +> **Deprecated** + Returns an action object used to open/close the inserter. _Parameters_ -- _value_ `boolean|Object`: Whether the inserter should be opened (true) or closed (false). To specify an insertion point, use an object. -- _value.rootClientId_ `string`: The root client ID to insert at. -- _value.insertionIndex_ `number`: The index to insert at. - -_Returns_ - -- `Object`: Action object. +- _value_ `boolean|Object`: Whether the inserter should be opened (true) or closed (false). ### setIsListViewOpened +> **Deprecated** + Returns an action object used to open/close the list view. _Parameters_ - _isOpen_ `boolean`: A boolean representing whether the list view should be opened or closed. -_Returns_ - -- `Object`: Action object. - ### showBlockTypes Update the provided block types to be visible. diff --git a/docs/reference-guides/data/data-core-edit-site.md b/docs/reference-guides/data/data-core-edit-site.md index 7a0d67f9db0be0..636ccab4f3c6da 100644 --- a/docs/reference-guides/data/data-core-edit-site.md +++ b/docs/reference-guides/data/data-core-edit-site.md @@ -157,7 +157,9 @@ _Returns_ ### isInserterOpened -Returns the current opened/closed state of the inserter panel. +> **Deprecated** + +Returns true if the inserter is opened. _Parameters_ @@ -165,11 +167,11 @@ _Parameters_ _Returns_ -- `boolean`: True if the inserter panel should be open; false if closed. +- `boolean`: Whether the inserter is opened. ### isListViewOpened -Returns the current opened/closed state of the list view panel. +Returns true if the list view is opened. _Parameters_ @@ -177,7 +179,7 @@ _Parameters_ _Returns_ -- `boolean`: True if the list view panel should be open; false if closed. +- `boolean`: Whether the list view is opened. ### isNavigationOpened @@ -307,25 +309,23 @@ _Parameters_ ### setIsInserterOpened -Opens or closes the inserter. - -_Parameters_ +> **Deprecated** -- _value_ `boolean|Object`: Whether the inserter should be opened (true) or closed (false). To specify an insertion point, use an object. -- _value.rootClientId_ `string`: The root client ID to insert at. -- _value.insertionIndex_ `number`: The index to insert at. +Returns an action object used to open/close the inserter. -_Returns_ +_Parameters_ -- `Object`: Action object. +- _value_ `boolean|Object`: Whether the inserter should be opened (true) or closed (false). ### setIsListViewOpened -Sets whether the list view panel should be open. +> **Deprecated** + +Returns an action object used to open/close the list view. _Parameters_ -- _isOpen_ `boolean`: If true, opens the list view. If false, closes it. It does not toggle the state, but sets it directly. +- _isOpen_ `boolean`: A boolean representing whether the list view should be opened or closed. ### setIsNavigationPanelOpened diff --git a/docs/reference-guides/data/data-core-editor.md b/docs/reference-guides/data/data-core-editor.md index 266fce765fd6da..be2276bbf03641 100644 --- a/docs/reference-guides/data/data-core-editor.md +++ b/docs/reference-guides/data/data-core-editor.md @@ -924,6 +924,30 @@ _Related_ - isFirstMultiSelectedBlock in core/block-editor store. +### isInserterOpened + +Returns true if the inserter is opened. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether the inserter is opened. + +### isListViewOpened + +Returns true if the list view is opened. + +_Parameters_ + +- _state_ `Object`: Global application state. + +_Returns_ + +- `boolean`: Whether the list view is opened. + ### isMultiSelecting _Related_ @@ -1349,6 +1373,32 @@ _Returns_ - `Object`: Action object. +### setIsInserterOpened + +Returns an action object used to open/close the inserter. + +_Parameters_ + +- _value_ `boolean|Object`: Whether the inserter should be opened (true) or closed (false). To specify an insertion point, use an object. +- _value.rootClientId_ `string`: The root client ID to insert at. +- _value.insertionIndex_ `number`: The index to insert at. + +_Returns_ + +- `Object`: Action object. + +### setIsListViewOpened + +Returns an action object used to open/close the list view. + +_Parameters_ + +- _isOpen_ `boolean`: A boolean representing whether the list view should be opened or closed. + +_Returns_ + +- `Object`: Action object. + ### setRenderingMode Returns an action used to set the rendering mode of the post editor. We support multiple rendering modes: diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index 9c007da8e115f0..d585102c06db92 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -35,7 +35,7 @@ const preventDefault = ( event ) => { function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { const inserterButton = useRef(); const { setIsInserterOpened, setIsListViewOpened } = - useDispatch( editPostStore ); + useDispatch( editorStore ); const { isInserterEnabled, isInserterOpened, @@ -46,9 +46,8 @@ function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { } = useSelect( ( select ) => { const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } = select( blockEditorStore ); - const { getEditorSettings } = select( editorStore ); - const { getEditorMode, isFeatureActive, isListViewOpened } = - select( editPostStore ); + const { getEditorSettings, isListViewOpened } = select( editorStore ); + const { getEditorMode, isFeatureActive } = select( editPostStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); return { @@ -59,7 +58,7 @@ function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { hasInserterItems( getBlockRootClientId( getBlockSelectionEnd() ) ), - isInserterOpened: select( editPostStore ).isInserterOpened(), + isInserterOpened: select( editorStore ).isInserterOpened(), isTextModeEnabled: getEditorMode() === 'text', showIconLabels: isFeatureActive( 'showIconLabels' ), isListViewOpen: isListViewOpened(), diff --git a/packages/edit-post/src/components/header/writing-menu/index.js b/packages/edit-post/src/components/header/writing-menu/index.js index 26cc6bc5871650..11d07e52ec590d 100644 --- a/packages/edit-post/src/components/header/writing-menu/index.js +++ b/packages/edit-post/src/components/header/writing-menu/index.js @@ -10,6 +10,7 @@ import { PreferenceToggleMenuItem, store as preferencesStore, } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -19,9 +20,10 @@ import { store as postEditorStore } from '../../../store'; function WritingMenu() { const registry = useRegistry(); - const { setIsInserterOpened, setIsListViewOpened, closeGeneralSidebar } = - useDispatch( postEditorStore ); + const { closeGeneralSidebar } = useDispatch( postEditorStore ); const { set: setPreference } = useDispatch( preferencesStore ); + const { setIsInserterOpened, setIsListViewOpened } = + useDispatch( editorStore ); const toggleDistractionFree = () => { registry.batch( () => { diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index 48f9b185cd9435..c808d65ae5b165 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -18,24 +18,22 @@ import { createBlock } from '@wordpress/blocks'; import { store as editPostStore } from '../../store'; function KeyboardShortcuts() { - const { getEditorMode, isEditorSidebarOpened, isListViewOpened } = - useSelect( editPostStore ); + const { getEditorMode, isEditorSidebarOpened } = useSelect( editPostStore ); const isModeToggleDisabled = useSelect( ( select ) => { const { richEditingEnabled, codeEditingEnabled } = select( editorStore ).getEditorSettings(); return ! richEditingEnabled || ! codeEditingEnabled; }, [] ); - + const { isListViewOpened } = useSelect( editorStore ); const { switchEditorMode, openGeneralSidebar, closeGeneralSidebar, toggleFeature, - setIsListViewOpened, toggleDistractionFree, } = useDispatch( editPostStore ); const { registerShortcut } = useDispatch( keyboardShortcutsStore ); - + const { setIsListViewOpened } = useDispatch( editorStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); const { getBlockName, diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 6d51b2cf175c3b..3895c2566b1948 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -142,9 +142,10 @@ function Layout() { const isWideViewport = useViewportMatch( 'large' ); const isLargeViewport = useViewportMatch( 'medium' ); - const { openGeneralSidebar, closeGeneralSidebar, setIsInserterOpened } = + const { openGeneralSidebar, closeGeneralSidebar } = useDispatch( editPostStore ); const { createErrorNotice } = useDispatch( noticesStore ); + const { setIsInserterOpened } = useDispatch( editorStore ); const { mode, isFullscreenActive, @@ -176,8 +177,8 @@ function Layout() { ), isFullscreenActive: select( editPostStore ).isFeatureActive( 'fullscreenMode' ), - isInserterOpened: select( editPostStore ).isInserterOpened(), - isListViewOpened: select( editPostStore ).isListViewOpened(), + isInserterOpened: select( editorStore ).isInserterOpened(), + isListViewOpened: select( editorStore ).isListViewOpened(), mode: select( editPostStore ).getEditorMode(), isRichEditingEnabled: editorSettings.richEditingEnabled, hasActiveMetaboxes: select( editPostStore ).hasMetaBoxes(), diff --git a/packages/edit-post/src/components/preferences-modal/index.js b/packages/edit-post/src/components/preferences-modal/index.js index 2e46572efec0a1..20fecacbff655c 100644 --- a/packages/edit-post/src/components/preferences-modal/index.js +++ b/packages/edit-post/src/components/preferences-modal/index.js @@ -64,9 +64,9 @@ export default function EditPostPreferencesModal() { [ isLargeViewport ] ); - const { closeGeneralSidebar, setIsListViewOpened, setIsInserterOpened } = - useDispatch( editPostStore ); - + const { closeGeneralSidebar } = useDispatch( editPostStore ); + const { setIsListViewOpened, setIsInserterOpened } = + useDispatch( editorStore ); const { set: setPreference } = useDispatch( preferencesStore ); const toggleDistractionFree = () => { diff --git a/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js index e87488ffa6e656..6e7ccfec7773e7 100644 --- a/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/inserter-sidebar.js @@ -11,22 +11,24 @@ import { } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ import { store as editPostStore } from '../../store'; +import { unlock } from '../../lock-unlock'; export default function InserterSidebar() { const { insertionPoint, showMostUsedBlocks } = useSelect( ( select ) => { - const { isFeatureActive, __experimentalGetInsertionPoint } = - select( editPostStore ); + const { isFeatureActive } = select( editPostStore ); + const { getInsertionPoint } = unlock( select( editorStore ) ); return { - insertionPoint: __experimentalGetInsertionPoint(), + insertionPoint: getInsertionPoint(), showMostUsedBlocks: isFeatureActive( 'mostUsedBlocks' ), }; }, [] ); - const { setIsInserterOpened } = useDispatch( editPostStore ); + const { setIsInserterOpened } = useDispatch( editorStore ); const isMobileViewport = useViewportMatch( 'medium', '<' ); const TagName = ! isMobileViewport ? VisuallyHidden : 'div'; diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index 0d96606e9e0434..fa692d48690046 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -11,15 +11,15 @@ import { __, _x } from '@wordpress/i18n'; import { closeSmall } from '@wordpress/icons'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { ESCAPE } from '@wordpress/keycodes'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editPostStore } from '../../store'; import ListViewOutline from './list-view-outline'; export default function ListViewSidebar( { listViewToggleElement } ) { - const { setIsListViewOpened } = useDispatch( editPostStore ); + const { setIsListViewOpened } = useDispatch( editorStore ); // This hook handles focus when the sidebar first renders. const focusOnMountRef = useFocusOnMount( 'firstElement' ); diff --git a/packages/edit-post/src/editor.js b/packages/edit-post/src/editor.js index 5dbc28ea85947b..475b2aac9478bf 100644 --- a/packages/edit-post/src/editor.js +++ b/packages/edit-post/src/editor.js @@ -96,8 +96,7 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { [ postType, postId, isLargeViewport ] ); - const { updatePreferredStyleVariations, setIsInserterOpened } = - useDispatch( editPostStore ); + const { updatePreferredStyleVariations } = useDispatch( editPostStore ); const editorSettings = useMemo( () => { const result = { @@ -112,8 +111,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hasInlineToolbar, allowRightClickOverrides, - // This is marked as experimental to give time for the quick inserter to mature. - __experimentalSetIsInserterOpened: setIsInserterOpened, keepCaretInsideBlock, // Keep a reference of the `allowedBlockTypes` from the server to handle use cases // where we need to differentiate if a block is disabled by the user or some plugin. @@ -146,7 +143,6 @@ function Editor( { postId, postType, settings, initialEdits, ...props } ) { hiddenBlockTypes, blockTypes, preferredStyleVariations, - setIsInserterOpened, updatePreferredStyleVariations, keepCaretInsideBlock, ] ); diff --git a/packages/edit-post/src/hooks/commands/use-common-commands.js b/packages/edit-post/src/hooks/commands/use-common-commands.js index e9534b190416a3..0f6959d3b813be 100644 --- a/packages/edit-post/src/hooks/commands/use-common-commands.js +++ b/packages/edit-post/src/hooks/commands/use-common-commands.js @@ -32,7 +32,6 @@ export default function useCommonCommands() { openGeneralSidebar, closeGeneralSidebar, switchEditorMode, - setIsListViewOpened, toggleDistractionFree, } = useDispatch( editPostStore ); const { openModal } = useDispatch( interfaceStore ); @@ -44,8 +43,8 @@ export default function useCommonCommands() { showBlockBreadcrumbs, isDistractionFree, } = useSelect( ( select ) => { - const { getEditorMode, isListViewOpened, isFeatureActive } = - select( editPostStore ); + const { getEditorMode, isFeatureActive } = select( editPostStore ); + const { isListViewOpened } = select( editorStore ); return { activeSidebar: select( interfaceStore ).getActiveComplementaryArea( editPostStore.name @@ -63,7 +62,8 @@ export default function useCommonCommands() { }, [] ); const { toggle } = useDispatch( preferencesStore ); const { createInfoNotice } = useDispatch( noticesStore ); - const { __unstableSaveForPreview } = useDispatch( editorStore ); + const { __unstableSaveForPreview, setIsListViewOpened } = + useDispatch( editorStore ); const { getCurrentPostId } = useSelect( editorStore ); useCommand( { diff --git a/packages/edit-post/src/index.js b/packages/edit-post/src/index.js index ffe55e50efab08..38848f95efa8e7 100644 --- a/packages/edit-post/src/index.js +++ b/packages/edit-post/src/index.js @@ -79,7 +79,7 @@ export function initializeEditor( select( editPostStore ).isFeatureActive( 'showListViewByDefault' ) && ! select( editPostStore ).isFeatureActive( 'distractionFree' ) ) { - dispatch( editPostStore ).setIsListViewOpened( true ); + dispatch( editorStore ).setIsListViewOpened( true ); } registerCoreBlocks(); diff --git a/packages/edit-post/src/store/actions.js b/packages/edit-post/src/store/actions.js index 89141397f23ee9..8bf784aad16d5d 100644 --- a/packages/edit-post/src/store/actions.js +++ b/packages/edit-post/src/store/actions.js @@ -438,8 +438,6 @@ export function metaBoxUpdatesFailure() { * @deprecated * * @param {string} deviceType - * - * @return {Object} Action object. */ export const __experimentalSetPreviewDeviceType = ( deviceType ) => @@ -458,41 +456,35 @@ export const __experimentalSetPreviewDeviceType = /** * Returns an action object used to open/close the inserter. * - * @param {boolean|Object} value Whether the inserter should be - * opened (true) or closed (false). - * To specify an insertion point, - * use an object. - * @param {string} value.rootClientId The root client ID to insert at. - * @param {number} value.insertionIndex The index to insert at. + * @deprecated * - * @return {Object} Action object. + * @param {boolean|Object} value Whether the inserter should be opened (true) or closed (false). */ -export function setIsInserterOpened( value ) { - return { - type: 'SET_IS_INSERTER_OPENED', - value, +export const setIsInserterOpened = + ( value ) => + ( { registry } ) => { + deprecated( "dispatch( 'core/edit-post' ).setIsInserterOpened", { + since: '6.5', + alternative: "dispatch( 'core/editor').setIsInserterOpened", + } ); + registry.dispatch( editorStore ).setIsInserterOpened( value ); }; -} /** * Returns an action object used to open/close the list view. * + * @deprecated + * * @param {boolean} isOpen A boolean representing whether the list view should be opened or closed. - * @return {Object} Action object. */ export const setIsListViewOpened = ( isOpen ) => - ( { dispatch, registry } ) => { - const isDistractionFree = registry - .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ); - if ( isDistractionFree && isOpen ) { - dispatch.toggleDistractionFree(); - } - dispatch( { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen, + ( { registry } ) => { + deprecated( "dispatch( 'core/edit-post' ).setIsListViewOpened", { + since: '6.5', + alternative: "dispatch( 'core/editor').setIsListViewOpened", } ); + registry.dispatch( editorStore ).setIsListViewOpened( isOpen ); }; /** @@ -594,8 +586,8 @@ export const toggleDistractionFree = registry .dispatch( preferencesStore ) .set( 'core/edit-post', 'fixedToolbar', true ); - dispatch.setIsInserterOpened( false ); - dispatch.setIsListViewOpened( false ); + registry.dispatch( editorStore ).setIsInserterOpened( false ); + registry.dispatch( editorStore ).setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); } ); } diff --git a/packages/edit-post/src/store/reducer.js b/packages/edit-post/src/store/reducer.js index 151c9951cc5e2e..d3cf1d40492053 100644 --- a/packages/edit-post/src/store/reducer.js +++ b/packages/edit-post/src/store/reducer.js @@ -79,44 +79,6 @@ export function metaBoxLocations( state = {}, action ) { return state; } -/** - * Reducer to set the block inserter panel open or closed. - * - * Note: this reducer interacts with the list view panel reducer - * to make sure that only one of the two panels is open at the same time. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - */ -export function blockInserterPanel( state = false, action ) { - switch ( action.type ) { - case 'SET_IS_LIST_VIEW_OPENED': - return action.isOpen ? false : state; - case 'SET_IS_INSERTER_OPENED': - return action.value; - } - return state; -} - -/** - * Reducer to set the list view panel open or closed. - * - * Note: this reducer interacts with the inserter panel reducer - * to make sure that only one of the two panels is open at the same time. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - */ -export function listViewPanel( state = false, action ) { - switch ( action.type ) { - case 'SET_IS_INSERTER_OPENED': - return action.value ? false : state; - case 'SET_IS_LIST_VIEW_OPENED': - return action.isOpen; - } - return state; -} - /** * Reducer tracking whether meta boxes are initialized. * @@ -142,6 +104,4 @@ const metaBoxes = combineReducers( { export default combineReducers( { metaBoxes, publishSidebarActive, - blockInserterPanel, - listViewPanel, } ); diff --git a/packages/edit-post/src/store/selectors.js b/packages/edit-post/src/store/selectors.js index ca089e3db9f1aa..dff5f27c8918af 100644 --- a/packages/edit-post/src/store/selectors.js +++ b/packages/edit-post/src/store/selectors.js @@ -13,13 +13,13 @@ import { store as coreStore } from '@wordpress/core-data'; import { store as editorStore } from '@wordpress/editor'; import deprecated from '@wordpress/deprecated'; +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + const EMPTY_ARRAY = []; const EMPTY_OBJECT = {}; -const EMPTY_INSERTION_POINT = { - rootClientId: undefined, - insertionIndex: undefined, - filterValue: undefined, -}; /** * Returns the current editing mode. @@ -487,28 +487,41 @@ export const __experimentalGetPreviewDeviceType = createRegistrySelector( /** * Returns true if the inserter is opened. * + * @deprecated + * * @param {Object} state Global application state. * * @return {boolean} Whether the inserter is opened. */ -export function isInserterOpened( state ) { - return !! state.blockInserterPanel; -} +export const isInserterOpened = createRegistrySelector( ( select ) => () => { + deprecated( `select( 'core/edit-post' ).isInserterOpened`, { + since: '6.5', + alternative: `select( 'core/editor' ).isInserterOpened`, + } ); + return select( editorStore ).isInserterOpened(); +} ); /** * Get the insertion point for the inserter. * + * @deprecated + * * @param {Object} state Global application state. * * @return {Object} The root client ID, index to insert at and starting filter value. */ -export function __experimentalGetInsertionPoint( state ) { - if ( typeof state.blockInserterPanel === 'boolean' ) { - return EMPTY_INSERTION_POINT; +export const __experimentalGetInsertionPoint = createRegistrySelector( + ( select ) => () => { + deprecated( + `select( 'core/edit-post' ).__experimentalGetInsertionPoint`, + { + since: '6.5', + version: '6.7', + } + ); + return unlock( select( editorStore ) ).getInsertionPoint(); } - - return state.blockInserterPanel; -} +); /** * Returns true if the list view is opened. @@ -517,9 +530,13 @@ export function __experimentalGetInsertionPoint( state ) { * * @return {boolean} Whether the list view is opened. */ -export function isListViewOpened( state ) { - return state.listViewPanel; -} +export const isListViewOpened = createRegistrySelector( ( select ) => () => { + deprecated( `select( 'core/edit-post' ).isListViewOpened`, { + since: '6.5', + alternative: `select( 'core/editor' ).isListViewOpened`, + } ); + return select( editorStore ).isListViewOpened(); +} ); /** * Returns true if the template editing mode is enabled. diff --git a/packages/edit-post/src/store/test/actions.js b/packages/edit-post/src/store/test/actions.js index 5ec499551a09b4..82c80eb0f5273e 100644 --- a/packages/edit-post/src/store/test/actions.js +++ b/packages/edit-post/src/store/test/actions.js @@ -260,7 +260,7 @@ describe( 'actions', () => { registry .dispatch( preferencesStore ) .set( 'core/edit-post', 'fixedToolbar', true ); - registry.dispatch( editPostStore ).setIsListViewOpened( true ); + registry.dispatch( editorStore ).setIsListViewOpened( true ); registry .dispatch( editPostStore ) .openGeneralSidebar( 'edit-post/block' ); @@ -271,10 +271,10 @@ describe( 'actions', () => { .select( preferencesStore ) .get( 'core/edit-post', 'fixedToolbar' ) ).toBe( true ); - expect( registry.select( editPostStore ).isListViewOpened() ).toBe( + expect( registry.select( editorStore ).isListViewOpened() ).toBe( false ); - expect( registry.select( editPostStore ).isInserterOpened() ).toBe( + expect( registry.select( editorStore ).isInserterOpened() ).toBe( false ); expect( @@ -289,18 +289,4 @@ describe( 'actions', () => { ).toBe( true ); } ); } ); - - describe( 'setIsListViewOpened', () => { - it( 'should turn off distraction free mode when opening the list view', () => { - registry - .dispatch( preferencesStore ) - .set( 'core/edit-post', 'distractionFree', true ); - registry.dispatch( editPostStore ).setIsListViewOpened( true ); - expect( - registry - .select( preferencesStore ) - .get( 'core/edit-post', 'distractionFree' ) - ).toBe( false ); - } ); - } ); } ); diff --git a/packages/edit-post/src/store/test/reducer.js b/packages/edit-post/src/store/test/reducer.js index 3885fa6b9834be..6e4055b2998022 100644 --- a/packages/edit-post/src/store/test/reducer.js +++ b/packages/edit-post/src/store/test/reducer.js @@ -1,14 +1,7 @@ /** * Internal dependencies */ -import { - isSavingMetaBoxes, - metaBoxLocations, - blockInserterPanel, - listViewPanel, -} from '../reducer'; - -import { setIsInserterOpened } from '../actions'; +import { isSavingMetaBoxes, metaBoxLocations } from '../reducer'; describe( 'state', () => { describe( 'isSavingMetaBoxes', () => { @@ -88,78 +81,4 @@ describe( 'state', () => { } ); } ); } ); - - describe( 'blockInserterPanel()', () => { - it( 'should apply default state', () => { - expect( blockInserterPanel( undefined, {} ) ).toEqual( false ); - } ); - - it( 'should default to returning the same state', () => { - expect( blockInserterPanel( true, {} ) ).toBe( true ); - } ); - - it( 'should set the open state of the inserter panel', () => { - expect( - blockInserterPanel( false, setIsInserterOpened( true ) ) - ).toBe( true ); - expect( - blockInserterPanel( true, setIsInserterOpened( false ) ) - ).toBe( false ); - } ); - - it( 'should close the inserter when opening the list view panel', () => { - expect( - blockInserterPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: true, - } ) - ).toBe( false ); - } ); - - it( 'should not change the state when closing the list view panel', () => { - expect( - blockInserterPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: false, - } ) - ).toBe( true ); - } ); - } ); - - describe( 'listViewPanel()', () => { - it( 'should apply default state', () => { - expect( listViewPanel( undefined, {} ) ).toEqual( false ); - } ); - - it( 'should default to returning the same state', () => { - expect( listViewPanel( true, {} ) ).toBe( true ); - } ); - - it( 'should set the open state of the list view panel', () => { - expect( - listViewPanel( false, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: true, - } ) - ).toBe( true ); - expect( - listViewPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: false, - } ) - ).toBe( false ); - } ); - - it( 'should close the list view when opening the inserter panel', () => { - expect( listViewPanel( true, setIsInserterOpened( true ) ) ).toBe( - false - ); - } ); - - it( 'should not change the state when closing the inserter panel', () => { - expect( listViewPanel( true, setIsInserterOpened( false ) ) ).toBe( - true - ); - } ); - } ); } ); diff --git a/packages/edit-post/src/store/test/selectors.js b/packages/edit-post/src/store/test/selectors.js index 988574b9a53d81..5098c6f95ad221 100644 --- a/packages/edit-post/src/store/test/selectors.js +++ b/packages/edit-post/src/store/test/selectors.js @@ -6,8 +6,6 @@ import { isSavingMetaBoxes, getActiveMetaBoxLocations, isMetaBoxLocationActive, - isInserterOpened, - isListViewOpened, } from '../selectors'; describe( 'selectors', () => { @@ -107,26 +105,4 @@ describe( 'selectors', () => { expect( result ).toBe( true ); } ); } ); - - describe( 'isInserterOpened', () => { - it( 'returns the block inserter panel isOpened state', () => { - const state = { - blockInserterPanel: true, - }; - expect( isInserterOpened( state ) ).toBe( true ); - state.blockInserterPanel = false; - expect( isInserterOpened( state ) ).toBe( false ); - } ); - } ); - - describe( 'isListViewOpened', () => { - it( 'returns the list view panel isOpened state', () => { - const state = { - listViewPanel: true, - }; - expect( isListViewOpened( state ) ).toBe( true ); - state.listViewPanel = false; - expect( isListViewOpened( state ) ).toBe( false ); - } ); - } ); } ); diff --git a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js index 0c4aa8d340ee36..3cd65802b29de5 100644 --- a/packages/edit-site/src/components/block-editor/use-site-editor-settings.js +++ b/packages/edit-site/src/components/block-editor/use-site-editor-settings.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useViewportMatch } from '@wordpress/compose'; -import { useDispatch, useSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { useMemo } from '@wordpress/element'; import { store as coreStore } from '@wordpress/core-data'; import { privateApis as editorPrivateApis } from '@wordpress/editor'; @@ -89,7 +89,6 @@ function useArchiveLabel( templateSlug ) { } export function useSpecificEditorSettings() { - const { setIsInserterOpened } = useDispatch( editSiteStore ); const isLargeViewport = useViewportMatch( 'medium' ); const { templateSlug, @@ -152,7 +151,6 @@ export function useSpecificEditorSettings() { ...settings, supportsTemplateMode: true, - __experimentalSetIsInserterOpened: setIsInserterOpened, focusMode: canvasMode === 'view' && focusMode ? false : focusMode, allowRightClickOverrides, isDistractionFree, @@ -166,7 +164,6 @@ export function useSpecificEditorSettings() { }; }, [ settings, - setIsInserterOpened, focusMode, allowRightClickOverrides, isDistractionFree, diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js index c036374e7907e1..89bee6012722f6 100644 --- a/packages/edit-site/src/components/editor-canvas-container/index.js +++ b/packages/edit-site/src/components/editor-canvas-container/index.js @@ -13,6 +13,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { closeSmall } from '@wordpress/icons'; import { useFocusOnMount, useFocusReturn } from '@wordpress/compose'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -76,6 +77,8 @@ function EditorCanvasContainer( { const { setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) ); + const { setIsListViewOpened } = useDispatch( editorStore ); + const focusOnMountRef = useFocusOnMount( 'firstElement' ); const sectionFocusReturnRef = useFocusReturn(); const title = useMemo( @@ -83,8 +86,6 @@ function EditorCanvasContainer( { [ editorCanvasContainerView ] ); - const { setIsListViewOpened } = useDispatch( editSiteStore ); - function onCloseContainer() { if ( typeof onClose === 'function' ) { onClose(); diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 295f4ec3cf5c60..db92c36d75af74 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -108,17 +108,14 @@ export default function Editor( { listViewToggleElement, isLoading } ) { showIconLabels, showBlockBreadcrumbs, } = useSelect( ( select ) => { - const { - getEditedPostContext, - getEditorMode, - getCanvasMode, - isInserterOpened, - isListViewOpened, - } = unlock( select( editSiteStore ) ); + const { getEditedPostContext, getEditorMode, getCanvasMode } = unlock( + select( editSiteStore ) + ); const { __unstableGetEditorMode } = select( blockEditorStore ); const { getActiveComplementaryArea } = select( interfaceStore ); const { getEntityRecord } = select( coreDataStore ); - const { getRenderingMode } = select( editorStore ); + const { getRenderingMode, isInserterOpened, isListViewOpened } = + select( editorStore ); const _context = getEditedPostContext(); // The currently selected entity to display. diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index 53d7de6fba18fa..b67f842128e26d 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -40,11 +40,12 @@ export default function DocumentTools( { const inserterButton = useRef(); const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } = useSelect( ( select ) => { - const { isInserterOpened, isListViewOpened, getEditorMode } = - select( editSiteStore ); + const { getEditorMode } = select( editSiteStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + const { isInserterOpened, isListViewOpened } = + select( editorStore ); return { isInserterOpen: isInserterOpened(), @@ -55,11 +56,9 @@ export default function DocumentTools( { isVisualMode: getEditorMode() === 'visual', }; }, [] ); - - const { setIsInserterOpened, setIsListViewOpened } = - useDispatch( editSiteStore ); const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); - const { setDeviceType } = useDispatch( editorStore ); + const { setDeviceType, setIsInserterOpened, setIsListViewOpened } = + useDispatch( editorStore ); const isLargeViewport = useViewportMatch( 'medium' ); diff --git a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js index f6c47c1eb93bd9..4d892461a48043 100644 --- a/packages/edit-site/src/components/header-edit-mode/more-menu/index.js +++ b/packages/edit-site/src/components/header-edit-mode/more-menu/index.js @@ -15,6 +15,7 @@ import { PreferenceToggleMenuItem, store as preferencesStore, } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -37,8 +38,9 @@ import { store as siteEditorStore } from '../../../store'; export default function MoreMenu( { showIconLabels } ) { const registry = useRegistry(); - const { setIsInserterOpened, setIsListViewOpened, closeGeneralSidebar } = - useDispatch( siteEditorStore ); + const { closeGeneralSidebar } = useDispatch( siteEditorStore ); + const { setIsInserterOpened, setIsListViewOpened } = + useDispatch( editorStore ); const { openModal } = useDispatch( interfaceStore ); const { set: setPreference } = useDispatch( preferencesStore ); diff --git a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js index 1346041b6a94c1..8bc6497967902e 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js @@ -6,6 +6,7 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as interfaceStore } from '@wordpress/interface'; +import { store as editorStore } from '@wordpress/editor'; import { createBlock } from '@wordpress/blocks'; /** @@ -18,7 +19,7 @@ import { STORE_NAME } from '../../store/constants'; function KeyboardShortcutsEditMode() { const { getEditorMode } = useSelect( editSiteStore ); const isListViewOpen = useSelect( - ( select ) => select( editSiteStore ).isListViewOpened(), + ( select ) => select( editorStore ).isListViewOpened(), [] ); const isBlockInspectorOpen = useSelect( @@ -29,11 +30,11 @@ function KeyboardShortcutsEditMode() { [] ); const { redo, undo } = useDispatch( coreStore ); - const { setIsListViewOpened, switchEditorMode, toggleDistractionFree } = + const { switchEditorMode, toggleDistractionFree } = useDispatch( editSiteStore ); const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); - + const { setIsListViewOpened } = useDispatch( editorStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = useSelect( blockEditorStore ); diff --git a/packages/edit-site/src/components/preferences-modal/index.js b/packages/edit-site/src/components/preferences-modal/index.js index 87341f8bf287b0..64eb06b6530ca0 100644 --- a/packages/edit-site/src/components/preferences-modal/index.js +++ b/packages/edit-site/src/components/preferences-modal/index.js @@ -11,6 +11,7 @@ import { useMemo } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { useSelect, useDispatch, useRegistry } from '@wordpress/data'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -28,8 +29,9 @@ export default function EditSitePreferencesModal() { const toggleModal = () => isModalActive ? closeModal() : openModal( PREFERENCES_MODAL_NAME ); const registry = useRegistry(); - const { closeGeneralSidebar, setIsListViewOpened, setIsInserterOpened } = - useDispatch( editSiteStore ); + const { closeGeneralSidebar } = useDispatch( editSiteStore ); + const { setIsListViewOpened, setIsInserterOpened } = + useDispatch( editorStore ); const { set: setPreference } = useDispatch( preferencesStore ); const toggleDistractionFree = () => { diff --git a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js index 5567bcc3821195..9924a1471ae2f1 100644 --- a/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/inserter-sidebar.js @@ -11,16 +11,17 @@ import { } from '@wordpress/compose'; import { __ } from '@wordpress/i18n'; import { useEffect, useRef } from '@wordpress/element'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../store'; +import { unlock } from '../../lock-unlock'; export default function InserterSidebar() { - const { setIsInserterOpened } = useDispatch( editSiteStore ); + const { setIsInserterOpened } = useDispatch( editorStore ); const insertionPoint = useSelect( - ( select ) => select( editSiteStore ).__experimentalGetInsertionPoint(), + ( select ) => unlock( select( editorStore ) ).getInsertionPoint(), [] ); diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index f86b7b8c9784d0..3ec7814beebe32 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -11,17 +11,17 @@ import { closeSmall } from '@wordpress/icons'; import { ESCAPE } from '@wordpress/keycodes'; import { focus } from '@wordpress/dom'; import { useShortcut } from '@wordpress/keyboard-shortcuts'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies */ -import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; const { PrivateListView } = unlock( blockEditorPrivateApis ); export default function ListViewSidebar( { listViewToggleElement } ) { - const { setIsListViewOpened } = useDispatch( editSiteStore ); + const { setIsListViewOpened } = useDispatch( editorStore ); // This hook handles focus when the sidebar first renders. const focusOnMountRef = useFocusOnMount( 'firstElement' ); diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js index 6edb12e9ab053d..ebb2e8a2a27718 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js @@ -14,6 +14,7 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect } from '@wordpress/element'; import { store as interfaceStore } from '@wordpress/interface'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -77,7 +78,7 @@ export default function GlobalStylesSidebar() { } }, [ shouldClearCanvasContainerView ] ); - const { setIsListViewOpened } = useDispatch( editSiteStore ); + const { setIsListViewOpened } = useDispatch( editorStore ); const { goTo } = useNavigator(); const loadRevisions = () => { setIsListViewOpened( false ); diff --git a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js index 45fa8102456a54..9840098e338bd2 100644 --- a/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js +++ b/packages/edit-site/src/components/sidebar-navigation-screen-global-styles/index.js @@ -9,6 +9,7 @@ import { __experimentalNavigatorButton as NavigatorButton } from '@wordpress/com import { useViewportMatch } from '@wordpress/compose'; import { BlockEditorProvider } from '@wordpress/block-editor'; import { useCallback } from '@wordpress/element'; +import { store as editorStore } from '@wordpress/editor'; /** * Internal dependencies @@ -86,8 +87,8 @@ function SidebarNavigationScreenGlobalStylesContent() { export default function SidebarNavigationScreenGlobalStyles() { const { revisions, isLoading: isLoadingRevisions } = useGlobalStylesRevisions(); - const { openGeneralSidebar, setIsListViewOpened } = - useDispatch( editSiteStore ); + const { openGeneralSidebar } = useDispatch( editSiteStore ); + const { setIsListViewOpened } = useDispatch( editorStore ); const isMobileViewport = useViewportMatch( 'medium', '<' ); const { setCanvasMode, setEditorCanvasContainerView } = unlock( useDispatch( editSiteStore ) diff --git a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js index ece6d349db1e7f..ffd9907160d26c 100644 --- a/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js +++ b/packages/edit-site/src/hooks/commands/use-edit-mode-commands.js @@ -217,7 +217,8 @@ function useEditUICommands() { isListViewOpen, isDistractionFree, } = useSelect( ( select ) => { - const { isListViewOpened, getEditorMode } = select( editSiteStore ); + const { getEditorMode } = select( editSiteStore ); + const { isListViewOpened } = select( editorStore ); return { canvasMode: unlock( select( editSiteStore ) ).getCanvasMode(), editorMode: getEditorMode(), diff --git a/packages/edit-site/src/store/actions.js b/packages/edit-site/src/store/actions.js index 6397a31af120b3..820c6f5239cce4 100644 --- a/packages/edit-site/src/store/actions.js +++ b/packages/edit-site/src/store/actions.js @@ -321,23 +321,38 @@ export function setIsNavigationPanelOpened() { } /** - * Opens or closes the inserter. + * Returns an action object used to open/close the inserter. * - * @param {boolean|Object} value Whether the inserter should be - * opened (true) or closed (false). - * To specify an insertion point, - * use an object. - * @param {string} value.rootClientId The root client ID to insert at. - * @param {number} value.insertionIndex The index to insert at. + * @deprecated * - * @return {Object} Action object. + * @param {boolean|Object} value Whether the inserter should be opened (true) or closed (false). */ -export function setIsInserterOpened( value ) { - return { - type: 'SET_IS_INSERTER_OPENED', - value, +export const setIsInserterOpened = + ( value ) => + ( { registry } ) => { + deprecated( "dispatch( 'core/edit-site' ).setIsInserterOpened", { + since: '6.5', + alternative: "dispatch( 'core/editor').setIsInserterOpened", + } ); + registry.dispatch( editorStore ).setIsInserterOpened( value ); + }; + +/** + * Returns an action object used to open/close the list view. + * + * @deprecated + * + * @param {boolean} isOpen A boolean representing whether the list view should be opened or closed. + */ +export const setIsListViewOpened = + ( isOpen ) => + ( { registry } ) => { + deprecated( "dispatch( 'core/edit-site' ).setIsListViewOpened", { + since: '6.5', + alternative: "dispatch( 'core/editor').setIsListViewOpened", + } ); + registry.dispatch( editorStore ).setIsListViewOpened( isOpen ); }; -} /** * Returns an action object used to update the settings. @@ -353,27 +368,6 @@ export function updateSettings( settings ) { }; } -/** - * Sets whether the list view panel should be open. - * - * @param {boolean} isOpen If true, opens the list view. If false, closes it. - * It does not toggle the state, but sets it directly. - */ -export const setIsListViewOpened = - ( isOpen ) => - ( { dispatch, registry } ) => { - const isDistractionFree = registry - .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ); - if ( isDistractionFree && isOpen ) { - dispatch.toggleDistractionFree(); - } - dispatch( { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen, - } ); - }; - /** * Sets whether the save view panel should be open. * @@ -614,8 +608,8 @@ export const toggleDistractionFree = registry .dispatch( preferencesStore ) .set( 'core/edit-site', 'fixedToolbar', true ); - dispatch.setIsInserterOpened( false ); - dispatch.setIsListViewOpened( false ); + registry.dispatch( editorStore ).setIsInserterOpened( false ); + registry.dispatch( editorStore ).setIsListViewOpened( false ); dispatch.closeGeneralSidebar(); } ); } diff --git a/packages/edit-site/src/store/private-actions.js b/packages/edit-site/src/store/private-actions.js index 2d858c15208991..eb945fa748555e 100644 --- a/packages/edit-site/src/store/private-actions.js +++ b/packages/edit-site/src/store/private-actions.js @@ -3,6 +3,7 @@ */ import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as preferencesStore } from '@wordpress/preferences'; +import { store as editorStore } from '@wordpress/editor'; /** * Action that switches the canvas mode. @@ -28,8 +29,11 @@ export const setCanvasMode = .select( preferencesStore ) .get( 'core/edit-site', 'distractionFree' ) ) { - dispatch.setIsListViewOpened( true ); + registry.dispatch( editorStore ).setIsListViewOpened( true ); + } else { + registry.dispatch( editorStore ).setIsListViewOpened( false ); } + registry.dispatch( editorStore ).setIsInserterOpened( false ); }; /** diff --git a/packages/edit-site/src/store/reducer.js b/packages/edit-site/src/store/reducer.js index b55acbffd626e6..1e3d9c43f0eb34 100644 --- a/packages/edit-site/src/store/reducer.js +++ b/packages/edit-site/src/store/reducer.js @@ -50,48 +50,6 @@ export function editedPost( state = {}, action ) { return state; } -/** - * Reducer to set the block inserter panel open or closed. - * - * Note: this reducer interacts with the navigation and list view panels reducers - * to make sure that only one of the three panels is open at the same time. - * - * @param {boolean|Object} state Current state. - * @param {Object} action Dispatched action. - */ -export function blockInserterPanel( state = false, action ) { - switch ( action.type ) { - case 'SET_IS_LIST_VIEW_OPENED': - return action.isOpen ? false : state; - case 'SET_IS_INSERTER_OPENED': - return action.value; - case 'SET_CANVAS_MODE': - return false; - } - return state; -} - -/** - * Reducer to set the list view panel open or closed. - * - * Note: this reducer interacts with the navigation and inserter panels reducers - * to make sure that only one of the three panels is open at the same time. - * - * @param {Object} state Current state. - * @param {Object} action Dispatched action. - */ -export function listViewPanel( state = false, action ) { - switch ( action.type ) { - case 'SET_IS_INSERTER_OPENED': - return action.value ? false : state; - case 'SET_IS_LIST_VIEW_OPENED': - return action.isOpen; - case 'SET_CANVAS_MODE': - return false; - } - return state; -} - /** * Reducer to set the save view panel open or closed. * @@ -143,8 +101,6 @@ function editorCanvasContainerView( state = undefined, action ) { export default combineReducers( { settings, editedPost, - blockInserterPanel, - listViewPanel, saveViewPanel, canvasMode, editorCanvasContainerView, diff --git a/packages/edit-site/src/store/selectors.js b/packages/edit-site/src/store/selectors.js index ebaee12dfdc5e4..4d7adaaa848fe5 100644 --- a/packages/edit-site/src/store/selectors.js +++ b/packages/edit-site/src/store/selectors.js @@ -14,6 +14,8 @@ import { store as editorStore } from '@wordpress/editor'; */ import { getFilteredTemplatePartBlocks } from './utils'; import { TEMPLATE_PART_POST_TYPE } from '../utils/constants'; +import { unlock } from '../lock-unlock'; + /** * @typedef {'template'|'template_type'} TemplateType Template type. */ @@ -169,66 +171,58 @@ export function getPage( state ) { } /** - * Returns the current opened/closed state of the inserter panel. + * Returns true if the inserter is opened. + * + * @deprecated * * @param {Object} state Global application state. * - * @return {boolean} True if the inserter panel should be open; false if closed. + * @return {boolean} Whether the inserter is opened. */ -export function isInserterOpened( state ) { - return !! state.blockInserterPanel; -} +export const isInserterOpened = createRegistrySelector( ( select ) => () => { + deprecated( `select( 'core/edit-site' ).isInserterOpened`, { + since: '6.5', + alternative: `select( 'core/editor' ).isInserterOpened`, + } ); + return select( editorStore ).isInserterOpened(); +} ); /** * Get the insertion point for the inserter. * + * @deprecated + * * @param {Object} state Global application state. * * @return {Object} The root client ID, index to insert at and starting filter value. */ export const __experimentalGetInsertionPoint = createRegistrySelector( - ( select ) => ( state ) => { - if ( typeof state.blockInserterPanel === 'object' ) { - const { rootClientId, insertionIndex, filterValue } = - state.blockInserterPanel; - return { rootClientId, insertionIndex, filterValue }; - } - - if ( - isPage( state ) && - select( editorStore ).getRenderingMode() !== 'template-only' - ) { - const [ postContentClientId ] = - select( blockEditorStore ).__experimentalGetGlobalBlocksByName( - 'core/post-content' - ); - if ( postContentClientId ) { - return { - rootClientId: postContentClientId, - insertionIndex: undefined, - filterValue: undefined, - }; + ( select ) => () => { + deprecated( + `select( 'core/edit-site' ).__experimentalGetInsertionPoint`, + { + since: '6.5', + version: '6.7', } - } - - return { - rootClientId: undefined, - insertionIndex: undefined, - filterValue: undefined, - }; + ); + return unlock( select( editorStore ) ).getInsertionPoint(); } ); /** - * Returns the current opened/closed state of the list view panel. + * Returns true if the list view is opened. * * @param {Object} state Global application state. * - * @return {boolean} True if the list view panel should be open; false if closed. + * @return {boolean} Whether the list view is opened. */ -export function isListViewOpened( state ) { - return state.listViewPanel; -} +export const isListViewOpened = createRegistrySelector( ( select ) => () => { + deprecated( `select( 'core/edit-site' ).isListViewOpened`, { + since: '6.5', + alternative: `select( 'core/editor' ).isListViewOpened`, + } ); + return select( editorStore ).isListViewOpened(); +} ); /** * Returns the current opened/closed state of the save panel. diff --git a/packages/edit-site/src/store/test/actions.js b/packages/edit-site/src/store/test/actions.js index 6f0597fec12434..a0ea9b2fe0885f 100644 --- a/packages/edit-site/src/store/test/actions.js +++ b/packages/edit-site/src/store/test/actions.js @@ -76,34 +76,6 @@ describe( 'actions', () => { } ); } ); - describe( 'setIsListViewOpened', () => { - it( 'should set the list view opened state', () => { - const registry = createRegistryWithStores(); - - registry.dispatch( editSiteStore ).setIsListViewOpened( true ); - expect( registry.select( editSiteStore ).isListViewOpened() ).toBe( - true - ); - - registry.dispatch( editSiteStore ).setIsListViewOpened( false ); - expect( registry.select( editSiteStore ).isListViewOpened() ).toBe( - false - ); - } ); - it( 'should turn off distraction free mode when opening the list view', () => { - const registry = createRegistryWithStores(); - registry - .dispatch( preferencesStore ) - .set( 'core/edit-site', 'distractionFree', true ); - registry.dispatch( editSiteStore ).setIsListViewOpened( true ); - expect( - registry - .select( preferencesStore ) - .get( 'core/edit-site', 'distractionFree' ) - ).toBe( false ); - } ); - } ); - describe( 'openGeneralSidebar', () => { it( 'should turn off distraction free mode when opening a general sidebar', () => { const registry = createRegistryWithStores(); @@ -149,7 +121,7 @@ describe( 'actions', () => { registry .dispatch( preferencesStore ) .set( 'core/edit-site', 'fixedToolbar', true ); - registry.dispatch( editSiteStore ).setIsListViewOpened( true ); + registry.dispatch( editorStore ).setIsListViewOpened( true ); registry .dispatch( editSiteStore ) .openGeneralSidebar( 'edit-site/global-styles' ); @@ -160,10 +132,10 @@ describe( 'actions', () => { .select( preferencesStore ) .get( 'core/edit-site', 'fixedToolbar' ) ).toBe( true ); - expect( registry.select( editSiteStore ).isListViewOpened() ).toBe( + expect( registry.select( editorStore ).isListViewOpened() ).toBe( false ); - expect( registry.select( editSiteStore ).isInserterOpened() ).toBe( + expect( registry.select( editorStore ).isInserterOpened() ).toBe( false ); expect( diff --git a/packages/edit-site/src/store/test/reducer.js b/packages/edit-site/src/store/test/reducer.js index f39261fea38802..9e7929e737fb39 100644 --- a/packages/edit-site/src/store/test/reducer.js +++ b/packages/edit-site/src/store/test/reducer.js @@ -6,14 +6,7 @@ import deepFreeze from 'deep-freeze'; /** * Internal dependencies */ -import { - settings, - editedPost, - blockInserterPanel, - listViewPanel, -} from '../reducer'; - -import { setIsInserterOpened } from '../actions'; +import { settings, editedPost } from '../reducer'; describe( 'state', () => { describe( 'settings()', () => { @@ -73,78 +66,4 @@ describe( 'state', () => { } ); } ); } ); - - describe( 'blockInserterPanel()', () => { - it( 'should apply default state', () => { - expect( blockInserterPanel( undefined, {} ) ).toEqual( false ); - } ); - - it( 'should default to returning the same state', () => { - expect( blockInserterPanel( true, {} ) ).toBe( true ); - } ); - - it( 'should set the open state of the inserter panel', () => { - expect( - blockInserterPanel( false, setIsInserterOpened( true ) ) - ).toBe( true ); - expect( - blockInserterPanel( true, setIsInserterOpened( false ) ) - ).toBe( false ); - } ); - - it( 'should close the inserter when opening the list view panel', () => { - expect( - blockInserterPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: true, - } ) - ).toBe( false ); - } ); - - it( 'should not change the state when closing the list view panel', () => { - expect( - blockInserterPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: false, - } ) - ).toBe( true ); - } ); - } ); - - describe( 'listViewPanel()', () => { - it( 'should apply default state', () => { - expect( listViewPanel( undefined, {} ) ).toEqual( false ); - } ); - - it( 'should default to returning the same state', () => { - expect( listViewPanel( true, {} ) ).toBe( true ); - } ); - - it( 'should set the open state of the list view panel', () => { - expect( - listViewPanel( false, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: true, - } ) - ).toBe( true ); - expect( - listViewPanel( true, { - type: 'SET_IS_LIST_VIEW_OPENED', - isOpen: false, - } ) - ).toBe( false ); - } ); - - it( 'should close the list view when opening the inserter panel', () => { - expect( listViewPanel( true, setIsInserterOpened( true ) ) ).toBe( - false - ); - } ); - - it( 'should not change the state when closing the inserter panel', () => { - expect( listViewPanel( true, setIsInserterOpened( false ) ) ).toBe( - true - ); - } ); - } ); } ); diff --git a/packages/edit-site/src/store/test/selectors.js b/packages/edit-site/src/store/test/selectors.js index 07577e897b04ec..1e25b3faefff76 100644 --- a/packages/edit-site/src/store/test/selectors.js +++ b/packages/edit-site/src/store/test/selectors.js @@ -10,8 +10,6 @@ import { getCanUserCreateMedia, getEditedPostType, getEditedPostId, - isInserterOpened, - isListViewOpened, isPage, } from '../selectors'; @@ -45,28 +43,6 @@ describe( 'selectors', () => { } ); } ); - describe( 'isInserterOpened', () => { - it( 'returns the block inserter panel isOpened state', () => { - const state = { - blockInserterPanel: true, - }; - expect( isInserterOpened( state ) ).toBe( true ); - state.blockInserterPanel = false; - expect( isInserterOpened( state ) ).toBe( false ); - } ); - } ); - - describe( 'isListViewOpened', () => { - it( 'returns the list view panel isOpened state', () => { - const state = { - listViewPanel: true, - }; - expect( isListViewOpened( state ) ).toBe( true ); - state.listViewPanel = false; - expect( isListViewOpened( state ) ).toBe( false ); - } ); - } ); - describe( 'isPage', () => { it( 'returns true if the edited post type is a page', () => { const state = { diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index 34aa472a9921d5..49f61815f663d0 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -25,7 +25,6 @@ const BLOCK_EDITOR_SETTINGS = [ '__experimentalFeatures', '__experimentalGlobalStylesBaseStyles', '__experimentalPreferredStyleVariations', - '__experimentalSetIsInserterOpened', '__unstableGalleryWithImageBlocks', 'alignWide', 'allowedBlockTypes', @@ -178,7 +177,7 @@ function useBlockEditorSettings( settings, postType, postId ) { [ settingsBlockPatternCategories, restBlockPatternCategories ] ); - const { undo } = useDispatch( editorStore ); + const { undo, setIsInserterOpened } = useDispatch( editorStore ); const { saveEntityRecord } = useDispatch( coreStore ); @@ -239,6 +238,7 @@ function useBlockEditorSettings( settings, postType, postId ) { postType === 'wp_navigation' ? [ [ 'core/navigation', {}, [] ] ] : settings.template, + __experimentalSetIsInserterOpened: setIsInserterOpened, } ), [ settings, @@ -254,6 +254,7 @@ function useBlockEditorSettings( settings, postType, postId ) { pageOnFront, pageForPosts, postType, + setIsInserterOpened, ] ); } diff --git a/packages/editor/src/store/actions.js b/packages/editor/src/store/actions.js index d35907e5ed04d3..49f49a5d06da5d 100644 --- a/packages/editor/src/store/actions.js +++ b/packages/editor/src/store/actions.js @@ -689,6 +689,38 @@ export function removeEditorPanel( panelName ) { }; } +/** + * Returns an action object used to open/close the inserter. + * + * @param {boolean|Object} value Whether the inserter should be + * opened (true) or closed (false). + * To specify an insertion point, + * use an object. + * @param {string} value.rootClientId The root client ID to insert at. + * @param {number} value.insertionIndex The index to insert at. + * + * @return {Object} Action object. + */ +export function setIsInserterOpened( value ) { + return { + type: 'SET_IS_INSERTER_OPENED', + value, + }; +} + +/** + * Returns an action object used to open/close the list view. + * + * @param {boolean} isOpen A boolean representing whether the list view should be opened or closed. + * @return {Object} Action object. + */ +export function setIsListViewOpened( isOpen ) { + return { + type: 'SET_IS_LIST_VIEW_OPENED', + isOpen, + }; +} + /** * Backward compatibility */ diff --git a/packages/editor/src/store/index.js b/packages/editor/src/store/index.js index ebd41354308e7a..392f5f42cbd55b 100644 --- a/packages/editor/src/store/index.js +++ b/packages/editor/src/store/index.js @@ -10,6 +10,7 @@ import reducer from './reducer'; import * as selectors from './selectors'; import * as actions from './actions'; import * as privateActions from './private-actions'; +import * as privateSelectors from './private-selectors'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; @@ -39,3 +40,4 @@ export const store = createReduxStore( STORE_NAME, { register( store ); unlock( store ).registerPrivateActions( privateActions ); +unlock( store ).registerPrivateSelectors( privateSelectors ); diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js new file mode 100644 index 00000000000000..0ab97dea67ce55 --- /dev/null +++ b/packages/editor/src/store/private-selectors.js @@ -0,0 +1,47 @@ +/** + * WordPress dependencies + */ +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { createRegistrySelector } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { getRenderingMode } from './selectors'; + +const EMPTY_INSERTION_POINT = { + rootClientId: undefined, + insertionIndex: undefined, + filterValue: undefined, +}; + +/** + * Get the insertion point for the inserter. + * + * @param {Object} state Global application state. + * + * @return {Object} The root client ID, index to insert at and starting filter value. + */ +export const getInsertionPoint = createRegistrySelector( + ( select ) => ( state ) => { + if ( typeof state.blockInserterPanel === 'object' ) { + return state.blockInserterPanel; + } + + if ( getRenderingMode( state ) === 'template-locked' ) { + const [ postContentClientId ] = + select( blockEditorStore ).__experimentalGetGlobalBlocksByName( + 'core/post-content' + ); + if ( postContentClientId ) { + return { + rootClientId: postContentClientId, + insertionIndex: undefined, + filterValue: undefined, + }; + } + } + + return EMPTY_INSERTION_POINT; + } +); diff --git a/packages/editor/src/store/reducer.js b/packages/editor/src/store/reducer.js index 48c52d44327c3e..a2d24789cd33f5 100644 --- a/packages/editor/src/store/reducer.js +++ b/packages/editor/src/store/reducer.js @@ -311,6 +311,44 @@ export function removedPanels( state = [], action ) { return state; } +/** + * Reducer to set the block inserter panel open or closed. + * + * Note: this reducer interacts with the list view panel reducer + * to make sure that only one of the two panels is open at the same time. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + */ +export function blockInserterPanel( state = false, action ) { + switch ( action.type ) { + case 'SET_IS_LIST_VIEW_OPENED': + return action.isOpen ? false : state; + case 'SET_IS_INSERTER_OPENED': + return action.value; + } + return state; +} + +/** + * Reducer to set the list view panel open or closed. + * + * Note: this reducer interacts with the inserter panel reducer + * to make sure that only one of the two panels is open at the same time. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + */ +export function listViewPanel( state = false, action ) { + switch ( action.type ) { + case 'SET_IS_INSERTER_OPENED': + return action.value ? false : state; + case 'SET_IS_LIST_VIEW_OPENED': + return action.isOpen; + } + return state; +} + export default combineReducers( { postId, postType, @@ -325,4 +363,6 @@ export default combineReducers( { renderingMode, deviceType, removedPanels, + blockInserterPanel, + listViewPanel, } ); diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 1c89d0b7f58009..70d726638a0940 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1279,6 +1279,28 @@ export function getDeviceType( state ) { return state.deviceType; } +/** + * Returns true if the list view is opened. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the list view is opened. + */ +export function isListViewOpened( state ) { + return state.listViewPanel; +} + +/** + * Returns true if the inserter is opened. + * + * @param {Object} state Global application state. + * + * @return {boolean} Whether the inserter is opened. + */ +export function isInserterOpened( state ) { + return !! state.blockInserterPanel; +} + /* * Backward compatibility */ diff --git a/packages/editor/src/store/test/reducer.js b/packages/editor/src/store/test/reducer.js index c1f3fd73750499..b4fd013c6b4d42 100644 --- a/packages/editor/src/store/test/reducer.js +++ b/packages/editor/src/store/test/reducer.js @@ -15,7 +15,10 @@ import { postSavingLock, postAutosavingLock, removedPanels, + blockInserterPanel, + listViewPanel, } from '../reducer'; +import { setIsInserterOpened } from '../actions'; describe( 'state', () => { describe( 'hasSameKeys()', () => { @@ -285,4 +288,78 @@ describe( 'state', () => { expect( state ).toBe( original ); } ); } ); + + describe( 'blockInserterPanel()', () => { + it( 'should apply default state', () => { + expect( blockInserterPanel( undefined, {} ) ).toEqual( false ); + } ); + + it( 'should default to returning the same state', () => { + expect( blockInserterPanel( true, {} ) ).toBe( true ); + } ); + + it( 'should set the open state of the inserter panel', () => { + expect( + blockInserterPanel( false, setIsInserterOpened( true ) ) + ).toBe( true ); + expect( + blockInserterPanel( true, setIsInserterOpened( false ) ) + ).toBe( false ); + } ); + + it( 'should close the inserter when opening the list view panel', () => { + expect( + blockInserterPanel( true, { + type: 'SET_IS_LIST_VIEW_OPENED', + isOpen: true, + } ) + ).toBe( false ); + } ); + + it( 'should not change the state when closing the list view panel', () => { + expect( + blockInserterPanel( true, { + type: 'SET_IS_LIST_VIEW_OPENED', + isOpen: false, + } ) + ).toBe( true ); + } ); + } ); + + describe( 'listViewPanel()', () => { + it( 'should apply default state', () => { + expect( listViewPanel( undefined, {} ) ).toEqual( false ); + } ); + + it( 'should default to returning the same state', () => { + expect( listViewPanel( true, {} ) ).toBe( true ); + } ); + + it( 'should set the open state of the list view panel', () => { + expect( + listViewPanel( false, { + type: 'SET_IS_LIST_VIEW_OPENED', + isOpen: true, + } ) + ).toBe( true ); + expect( + listViewPanel( true, { + type: 'SET_IS_LIST_VIEW_OPENED', + isOpen: false, + } ) + ).toBe( false ); + } ); + + it( 'should close the list view when opening the inserter panel', () => { + expect( listViewPanel( true, setIsInserterOpened( true ) ) ).toBe( + false + ); + } ); + + it( 'should not change the state when closing the inserter panel', () => { + expect( listViewPanel( true, setIsInserterOpened( false ) ) ).toBe( + true + ); + } ); + } ); } ); diff --git a/packages/editor/src/store/test/selectors.js b/packages/editor/src/store/test/selectors.js index 285d97cabb288a..1de25604ebd7e3 100644 --- a/packages/editor/src/store/test/selectors.js +++ b/packages/editor/src/store/test/selectors.js @@ -193,6 +193,8 @@ const { __experimentalGetTemplateInfo, __experimentalGetDefaultTemplatePartAreas, isEditorPanelRemoved, + isInserterOpened, + isListViewOpened, } = selectors; const defaultTemplateTypes = [ @@ -3035,4 +3037,26 @@ describe( 'selectors', () => { expect( isEditorPanelRemoved( state, 'post-status' ) ).toBe( true ); } ); } ); + + describe( 'isInserterOpened', () => { + it( 'returns the block inserter panel isOpened state', () => { + const state = { + blockInserterPanel: true, + }; + expect( isInserterOpened( state ) ).toBe( true ); + state.blockInserterPanel = false; + expect( isInserterOpened( state ) ).toBe( false ); + } ); + } ); + + describe( 'isListViewOpened', () => { + it( 'returns the list view panel isOpened state', () => { + const state = { + listViewPanel: true, + }; + expect( isListViewOpened( state ) ).toBe( true ); + state.listViewPanel = false; + expect( isListViewOpened( state ) ).toBe( false ); + } ); + } ); } ); From 4f8f85686a8621269d995745a7041db94a0de241 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Mon, 18 Dec 2023 18:50:47 +0100 Subject: [PATCH 247/325] Site Editor: Add Page Attributes panel (#57151) --- .../sidebar/settings-sidebar/index.js | 4 ++-- .../sidebar-edit-mode/page-panels/index.js | 2 ++ .../sidebar-edit-mode/template-panel/index.js | 2 ++ packages/editor/src/components/index.js | 1 + .../src/components/page-attributes/panel.js} | 18 +++++++++--------- 5 files changed, 16 insertions(+), 11 deletions(-) rename packages/{edit-post/src/components/sidebar/page-attributes/index.js => editor/src/components/page-attributes/panel.js} (81%) diff --git a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js index 8f71b3908d584d..0cd69cb11538c6 100644 --- a/packages/edit-post/src/components/sidebar/settings-sidebar/index.js +++ b/packages/edit-post/src/components/sidebar/settings-sidebar/index.js @@ -13,6 +13,7 @@ import { store as interfaceStore } from '@wordpress/interface'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editorStore, + PageAttributesPanel, PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, @@ -25,7 +26,6 @@ import { */ import SettingsHeader from '../settings-header'; import PostStatus from '../post-status'; -import PageAttributes from '../page-attributes'; import MetaBoxes from '../../meta-boxes'; import PluginDocumentSettingPanel from '../plugin-document-setting-panel'; import PluginSidebarEditPost from '../plugin-sidebar'; @@ -86,7 +86,7 @@ const SidebarContent = ( { <PostFeaturedImagePanel /> <PostExcerptPanel /> <PostDiscussionPanel /> - <PageAttributes /> + <PageAttributesPanel /> <MetaBoxes location="side" /> </> ) } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js index 87be48220ec95e..0d4dee97aad984 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/page-panels/index.js @@ -13,6 +13,7 @@ import { useSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; import { + PageAttributesPanel, PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, @@ -106,6 +107,7 @@ export default function PagePanels() { <PostFeaturedImagePanel /> <PostExcerptPanel /> <PostDiscussionPanel /> + <PageAttributesPanel /> </> ); } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 21903f0066767f..21df325ee34c20 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -4,6 +4,7 @@ import { useSelect } from '@wordpress/data'; import { PanelBody } from '@wordpress/components'; import { + PageAttributesPanel, PostDiscussionPanel, PostExcerptPanel, PostFeaturedImagePanel, @@ -70,6 +71,7 @@ export default function TemplatePanel() { <PostFeaturedImagePanel /> <PostExcerptPanel /> <PostDiscussionPanel /> + <PageAttributesPanel /> </> ); } diff --git a/packages/editor/src/components/index.js b/packages/editor/src/components/index.js index d20ba59215b9b1..33a18e6f9a6ad2 100644 --- a/packages/editor/src/components/index.js +++ b/packages/editor/src/components/index.js @@ -23,6 +23,7 @@ export { default as ErrorBoundary } from './error-boundary'; export { default as LocalAutosaveMonitor } from './local-autosave-monitor'; export { default as PageAttributesCheck } from './page-attributes/check'; export { default as PageAttributesOrder } from './page-attributes/order'; +export { default as PageAttributesPanel } from './page-attributes/panel'; export { default as PageAttributesParent } from './page-attributes/parent'; export { default as PageTemplate } from './post-template/classic-theme'; export { default as PostTemplatePanel } from './post-template/panel'; diff --git a/packages/edit-post/src/components/sidebar/page-attributes/index.js b/packages/editor/src/components/page-attributes/panel.js similarity index 81% rename from packages/edit-post/src/components/sidebar/page-attributes/index.js rename to packages/editor/src/components/page-attributes/panel.js index 7a5d6222e11fcd..63d5bbb5a87048 100644 --- a/packages/edit-post/src/components/sidebar/page-attributes/index.js +++ b/packages/editor/src/components/page-attributes/panel.js @@ -3,21 +3,21 @@ */ import { __ } from '@wordpress/i18n'; import { PanelBody, PanelRow } from '@wordpress/components'; -import { - store as editorStore, - PageAttributesCheck, - PageAttributesOrder, - PageAttributesParent, -} from '@wordpress/editor'; + import { useSelect, useDispatch } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; /** - * Module Constants + * Internal dependencies */ +import { store as editorStore } from '../../store'; +import PageAttributesCheck from './check'; +import PageAttributesOrder from './order'; +import PageAttributesParent from './parent'; + const PANEL_NAME = 'page-attributes'; -export function PageAttributes() { +export function PageAttributesPanel() { const { isEnabled, isOpened, postType } = useSelect( ( select ) => { const { getEditedPostAttribute, @@ -59,4 +59,4 @@ export function PageAttributes() { ); } -export default PageAttributes; +export default PageAttributesPanel; From bf6d1dc6b04407edb07219d26fa8bf44748c56b8 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Mon, 18 Dec 2023 20:04:23 +0100 Subject: [PATCH 248/325] Docs: Fundamentals of Block Development - block in the editor (#56488) * Add block-in-the-editor.md file * Add block editing interface details and built-in components * Update block in the Editor with React components * Update block display and behavior in the Block Editor * new file destination * new changes * Update block registration link in block-in-the-editor.md * Add Block Controls and Settings Sidebar documentation * Add additional references to block in the editor * Remove duplicated content page from how-to-guides * Update block fundamentals README with additional information * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update block editor component references * Add BlockControls link to block-in-the-editor.md --------- Co-authored-by: Nick Diego <nick.diego@automattic.com> --- docs/getting-started/fundamentals/README.md | 3 +- .../fundamentals/block-in-the-editor.md | 169 ++++++++++++++++++ .../javascript-in-the-block-editor.md | 1 + docs/manifest.json | 6 - docs/toc.json | 3 - 5 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 docs/getting-started/fundamentals/block-in-the-editor.md diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md index 26fc88981348b8..4fde0f3a0d1009 100644 --- a/docs/getting-started/fundamentals/README.md +++ b/docs/getting-started/fundamentals/README.md @@ -5,7 +5,8 @@ This section provides an introduction to the most relevant concepts in Block Dev In this section, you will learn: 1. [**File structure of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/file-structure-of-a-block) - The purpose of each one of the types of files available for a block, the relationships between them, and their role in the output of the block. -1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file. +1. [**`block.json`**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json) - How a block is defined using its `block.json` metadata and some relevant properties of this file (such as `attributes` and `supports`). 1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. 1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. +1. [**The block in the Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor) - The block as a React component loaded in the Block Editor and its possibilities. 1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/block-in-the-editor.md b/docs/getting-started/fundamentals/block-in-the-editor.md new file mode 100644 index 00000000000000..99f5d26304a7ee --- /dev/null +++ b/docs/getting-started/fundamentals/block-in-the-editor.md @@ -0,0 +1,169 @@ +# The block in the Editor + +The Block Editor is a React Single Page Application (SPA) and every block in the editor is displayed through a React component defined in the `edit` property of the settings object used to [register the block on the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side). + +The `props` object received by the block's `Edit` React component includes: +- [`attributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) - attributes object +- [`setAttributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#setattributes) - method to update the attributes object +- [`isSelected`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#isselected) - boolean that communicates whether the block is currently selected + +WordPress provides many built-in standard components that can be used to define the interface of the block in the editor. These built-in components are available via packages such as [`@wordpress/components`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/) and [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/). + +<div class="callout"> +The WordPress Gutenberg project uses <a href="https://wordpress.github.io/gutenberg/?path=/docs/docs-introduction--page">Storybook</a> to document the user interface components that are available in WordPress packages. +</div> + +Custom settings controls for the block in the Block Toolbar or the Settings Sidebar can also be defined through this `Edit` React component via built-in components such as: +- [`InspectorControls`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md) +- [`BlockControls`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor/src/components/block-controls) + +## Built-in components + +The package [`@wordpress/components`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/) includes a library of generic WordPress components to create common UI elements for the Block Editor and the WordPress dashboard. Some of the most commonly used components from this package are: +- [`TextControl`](https://wordpress.github.io/gutenberg/?path=/docs/components-textcontrol--docs) +- [`Panel`](https://wordpress.github.io/gutenberg/?path=/docs/components-panel--docs) +- [`ToggleControl`](https://wordpress.github.io/gutenberg/?path=/docs/components-togglecontrol--docs) +- [`ExternalLink`](https://wordpress.github.io/gutenberg/?path=/docs/components-externallink--docs) + +The package [`@wordpress/block-editor`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/) includes a library of components and hooks for the Block Editor, including those to define custom settings controls for the block in the Editor. Some of the components most commonly used from this package are: +- [`RichText`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/rich-text/README.md) +- [`BlockControls`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor/src/components/block-controls) +- [`InspectorControls`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md) +- [`InnerBlocks`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inner-blocks/README.md) +- `PanelColorSettings` or `ColorPalette` + +<div class="callout callout-tip"> +The package <a href="https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/"><code>@wordpress/block-editor</code></a> also provide the tools to create and use standalone block editors. +</div> + +A good workflow when using a component for the Block Editor is: +- Import the component from a WordPress package +- Add the corresponding code for the component to your project in JSX format +- Most built-in components will be used to set [block attributes](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#using-attributes-to-store-block-data), so define any necessary attributes in `block.json` and create event handlers to update those attributes with `setAttributes` in your component +- If needed, adapt the code to be serialized and stored in the database + + + +## Block Controls: Block Toolbar and Settings Sidebar + +To simplify block customization and ensure a consistent experience for users, there are a number of built-in UI patterns to help generate the editor preview. + +### Block Toolbar + +<img alt="Screenshot of the rich text toolbar applied to a Paragraph block inside the block editor" src="https://developer.wordpress.org/files/2023/12/toolbar-text.png" width="60%"> + +When the user selects a block, a number of control buttons may be shown in a toolbar above the selected block. Some of these block-level controls may be included automatically but you can also customize the toolbar to include controls specific to your block type. If the return value of your block type's `edit` function includes a `BlockControls` element, those controls will be shown in the selected block's toolbar. + +```jsx +export default function Edit( { className, attributes: attr, setAttributes } ) { + + const onChangeContent = ( newContent ) => { + setAttributes( { content: newContent } ); + }; + + const onChangeAlignment = ( newAlignment ) => { + setAttributes( { + alignment: newAlignment === undefined ? 'none' : newAlignment, + } ); + }; + + return ( + <div { ...useBlockProps() }> + <BlockControls> + <ToolbarGroup> + <AlignmentToolbar + value={ attr.alignment } + onChange={ onChangeAlignment } + /> + </ToolbarGroup> + </BlockControls> + + <RichText + className={ className } + style={ { textAlign: attr.alignment } } + tagName="p" + onChange={ onChangeContent } + value={ attr.content } + /> + </div> + ); +} +``` + +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/block-toolbar-ab967f) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/block-toolbar-ab967f/src/edit.js)._ + + +Note that `BlockControls` is only visible when the block is currently selected and in visual editing mode. `BlockControls` are not shown when editing a block in HTML editing mode. + + +### Settings Sidebar + +<img alt="Screenshot of the inspector panel focused on the settings for a Paragraph block" src="https://developer.wordpress.org/files/2023/12/settings-sidebar.png" width="60%"> + +The Settings Sidebar is used to display less-often-used settings or settings that require more screen space. The Settings Sidebar should be used for **block-level settings only**. + +If you have settings that affects only selected content inside a block (example: the "bold" setting for selected text inside a paragraph): **do not place it inside the Settings Sidebar**. The Settings Sidebar is displayed even when editing a block in HTML mode, so it should only contain block-level settings. + +The Block Tab is shown in place of the Document Tab when a block is selected. + +Similar to rendering a toolbar, if you include an `InspectorControls` element in the return value of your block type's `edit` function, those controls will be shown in the Settings Sidebar region. + +```jsx +export default function Edit( { attributes, setAttributes } ) { + const onChangeBGColor = ( hexColor ) => { + setAttributes( { bg_color: hexColor } ); + }; + + const onChangeTextColor = ( hexColor ) => { + setAttributes( { text_color: hexColor } ); + }; + + return ( + <div { ...useBlockProps() }> + <InspectorControls key="setting"> + <div> + <fieldset> + <legend className="blocks-base-control__label"> + { __( 'Background color', 'block-development-examples' ) } + </legend> + <ColorPalette // Element Tag for Gutenberg standard colour selector + onChange={ onChangeBGColor } // onChange event callback + /> + </fieldset> + <fieldset> + <legend className="blocks-base-control__label"> + { __( 'Text color', 'block-development-examples' ) } + </legend> + <ColorPalette + onChange={ onChangeTextColor } + /> + </fieldset> + </div> + </InspectorControls> + <TextControl + value={ attributes.message } + onChange={ ( val ) => setAttributes( { message: val } ) } + style={ { + backgroundColor: attributes.bg_color, + color: attributes.text_color, + } } + /> + </div> + ); +} +``` +_See the [full block example](https://github.com/WordPress/block-development-examples/tree/trunk/plugins/settings-sidebar-82c525) of the [code above](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/settings-sidebar-82c525/src/edit.js)._ + +Block controls rendered in both the toolbar and sidebar will also be used when multiple blocks of the same type are selected. + +<div class="callout callout-note"> +For common customization settings including color, border, spacing customization and more, you can rely on <a href="https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-json/#enable-ui-settings-panels-for-the-block-with-supports">block supports</a> to provide the same functionality in a more efficient way. +</div> + +## Additional resources + +- [Storybook for WordPress components](https://wordpress.github.io/gutenberg/?path=/docs/docs-introduction--page) +- [@wordpress/block-editor](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-block-editor/) +- [@wordpress/components](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-components/) +- [`Inspector Controls`](https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/inspector-controls/README.md) +- [`BlockControls`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-editor/src/components/block-controls) \ No newline at end of file diff --git a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md index daaddd707c3156..96ec695d9cefb2 100644 --- a/docs/getting-started/fundamentals/javascript-in-the-block-editor.md +++ b/docs/getting-started/fundamentals/javascript-in-the-block-editor.md @@ -42,6 +42,7 @@ Use [`enqueue_block_editor_assets`](https://developer.wordpress.org/reference/ho ## Additional resources +- [Package Reference](https://developer.wordpress.org/block-editor/reference-guides/packages/) - [Get started with wp-scripts](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-scripts/) - [Enqueueing assets in the Editor](https://developer.wordpress.org/block-editor/how-to-guides/enqueueing-assets-in-the-editor/) - [WordPress Packages handles](https://developer.wordpress.org/block-editor/contributors/code/scripts/) diff --git a/docs/manifest.json b/docs/manifest.json index 929b001fd2dc38..81501d167fae28 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -131,12 +131,6 @@ "markdown_source": "../docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md", "parent": "block-tutorial" }, - { - "title": "Block Controls: Block Toolbar and Settings Sidebar", - "slug": "block-controls-toolbar-and-sidebar", - "markdown_source": "../docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md", - "parent": "block-tutorial" - }, { "title": "Creating dynamic blocks", "slug": "creating-dynamic-blocks", diff --git a/docs/toc.json b/docs/toc.json index 6179915e62ae3a..d9b6794aec4002 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -56,9 +56,6 @@ { "docs/how-to-guides/block-tutorial/applying-styles-with-stylesheets.md": [] }, - { - "docs/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar.md": [] - }, { "docs/how-to-guides/block-tutorial/creating-dynamic-blocks.md": [] }, From b81790dcf6bbf8ec69cd13fae400d8f996a0063a Mon Sep 17 00:00:00 2001 From: Carlos Garcia <fluiddot@gmail.com> Date: Mon, 18 Dec 2023 20:26:27 +0100 Subject: [PATCH 249/325] [RNMobile] Avoid keyboard dismiss when interacting text blocks (#57070) * Use debounce in Aztec's blur function * Execute `focus` UI block before other blocks * Add `hideAndroidSoftKeyboard` to RN bridge * Add `blurOnUnmount` to Aztec input state manager. This function will help us to deal with the special case of unfocusing an Aztec view upon unmounting. * Dismiss keyboard when Aztec view unmounts This was previously handled in the `RichText` component. * Fix unit test related to `AztecInputState` after adding debounce to `blur` function * Remove console warning from `hideAndroidSoftKeyboard` * Update inline comments of `blurOnUnmount` function * [Mobile] - Android - Bring the Keyboard back when closing modals (#57069) * React Native Bridge - Android - Introduces showAndroidSoftKeyboard to show the keyboard if there's a focused TextInput * Mobile - Bottom Sheet - Adds usage of showAndroidSoftKeyboard when closing the Modal so it shows the Keyboard on Android for focused TextInputs * React Native Bridge - Android - Introduces hideAndroidSoftKeyboard to hide the keyboard without triggering blur events * React Native Bridge - Remove console warnings for unsupported methods, as their names are self-explanatory. * Update showAndroidSoftKeyboard to take into account when the window focus changed, when we show the Modals these are shown on top of the editor activity. It also adds an option to delay this for full screen modals * Mobile - BottomSheet - Enable hardwareAccelerated and useNativeDriverForBackdrop props to improve performance on Android * Update snapshots * Removes hasWindowFocus condition as it is not being called hence not needed * Refactor showAndroidSoftKeyboard to split into several functions, it also removes the delay functionality as it is no longer needed. It fixes an issue where mKeyboardRunnable was not being set. It removes the delay logic from the Bottom Sheet component and the bridge. * Updates createShowKeyboardRunnable to get the activity within the runnable instead of getting it as an param * Remove unneeded check * Update `react-native-editor` changelog --------- Co-authored-by: Gerardo Pacheco <gerardo.pacheco@automattic.com> --- .../rich-text/native/index.native.js | 6 -- .../src/mobile/bottom-sheet/index.native.js | 16 +++- .../test/__snapshots__/modal.native.js.snap | 2 + .../RNTAztecView/RCTAztecViewManager.swift | 13 +++- .../react-native-aztec/src/AztecInputState.js | 45 +++++++++++- packages/react-native-aztec/src/AztecView.js | 4 + .../src/test/AztecInputState.test.js | 3 + .../RNReactNativeGutenbergBridgeModule.java | 73 +++++++++++++++++++ packages/react-native-bridge/index.js | 27 +++++++ packages/react-native-editor/CHANGELOG.md | 1 + test/native/setup.js | 2 + 11 files changed, 183 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/native/index.native.js b/packages/block-editor/src/components/rich-text/native/index.native.js index 83396cbb4319fb..f83d03ece47983 100644 --- a/packages/block-editor/src/components/rich-text/native/index.native.js +++ b/packages/block-editor/src/components/rich-text/native/index.native.js @@ -824,12 +824,6 @@ export class RichText extends Component { } } - componentWillUnmount() { - if ( this._editor.isFocused() ) { - this._editor.blur(); - } - } - componentDidUpdate( prevProps ) { const { style, tagName } = this.props; const { currentFontSize } = this.state; diff --git a/packages/components/src/mobile/bottom-sheet/index.native.js b/packages/components/src/mobile/bottom-sheet/index.native.js index 11918782f4dfb4..820115b4ffea79 100644 --- a/packages/components/src/mobile/bottom-sheet/index.native.js +++ b/packages/components/src/mobile/bottom-sheet/index.native.js @@ -19,7 +19,10 @@ import SafeArea from 'react-native-safe-area'; /** * WordPress dependencies */ -import { subscribeAndroidModalClosed } from '@wordpress/react-native-bridge'; +import { + subscribeAndroidModalClosed, + showAndroidSoftKeyboard, +} from '@wordpress/react-native-bridge'; import { Component } from '@wordpress/element'; import { withPreferredColorScheme } from '@wordpress/compose'; @@ -215,6 +218,11 @@ class BottomSheet extends Component { if ( this.androidModalClosedSubscription ) { this.androidModalClosedSubscription.remove(); } + + if ( this.props.isVisible ) { + showAndroidSoftKeyboard(); + } + if ( this.safeAreaEventSubscription === null ) { return; } @@ -315,6 +323,9 @@ class BottomSheet extends Component { onDismiss() { const { onDismiss } = this.props; + // Restore Keyboard Visibility + showAndroidSoftKeyboard(); + if ( onDismiss ) { onDismiss(); } @@ -368,6 +379,7 @@ class BottomSheet extends Component { onHardwareButtonPress() { const { onClose } = this.props; const { handleHardwareButtonPress } = this.state; + if ( handleHardwareButtonPress && handleHardwareButtonPress() ) { return; } @@ -528,6 +540,8 @@ class BottomSheet extends Component { } onAccessibilityEscape={ this.onCloseBottomSheet } testID="bottom-sheet" + hardwareAccelerated={ true } + useNativeDriverForBackdrop={ true } { ...rest } > <KeyboardAvoidingView diff --git a/packages/format-library/src/link/test/__snapshots__/modal.native.js.snap b/packages/format-library/src/link/test/__snapshots__/modal.native.js.snap index 12d7ac6d30136f..59bbc23a7d81f0 100644 --- a/packages/format-library/src/link/test/__snapshots__/modal.native.js.snap +++ b/packages/format-library/src/link/test/__snapshots__/modal.native.js.snap @@ -7,6 +7,7 @@ exports[`LinksUI LinksUI renders 1`] = ` backdropOpacity={0.2} backdropTransitionInTiming={50} backdropTransitionOutTiming={50} + hardwareAccelerated={true} isVisible={true} onAccessibilityEscape={[Function]} onBackButtonPress={[Function]} @@ -18,6 +19,7 @@ exports[`LinksUI LinksUI renders 1`] = ` preferredColorScheme="light" swipeDirection="down" testID="link-settings-modal" + useNativeDriverForBackdrop={true} > <View behavior={false} diff --git a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift index 184d465e5d25cd..8806c780779445 100644 --- a/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift +++ b/packages/react-native-aztec/ios/RNTAztecView/RCTAztecViewManager.swift @@ -34,6 +34,17 @@ public class RCTAztecViewManager: RCTViewManager { return view } + /// This method is similar to `executeBlock` but prepends the block to execute it before other pending blocks. + func executeBlockBeforeOthers(viewTag: NSNumber, block: @escaping (RCTAztecView) -> Void) { + self.bridge.uiManager.prependUIBlock { (uiManager, viewRegistry) in + let view = viewRegistry?[viewTag] + guard let aztecView = view as? RCTAztecView else { + return + } + block(aztecView) + } + } + func executeBlock(viewTag: NSNumber, block: @escaping (RCTAztecView) -> Void) { self.bridge.uiManager.addUIBlock { (uiManager, viewRegistry) in let view = viewRegistry?[viewTag] @@ -69,7 +80,7 @@ public class RCTAztecViewManager: RCTViewManager { @objc func focus(_ viewTag: NSNumber) -> Void { - self.executeBlock(viewTag: viewTag) { (aztecView) in + self.executeBlockBeforeOthers(viewTag: viewTag) { (aztecView) in aztecView.reactFocus() } } diff --git a/packages/react-native-aztec/src/AztecInputState.js b/packages/react-native-aztec/src/AztecInputState.js index 5f0a1dd7596284..ca752d36e3d048 100644 --- a/packages/react-native-aztec/src/AztecInputState.js +++ b/packages/react-native-aztec/src/AztecInputState.js @@ -1,8 +1,15 @@ /** * External dependencies */ +import { Platform } from 'react-native'; import TextInputState from 'react-native/Libraries/Components/TextInput/TextInputState'; +/** + * WordPress dependencies + */ +import { debounce } from '@wordpress/compose'; +import { hideAndroidSoftKeyboard } from '@wordpress/react-native-bridge'; + /** @typedef {import('@wordpress/element').RefObject} RefObject */ const focusChangeListeners = []; @@ -131,21 +138,57 @@ export const focusInput = ( element ) => { * @param {RefObject} element Element to be focused. */ export const focus = ( element ) => { + // If other blur events happen at the same time that focus is triggered, the focus event + // will take precedence and cancels pending blur events. + blur.cancel(); + // Similar to blur events, we also need to cancel potential keyboard dismiss. + dismissKeyboardDebounce.cancel(); + TextInputState.focusTextInput( element ); notifyInputChange(); }; /** * Unfocuses the specified element. + * This function uses debounce to avoid conflicts with the focus event when both are + * triggered at the same time. Focus events will take precedence. * * @param {RefObject} element Element to be unfocused. */ -export const blur = ( element ) => { +export const blur = debounce( ( element ) => { TextInputState.blurTextInput( element ); setCurrentCaretData( null ); notifyInputChange(); +}, 0 ); + +/** + * Unfocuses the specified element in case it's about to be unmounted. + * + * On iOS text inputs are automatically unfocused and keyboard dismissed when they + * are removed. However, this is not the case on Android, where text inputs are + * unfocused but the keyboard remains open. + * + * For dismissing the keyboard, we use debounce to avoid conflicts with the focus + * event when both are triggered at the same time. + * + * Note that we can't trigger the blur event, as it's likely that the Aztec view is no + * longer available when the event is executed and will produce an exception. + * + * @param {RefObject} element Element to be unfocused. + */ +export const blurOnUnmount = ( element ) => { + if ( getCurrentFocusedElement() === element ) { + // If a blur event was triggered before unmount, we need to cancel them to avoid + // exceptions. + blur.cancel(); + if ( Platform.OS === 'android' ) { + dismissKeyboardDebounce(); + } + } }; +const dismissKeyboardDebounce = debounce( () => hideAndroidSoftKeyboard(), 0 ); + /** * Unfocuses the current focused element. */ diff --git a/packages/react-native-aztec/src/AztecView.js b/packages/react-native-aztec/src/AztecView.js index c9d0d633f1c14f..650790658ba32a 100644 --- a/packages/react-native-aztec/src/AztecView.js +++ b/packages/react-native-aztec/src/AztecView.js @@ -66,6 +66,10 @@ class AztecView extends Component { this.focus = this.focus.bind( this ); } + componentWillUnmount() { + AztecInputState.blurOnUnmount( this.aztecViewRef.current ); + } + dispatch( command, params ) { params = params || []; UIManager.dispatchViewManagerCommand( diff --git a/packages/react-native-aztec/src/test/AztecInputState.test.js b/packages/react-native-aztec/src/test/AztecInputState.test.js index 4e5a59c6caf23d..e95d25a695c964 100644 --- a/packages/react-native-aztec/src/test/AztecInputState.test.js +++ b/packages/react-native-aztec/src/test/AztecInputState.test.js @@ -32,6 +32,8 @@ const updateCurrentFocusedInput = ( value ) => { notifyInputChange(); }; +jest.useFakeTimers(); + describe( 'Aztec Input State', () => { it( 'listens to focus change event', () => { const listener = jest.fn(); @@ -96,6 +98,7 @@ describe( 'Aztec Input State', () => { it( 'unfocuses an element', () => { blur( ref ); + jest.runAllTimers(); expect( TextInputState.blurTextInput ).toHaveBeenCalledWith( ref ); } ); } ); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 0073db769d9cd5..8869148379905a 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -1,11 +1,15 @@ package org.wordpress.mobile.ReactNativeGutenbergBridge; +import android.app.Activity; import android.content.Context; import android.os.Build; import android.os.Bundle; import android.os.VibrationEffect; import android.os.Vibrator; import android.provider.Settings; +import android.view.View; +import android.view.ViewTreeObserver; +import android.view.inputmethod.InputMethodManager; import androidx.annotation.Nullable; @@ -41,6 +45,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu DeferredEventEmitter.JSEventEmitter { private final ReactApplicationContext mReactContext; private final GutenbergBridgeJS2Parent mGutenbergBridgeJS2Parent; + private Runnable mKeyboardRunnable; private static final String EVENT_NAME_REQUEST_GET_HTML = "requestGetHtml"; private static final String EVENT_NAME_UPDATE_HTML = "updateHtml"; @@ -550,4 +555,72 @@ private ConnectionStatusCallback requestConnectionStatusCallback(final Callback } }; } + + @ReactMethod + public void showAndroidSoftKeyboard() { + Activity currentActivity = mReactContext.getCurrentActivity(); + if (isAnyViewFocused()) { + // Cancel any previously scheduled Runnable + if (mKeyboardRunnable != null) { + currentActivity.getWindow().getDecorView().removeCallbacks(mKeyboardRunnable); + } + + View currentFocusedView = getCurrentFocusedView(); + currentFocusedView.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) { + mKeyboardRunnable = createShowKeyboardRunnable(); + currentActivity.getWindow().getDecorView().post(mKeyboardRunnable); + currentFocusedView.getViewTreeObserver().removeOnWindowFocusChangeListener(this); + } + } + }); + } + } + + private Runnable createShowKeyboardRunnable() { + return new Runnable() { + @Override + public void run() { + try { + Activity activity = mReactContext.getCurrentActivity(); + View activeFocusedView = getCurrentFocusedView(); + if (activeFocusedView != null && activity.getWindow().getDecorView().isShown()) { + InputMethodManager imm = + (InputMethodManager) mReactContext.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(activeFocusedView, InputMethodManager.SHOW_IMPLICIT); + } + } catch (Exception e) { + // Noop + } + } + }; + } + + private View getCurrentFocusedView() { + Activity activity = mReactContext.getCurrentActivity(); + if (activity == null) { + return null; + } + return activity.getCurrentFocus(); + } + + private boolean isAnyViewFocused() { + View getCurrentFocusedView = getCurrentFocusedView(); + return getCurrentFocusedView != null; + } + + @ReactMethod + public void hideAndroidSoftKeyboard() { + Activity currentActivity = mReactContext.getCurrentActivity(); + if (currentActivity != null) { + View currentFocusedView = currentActivity.getCurrentFocus(); + if (currentFocusedView != null) { + InputMethodManager imm = + (InputMethodManager) mReactContext.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(currentFocusedView.getWindowToken(), 0); + } + } + } } diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 8e9065cc568e56..1f4ee13d8625ec 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -515,6 +515,33 @@ export function sendEventToHost( eventName, properties ) { ); } +/** + * Shows Android's soft keyboard if there's a TextInput focused and + * the keyboard is hidden. + * + * @return {void} + */ +export function showAndroidSoftKeyboard() { + if ( isIOS ) { + return; + } + + RNReactNativeGutenbergBridge.showAndroidSoftKeyboard(); +} + +/** + * Hides Android's soft keyboard. + * + * @return {void} + */ +export function hideAndroidSoftKeyboard() { + if ( isIOS ) { + return; + } + + RNReactNativeGutenbergBridge.hideAndroidSoftKeyboard(); +} + /** * Generate haptic feedback. */ diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index a66368115a3206..fc12b7df655cd5 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -16,6 +16,7 @@ For each user feature we should also add a importance categorization label to i - [*] Guard against an Image block styles crash due to null block values [#56903] - [**] Fix crash when sharing unsupported media types on Android [#56791] - [**] Fix regressions with wrapper props and font size customization [#56985] +- [***] Avoid keyboard dismiss when interacting with text blocks [#57070] ## 1.109.3 - [**] Fix duplicate/unresponsive options in font size settings. [#56985] diff --git a/test/native/setup.js b/test/native/setup.js index 0f4c9f9eda20c9..8bfa8fb0626f24 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -109,6 +109,8 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeOnRedoPressed: jest.fn(), useIsConnected: jest.fn( () => ( { isConnected: true } ) ), editorDidMount: jest.fn(), + showAndroidSoftKeyboard: jest.fn(), + hideAndroidSoftKeyboard: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), subscribeMediaSave: jest.fn(), From b0f04c92403b3b29168a089dc6cfe4932179ef05 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Mon, 18 Dec 2023 21:00:34 +0100 Subject: [PATCH 250/325] Docs: Fundamentals block development/block in the editor -- add page to manifest (toc) (#57179) * Add block-in-the-editor.md file * Add block editing interface details and built-in components * Update block in the Editor with React components * Update block display and behavior in the Block Editor * new file destination * new changes * Update block registration link in block-in-the-editor.md * Add Block Controls and Settings Sidebar documentation * Add additional references to block in the editor * Remove duplicated content page from how-to-guides * Update block fundamentals README with additional information * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> * Update block editor component references * Add BlockControls link to block-in-the-editor.md * Add new block in the Editor to TOC --------- Co-authored-by: Nick Diego <nick.diego@automattic.com> --- docs/manifest.json | 6 ++++++ docs/toc.json | 3 +++ 2 files changed, 9 insertions(+) diff --git a/docs/manifest.json b/docs/manifest.json index 81501d167fae28..fb6d8550fa7ec9 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -83,6 +83,12 @@ "markdown_source": "../docs/getting-started/fundamentals/block-wrapper.md", "parent": "fundamentals" }, + { + "title": "The block in the Editor", + "slug": "block-in-the-editor", + "markdown_source": "../docs/getting-started/fundamentals/block-in-the-editor.md", + "parent": "fundamentals" + }, { "title": "Working with Javascript for the Block Editor", "slug": "javascript-in-the-block-editor", diff --git a/docs/toc.json b/docs/toc.json index d9b6794aec4002..4b4f5bbd69a5f6 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -36,6 +36,9 @@ { "docs/getting-started/fundamentals/block-wrapper.md": [] }, + { + "docs/getting-started/fundamentals/block-in-the-editor.md": [] + }, { "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] } From 33b1cadc8efa375f8088acb66a85d44d875b6afa Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 19 Dec 2023 06:26:32 +0900 Subject: [PATCH 251/325] Image Block: Fix deprecation when width/height attribute is number (#57063) --- packages/block-library/src/image/deprecated.js | 8 ++++++++ .../core__image__deprecated-v3-add-align-wrapper.json | 4 ++-- ...image__deprecated-v3-add-align-wrapper.serialized.html | 2 +- ...core__image__deprecated-v6-add-style-width-height.json | 4 ++-- ...__deprecated-v6-add-style-width-height.serialized.html | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/image/deprecated.js b/packages/block-library/src/image/deprecated.js index 0365ddcfff5d17..8d1039696647a3 100644 --- a/packages/block-library/src/image/deprecated.js +++ b/packages/block-library/src/image/deprecated.js @@ -651,6 +651,14 @@ const v6 = { }, }, }, + migrate( attributes ) { + const { height, width } = attributes; + return { + ...attributes, + width: typeof width === 'number' ? `${ width }px` : width, + height: typeof height === 'number' ? `${ height }px` : height, + }; + }, save( { attributes } ) { const { url, diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json index bae213510011ac..644f9629ea8d86 100644 --- a/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json +++ b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.json @@ -7,8 +7,8 @@ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", "alt": "", "caption": "", - "width": 100, - "height": 100 + "width": "100px", + "height": "100px" }, "innerBlocks": [] } diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html index c03189e9b456c0..2c0d6f487d57b1 100644 --- a/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html +++ b/test/integration/fixtures/blocks/core__image__deprecated-v3-add-align-wrapper.serialized.html @@ -1,3 +1,3 @@ -<!-- wp:image {"width":100,"height":100,"align":"left"} --> +<!-- wp:image {"width":"100px","height":"100px","align":"left"} --> <figure class="wp-block-image alignleft is-resized"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" alt="" style="width:100px;height:100px"/></figure> <!-- /wp:image --> diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.json b/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.json index 7f83baa81fc635..1acdf6f92453c8 100644 --- a/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.json +++ b/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.json @@ -7,8 +7,8 @@ "url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==", "alt": "", "caption": "", - "width": 164, - "height": 164, + "width": "164px", + "height": "164px", "sizeSlug": "large", "className": "is-style-rounded", "style": { diff --git a/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.serialized.html b/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.serialized.html index 57545968847e1b..5ffd11a5baa8e7 100644 --- a/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.serialized.html +++ b/test/integration/fixtures/blocks/core__image__deprecated-v6-add-style-width-height.serialized.html @@ -1,3 +1,3 @@ -<!-- wp:image {"width":164,"height":164,"sizeSlug":"large","align":"left","className":"is-style-rounded","style":{"border":{"radius":"100%"}}} --> +<!-- wp:image {"width":"164px","height":"164px","sizeSlug":"large","align":"left","className":"is-style-rounded","style":{"border":{"radius":"100%"}}} --> <figure class="wp-block-image alignleft size-large is-resized has-custom-border is-style-rounded"><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg==" alt="" style="border-radius:100%;width:164px;height:164px"/></figure> <!-- /wp:image --> From 5ee266071aa2ccae703e95253a9cbac141743816 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Tue, 19 Dec 2023 02:42:56 +0400 Subject: [PATCH 252/325] Block Editor: Combine selectors in the 'BackgroundImagePanelItem' component (#57159) --- packages/block-editor/src/hooks/background.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index e8518bcbc419f2..ff494590c21c77 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -143,21 +143,22 @@ function InspectorImagePreview( { label, filename, url: imgUrl } ) { } function BackgroundImagePanelItem( { clientId, setAttributes } ) { - const style = useSelect( - ( select ) => - select( blockEditorStore ).getBlockAttributes( clientId )?.style, + const { style, mediaUpload } = useSelect( + ( select ) => { + const { getBlockAttributes, getSettings } = + select( blockEditorStore ); + + return { + style: getBlockAttributes( clientId )?.style, + mediaUpload: getSettings().mediaUpload, + }; + }, [ clientId ] ); const { id, title, url } = style?.background?.backgroundImage || {}; const replaceContainerRef = useRef(); - const { mediaUpload } = useSelect( ( select ) => { - return { - mediaUpload: select( blockEditorStore ).getSettings().mediaUpload, - }; - } ); - const { createErrorNotice } = useDispatch( noticesStore ); const onUploadError = ( message ) => { createErrorNotice( message, { type: 'snackbar' } ); From aa84ec2a0f1372880ac2442c8b9475678f353f59 Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Tue, 19 Dec 2023 11:06:42 +0900 Subject: [PATCH 253/325] Disable resizing when viewport is small and wide-aligned (#57041) --- packages/block-library/src/image/image.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index b74079b2b8b79d..866bea022fdcf1 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -196,7 +196,8 @@ export default function Image( { const isResizable = allowResize && hasNonContentControls && - ! ( isWideAligned && isLargeViewport ); + ! isWideAligned && + isLargeViewport; const imageSizeOptions = imageSizes .filter( ( { slug } ) => image?.media_details?.sizes?.[ slug ]?.source_url From 391aef63dc6020610d58065dc42d94761e280e06 Mon Sep 17 00:00:00 2001 From: Rich Tabor <hi@richtabor.com> Date: Mon, 18 Dec 2023 21:40:58 -0500 Subject: [PATCH 254/325] Move tools panel to the left of the inspector (#55785) --- .../components/global-styles/border-panel.js | 3 +- .../components/global-styles/color-panel.js | 3 +- .../global-styles/dimensions-panel.js | 3 +- .../components/global-styles/effects-panel.js | 3 +- .../components/global-styles/filters-panel.js | 7 ++-- .../global-styles/image-settings-panel.js | 6 ++++ .../global-styles/typography-panel.js | 3 +- .../src/components/global-styles/utils.js | 7 ++++ .../block-support-tools-panel.js | 2 ++ packages/block-library/src/image/image.js | 13 ++++++-- .../query/edit/inspector-controls/index.js | 2 ++ packages/block-library/src/utils/constants.js | 8 +++++ packages/components/CHANGELOG.md | 1 + .../components/src/tools-panel/test/index.tsx | 32 +++++++----------- .../tools-panel/tools-panel-header/README.md | 7 ++++ .../tools-panel-header/component.tsx | 33 +++++++++++-------- .../src/tools-panel/tools-panel/README.md | 7 ++++ .../src/tools-panel/tools-panel/component.tsx | 2 ++ packages/components/src/tools-panel/types.ts | 9 +++++ 19 files changed, 106 insertions(+), 45 deletions(-) create mode 100644 packages/block-library/src/utils/constants.js diff --git a/packages/block-editor/src/components/global-styles/border-panel.js b/packages/block-editor/src/components/global-styles/border-panel.js index 63ff223f782722..ff1f3ddf55471e 100644 --- a/packages/block-editor/src/components/global-styles/border-panel.js +++ b/packages/block-editor/src/components/global-styles/border-panel.js @@ -16,7 +16,7 @@ import { __ } from '@wordpress/i18n'; */ import BorderRadiusControl from '../border-radius-control'; import { useColorsPerOrigin } from './hooks'; -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; export function useHasBorderPanel( settings ) { const controls = [ @@ -62,6 +62,7 @@ function BorderToolsPanel( { label={ __( 'Border' ) } resetAll={ resetAll } panelId={ panelId } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-editor/src/components/global-styles/color-panel.js b/packages/block-editor/src/components/global-styles/color-panel.js index 469a4080f1e600..c34335bc9dda50 100644 --- a/packages/block-editor/src/components/global-styles/color-panel.js +++ b/packages/block-editor/src/components/global-styles/color-panel.js @@ -27,7 +27,7 @@ import { __, sprintf } from '@wordpress/i18n'; */ import ColorGradientControl from '../colors-gradients/control'; import { useColorsPerOrigin, useGradientsPerOrigin } from './hooks'; -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { setImmutably } from '../../utils/object'; import { unlock } from '../../lock-unlock'; @@ -130,6 +130,7 @@ function ColorToolsPanel( { className="color-block-support-panel" __experimentalFirstVisibleItemClass="first" __experimentalLastVisibleItemClass="last" + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > <div className="color-block-support-panel__inner-wrapper"> { children } diff --git a/packages/block-editor/src/components/global-styles/dimensions-panel.js b/packages/block-editor/src/components/global-styles/dimensions-panel.js index b5eb6175c8c5e4..47b5bd329725a7 100644 --- a/packages/block-editor/src/components/global-styles/dimensions-panel.js +++ b/packages/block-editor/src/components/global-styles/dimensions-panel.js @@ -23,7 +23,7 @@ import { useCallback, Platform } from '@wordpress/element'; /** * Internal dependencies */ -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import SpacingSizesControl from '../spacing-sizes-control'; import HeightControl from '../height-control'; import ChildLayoutControl from '../child-layout-control'; @@ -178,6 +178,7 @@ function DimensionsToolsPanel( { label={ __( 'Dimensions' ) } resetAll={ resetAll } panelId={ panelId } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-editor/src/components/global-styles/effects-panel.js b/packages/block-editor/src/components/global-styles/effects-panel.js index cda641e0fb17a0..9a9fd8d1258edd 100644 --- a/packages/block-editor/src/components/global-styles/effects-panel.js +++ b/packages/block-editor/src/components/global-styles/effects-panel.js @@ -26,7 +26,7 @@ import { shadow as shadowIcon, Icon, check } from '@wordpress/icons'; /** * Internal dependencies */ -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { setImmutably } from '../../utils/object'; export function useHasEffectsPanel( settings ) { @@ -55,6 +55,7 @@ function EffectsToolsPanel( { label={ __( 'Effects' ) } resetAll={ resetAll } panelId={ panelId } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-editor/src/components/global-styles/filters-panel.js b/packages/block-editor/src/components/global-styles/filters-panel.js index 42c2494489ed14..79b2c00cf10b0a 100644 --- a/packages/block-editor/src/components/global-styles/filters-panel.js +++ b/packages/block-editor/src/components/global-styles/filters-panel.js @@ -28,7 +28,7 @@ import { useCallback, useMemo } from '@wordpress/element'; /** * Internal dependencies */ -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { setImmutably } from '../../utils/object'; const EMPTY_ARRAY = []; @@ -82,10 +82,7 @@ function FiltersToolsPanel( { label={ _x( 'Filters', 'Name for applying graphical effects' ) } resetAll={ resetAll } panelId={ panelId } - dropdownMenuProps={ { - placement: 'left-start', - offset: 258, // sidebar width (280px) - button width (24px) + border (2px) - } } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-editor/src/components/global-styles/image-settings-panel.js b/packages/block-editor/src/components/global-styles/image-settings-panel.js index 68054cf129e204..5ac0aa29b321e7 100644 --- a/packages/block-editor/src/components/global-styles/image-settings-panel.js +++ b/packages/block-editor/src/components/global-styles/image-settings-panel.js @@ -8,6 +8,11 @@ import { } from '@wordpress/components'; import { __, _x } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; + export function useHasImageSettingsPanel( name, value, inheritedValue ) { // Note: If lightbox `value` exists, that means it was // defined via the the Global Styles UI and will NOT @@ -47,6 +52,7 @@ export default function ImageSettingsPanel( { label={ _x( 'Settings', 'Image settings' ) } resetAll={ resetLightbox } panelId={ panelId } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > <ToolsPanelItem // We use the `userSettings` prop instead of `settings`, because `settings` diff --git a/packages/block-editor/src/components/global-styles/typography-panel.js b/packages/block-editor/src/components/global-styles/typography-panel.js index 103b1e63b75b5e..8e6755a6e4c2c4 100644 --- a/packages/block-editor/src/components/global-styles/typography-panel.js +++ b/packages/block-editor/src/components/global-styles/typography-panel.js @@ -21,7 +21,7 @@ import LetterSpacingControl from '../letter-spacing-control'; import TextTransformControl from '../text-transform-control'; import TextDecorationControl from '../text-decoration-control'; import WritingModeControl from '../writing-mode-control'; -import { getValueFromVariable } from './utils'; +import { getValueFromVariable, TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; import { setImmutably } from '../../utils/object'; const MIN_TEXT_COLUMNS = 1; @@ -129,6 +129,7 @@ function TypographyToolsPanel( { label={ __( 'Typography' ) } resetAll={ resetAll } panelId={ panelId } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-editor/src/components/global-styles/utils.js b/packages/block-editor/src/components/global-styles/utils.js index f4adb7a7903122..34964d3c92905b 100644 --- a/packages/block-editor/src/components/global-styles/utils.js +++ b/packages/block-editor/src/components/global-styles/utils.js @@ -159,6 +159,13 @@ export const STYLE_PATH_TO_PRESET_BLOCK_ATTRIBUTE = { 'typography.fontFamily': 'fontFamily', }; +export const TOOLSPANEL_DROPDOWNMENU_PROPS = { + popoverProps: { + placement: 'left-start', + offset: 259, // Inner sidebar width (248px) - button width (24px) - border (1px) + padding (16px) + spacing (20px) + }, +}; + function findInPresetsBy( features, blockName, diff --git a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js index 46ec5839726e6a..e722f2ee44bf8c 100644 --- a/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js +++ b/packages/block-editor/src/components/inspector-controls/block-support-tools-panel.js @@ -10,6 +10,7 @@ import { useCallback } from '@wordpress/element'; */ import { store as blockEditorStore } from '../../store'; import { cleanEmptyObject } from '../../hooks/utils'; +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from '../global-styles/utils'; export default function BlockSupportToolsPanel( { children, group, label } ) { const { updateBlockAttributes } = useDispatch( blockEditorStore ); @@ -71,6 +72,7 @@ export default function BlockSupportToolsPanel( { children, group, label } ) { shouldRenderPlaceholderItems={ true } // Required to maintain fills ordering. __experimentalFirstVisibleItemClass="first" __experimentalLastVisibleItemClass="last" + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { children } </ToolsPanel> diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 866bea022fdcf1..e76a99e424a510 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -48,6 +48,7 @@ import { Caption } from '../utils/caption'; /** * Module constants */ +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from '../utils/constants'; import { MIN_SIZE, ALLOWED_MEDIA_TYPES } from './constants'; import { evalAspectRatio } from './utils'; @@ -392,7 +393,11 @@ export default function Image( { const sizeControls = ( <InspectorControls> - <ToolsPanel label={ __( 'Settings' ) } resetAll={ resetAll }> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ resetAll } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } + > { isResizable && dimensionsControl } </ToolsPanel> </InspectorControls> @@ -453,7 +458,11 @@ export default function Image( { </BlockControls> ) } <InspectorControls> - <ToolsPanel label={ __( 'Settings' ) } resetAll={ resetAll }> + <ToolsPanel + label={ __( 'Settings' ) } + resetAll={ resetAll } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } + > { ! multiImageSelection && ( <ToolsPanelItem label={ __( 'Alternative text' ) } diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index d276bdec98ed61..398016728c499c 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -37,6 +37,7 @@ import { isControlAllowed, useTaxonomies, } from '../../utils'; +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from '../../../utils/constants'; const { BlockInfo } = unlock( blockEditorPrivateApis ); @@ -226,6 +227,7 @@ export default function QueryInspectorControls( props ) { } ); setQuerySearch( '' ); } } + dropdownMenuProps={ TOOLSPANEL_DROPDOWNMENU_PROPS } > { showTaxControl && ( <ToolsPanelItem diff --git a/packages/block-library/src/utils/constants.js b/packages/block-library/src/utils/constants.js new file mode 100644 index 00000000000000..fc55a5bb11fbd4 --- /dev/null +++ b/packages/block-library/src/utils/constants.js @@ -0,0 +1,8 @@ +// The following dropdown menu props aim to provide a consistent offset and +// placement for ToolsPanel menus for block controls to match color popovers. +export const TOOLSPANEL_DROPDOWNMENU_PROPS = { + popoverProps: { + placement: 'left-start', + offset: 259, // Inner sidebar width (248px) - button width (24px) - border (1px) + padding (16px) + spacing (20px) + }, +}; diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 970341765bfe69..01a073567e2f22 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -28,6 +28,7 @@ - `FocalPointPicker`: Add opt-in prop for 40px default size ([#56021](https://github.com/WordPress/gutenberg/pull/56021)). - `DimensionControl`: Add opt-in prop for 40px default size ([#56805](https://github.com/WordPress/gutenberg/pull/56805)). - `FontSizePicker`: Add opt-in prop for 40px default size ([#56804](https://github.com/WordPress/gutenberg/pull/56804)). +- `ToolsPanel`/`ToolsPanelHeader`: Added `dropdownMenuProps` to allow customization of the panel's dropdown menu. Also merged default and optional control menu groups ([#55785](https://github.com/WordPress/gutenberg/pull/55785)). ### Bug Fix diff --git a/packages/components/src/tools-panel/test/index.tsx b/packages/components/src/tools-panel/test/index.tsx index 812a21ec0d76ea..c7aaf1522b8660 100644 --- a/packages/components/src/tools-panel/test/index.tsx +++ b/packages/components/src/tools-panel/test/index.tsx @@ -455,8 +455,8 @@ describe( 'ToolsPanel', () => { const menuGroups = screen.getAllByRole( 'group' ); - // Groups should be: default controls, optional controls & reset all. - expect( menuGroups.length ).toEqual( 3 ); + // There are now only two groups controls & reset all. + expect( menuGroups.length ).toEqual( 2 ); } ); it( 'should not render contents of items when in placeholder state', () => { @@ -517,15 +517,11 @@ describe( 'ToolsPanel', () => { await openDropdownMenu(); - // The linked control should initially appear in the optional controls - // menu group. There should be three menu groups: default controls, - // optional controls, and the group to reset all options. let menuGroups = screen.getAllByRole( 'group' ); - expect( menuGroups.length ).toEqual( 3 ); - // The linked control should be in the second group, of optional controls. + // The linked control should be in the first group of controls. expect( - within( menuGroups[ 1 ] ).getByText( 'Linked' ) + within( menuGroups[ 0 ] ).getByText( 'Linked' ) ).toBeInTheDocument(); // Simulate the main control having a value set which should @@ -540,22 +536,18 @@ describe( 'ToolsPanel', () => { linkedItem = screen.getByText( 'Linked control' ); expect( linkedItem ).toBeInTheDocument(); - // The linked control should now appear in the default controls - // menu group and have been removed from the optional group. + // The linked control should still appear in the controls + // menu group but as a default control. menuGroups = screen.getAllByRole( 'group' ); - // There should now only be two groups. The default controls and - // and the group for the reset all option. - expect( menuGroups.length ).toEqual( 2 ); - - // The new default control item for the Linked control should be - // within the first menu group. + // The new default control item for the Linked control should still + // be within the first menu group. const defaultItem = within( menuGroups[ 0 ] ).getByText( 'Linked' ); expect( defaultItem ).toBeInTheDocument(); // Optional controls have an additional aria-label. This can be used - // to confirm the conditional default control has been removed from - // the optional menu item group. + // to confirm the conditional default control is now being treated + // as default control. expect( screen.queryByRole( 'menuitemcheckbox', { name: 'Show Linked', @@ -599,7 +591,7 @@ describe( 'ToolsPanel', () => { let conditionalItem = screen.queryByText( 'Conditional control' ); expect( conditionalItem ).not.toBeInTheDocument(); - // The conditional control should not yet appear in the default controls + // The conditional control should not yet appear in the controls // menu group. await openDropdownMenu(); let menuGroups = screen.getAllByRole( 'group' ); @@ -619,7 +611,7 @@ describe( 'ToolsPanel', () => { conditionalItem = screen.getByText( 'Conditional control' ); expect( conditionalItem ).toBeInTheDocument(); - // The conditional control should now appear in the default controls + // The conditional control should now appear in the controls // menu group. menuGroups = screen.getAllByRole( 'group' ); diff --git a/packages/components/src/tools-panel/tools-panel-header/README.md b/packages/components/src/tools-panel/tools-panel-header/README.md index e6164306dbee31..85e5aae5043948 100644 --- a/packages/components/src/tools-panel/tools-panel-header/README.md +++ b/packages/components/src/tools-panel/tools-panel-header/README.md @@ -18,6 +18,13 @@ This component is generated automatically by its parent ## Props +### `dropdownMenuProps`: `{}` + +The dropdown menu props to configure the panel's `DropdownMenu`. + +- Type: `DropdownMenuProps` +- Required: No + ### `headingLevel`: `1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6'` The heading level of the panel's header. diff --git a/packages/components/src/tools-panel/tools-panel-header/component.tsx b/packages/components/src/tools-panel/tools-panel-header/component.tsx index a48ddac3bc34a0..3739134008577d 100644 --- a/packages/components/src/tools-panel/tools-panel-header/component.tsx +++ b/packages/components/src/tools-panel/tools-panel-header/component.tsx @@ -39,7 +39,7 @@ const DefaultControlsGroup = ( { const resetSuffix = <ResetLabel aria-hidden>{ __( 'Reset' ) }</ResetLabel>; return ( - <MenuGroup label={ __( 'Defaults' ) }> + <> { items.map( ( [ label, hasValue ] ) => { if ( hasValue ) { return ( @@ -73,6 +73,7 @@ const DefaultControlsGroup = ( { return ( <MenuItem key={ label } + icon={ check } className={ itemClassName } role="menuitemcheckbox" isSelected @@ -82,7 +83,7 @@ const DefaultControlsGroup = ( { </MenuItem> ); } ) } - </MenuGroup> + </> ); }; @@ -95,7 +96,7 @@ const OptionalControlsGroup = ( { } return ( - <MenuGroup label={ __( 'Tools' ) }> + <> { items.map( ( [ label, isSelected ] ) => { const itemLabel = isSelected ? sprintf( @@ -143,7 +144,7 @@ const OptionalControlsGroup = ( { </MenuItem> ); } ) } - </MenuGroup> + </> ); }; @@ -162,6 +163,7 @@ const ToolsPanelHeader = ( menuItems, resetAll, toggleItem, + dropdownMenuProps, ...headerProps } = useToolsPanelHeader( props ); @@ -192,6 +194,7 @@ const ToolsPanelHeader = ( </Heading> { hasMenuItems && ( <DropdownMenu + { ...dropdownMenuProps } icon={ dropDownMenuIcon } label={ dropDownMenuLabelText } menuProps={ { className: dropdownMenuClassName } } @@ -202,15 +205,19 @@ const ToolsPanelHeader = ( > { () => ( <> - <DefaultControlsGroup - items={ defaultItems } - toggleItem={ toggleItem } - itemClassName={ defaultControlsItemClassName } - /> - <OptionalControlsGroup - items={ optionalItems } - toggleItem={ toggleItem } - /> + <MenuGroup label={ labelText }> + <DefaultControlsGroup + items={ defaultItems } + toggleItem={ toggleItem } + itemClassName={ + defaultControlsItemClassName + } + /> + <OptionalControlsGroup + items={ optionalItems } + toggleItem={ toggleItem } + /> + </MenuGroup> <MenuGroup> <MenuItem aria-disabled={ ! canResetAll } diff --git a/packages/components/src/tools-panel/tools-panel/README.md b/packages/components/src/tools-panel/tools-panel/README.md index 0ee251592d67b1..df41b623eefb6c 100644 --- a/packages/components/src/tools-panel/tools-panel/README.md +++ b/packages/components/src/tools-panel/tools-panel/README.md @@ -157,6 +157,13 @@ wrapper element allowing the panel to lay them out accordingly. - Required: No - Default: `false` +### `dropdownMenuProps`: `{}` + +The popover props to configure panel's `DropdownMenu`. + +- Type: `DropdownMenuProps` +- Required: No + ### `headingLevel`: `1 | 2 | 3 | 4 | 5 | 6 | '1' | '2' | '3' | '4' | '5' | '6'` The heading level of the panel's header. diff --git a/packages/components/src/tools-panel/tools-panel/component.tsx b/packages/components/src/tools-panel/tools-panel/component.tsx index 660a782e810214..4e01e39ffffb43 100644 --- a/packages/components/src/tools-panel/tools-panel/component.tsx +++ b/packages/components/src/tools-panel/tools-panel/component.tsx @@ -25,6 +25,7 @@ const UnconnectedToolsPanel = ( resetAllItems, toggleItem, headingLevel, + dropdownMenuProps, ...toolsPanelProps } = useToolsPanel( props ); @@ -36,6 +37,7 @@ const UnconnectedToolsPanel = ( resetAll={ resetAllItems } toggleItem={ toggleItem } headingLevel={ headingLevel } + dropdownMenuProps={ dropdownMenuProps } /> { children } </ToolsPanelContext.Provider> diff --git a/packages/components/src/tools-panel/types.ts b/packages/components/src/tools-panel/types.ts index 3156137e580442..9f4fc78bea46a4 100644 --- a/packages/components/src/tools-panel/types.ts +++ b/packages/components/src/tools-panel/types.ts @@ -7,6 +7,7 @@ import type { ReactNode } from 'react'; * Internal dependencies */ import type { HeadingSize } from '../heading/types'; +import type { DropdownMenu } from '../dropdown-menu'; export type ResetAllFilter = ( attributes?: any ) => any; type ResetAll = ( filters?: ResetAllFilter[] ) => void; @@ -16,6 +17,10 @@ export type ToolsPanelProps = { * The child elements. */ children: ReactNode; + /** + * The dropdown menu props to configure the panel's `DropdownMenu`. + */ + dropdownMenuProps?: React.ComponentProps< typeof DropdownMenu >; /** * Flags that the items in this ToolsPanel will be contained within an inner * wrapper element allowing the panel to lay them out accordingly. @@ -69,6 +74,10 @@ export type ToolsPanelProps = { }; export type ToolsPanelHeaderProps = { + /** + * The dropdown menu props to configure the panel's `DropdownMenu`. + */ + dropdownMenuProps?: React.ComponentProps< typeof DropdownMenu >; /** * The heading level of the panel's header. * From a4a7feb31306fa2a1a6e85d4568da432968fe6b9 Mon Sep 17 00:00:00 2001 From: Kai Hao <kevin830726@gmail.com> Date: Tue, 19 Dec 2023 12:02:36 +0800 Subject: [PATCH 255/325] Fix unsaved pattern not reflecting on pattern overrides (#57148) --- packages/block-library/src/block/edit.js | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/block/edit.js b/packages/block-library/src/block/edit.js index e86ed9b59c62b2..fbfef0b4cf1778 100644 --- a/packages/block-library/src/block/edit.js +++ b/packages/block-library/src/block/edit.js @@ -140,7 +140,7 @@ export default function ReusableBlockEdit( { } ) { const registry = useRegistry(); const hasAlreadyRendered = useHasRecursion( ref ); - const { record, hasResolved } = useEntityRecord( + const { record, editedRecord, hasResolved } = useEntityRecord( 'postType', 'wp_block', ref @@ -156,9 +156,13 @@ export default function ReusableBlockEdit( { const { getBlockEditingMode } = useSelect( blockEditorStore ); useEffect( () => { - if ( ! record?.content?.raw ) return; - const initialBlocks = parse( record.content.raw ); + const initialBlocks = + editedRecord.blocks ?? + ( editedRecord.content && typeof editedRecord.content !== 'function' + ? parse( editedRecord.content ) + : [] ); + defaultValuesRef.current = {}; const editingMode = getBlockEditingMode( patternClientId ); registry.batch( () => { setBlockEditingMode( patternClientId, 'default' ); @@ -176,7 +180,7 @@ export default function ReusableBlockEdit( { }, [ __unstableMarkNextChangeAsNotPersistent, patternClientId, - record, + editedRecord, replaceInnerBlocks, registry, getBlockEditingMode, From 16bc9a08e361c23c90a592898785725431e73cfd Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 19 Dec 2023 10:14:07 +0100 Subject: [PATCH 256/325] Site Editor: Add View Link (#57153) Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- packages/edit-post/src/components/header/index.js | 5 ++--- .../src/components/header-edit-mode/index.js | 3 ++- .../src/components/post-view-link}/index.js | 12 +++++------- packages/editor/src/private-apis.js | 2 ++ 4 files changed, 11 insertions(+), 11 deletions(-) rename packages/{edit-post/src/components/view-link => editor/src/components/post-view-link}/index.js (73%) diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index d6871f95f036ac..67349bd6404038 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -37,12 +37,11 @@ import FullscreenModeClose from './fullscreen-mode-close'; import HeaderToolbar from './header-toolbar'; import MoreMenu from './more-menu'; import PostPublishButtonOrToggle from './post-publish-button-or-toggle'; -import ViewLink from '../view-link'; import MainDashboardButton from './main-dashboard-button'; import { store as editPostStore } from '../../store'; import { unlock } from '../../lock-unlock'; -const { PreviewDropdown } = unlock( editorPrivateApis ); +const { PostViewLink, PreviewDropdown } = unlock( editorPrivateApis ); const slideY = { hidden: { y: '-50px' }, @@ -189,7 +188,7 @@ function Header( { className="edit-post-header__post-preview-button" forceIsAutosaveable={ hasActiveMetaboxes } /> - <ViewLink /> + <PostViewLink showIconLabels={ showIconLabels } /> <PostPublishButtonOrToggle forceIsDirty={ hasActiveMetaboxes } setEntitiesSavedStatesCallback={ diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index a18c7e3a3eaad4..4b9d48d797952d 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -42,7 +42,7 @@ import { import { unlock } from '../../lock-unlock'; import { FOCUSABLE_ENTITIES } from '../../utils/constants'; -const { PreviewDropdown } = unlock( editorPrivateApis ); +const { PostViewLink, PreviewDropdown } = unlock( editorPrivateApis ); export default function HeaderEditMode( { setListViewToggleElement } ) { const { @@ -217,6 +217,7 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { /> </div> ) } + <PostViewLink showIconLabels={ showIconLabels } /> <SaveButton /> { ! isDistractionFree && ( <PinnedItems.Slot scope="core/edit-site" /> diff --git a/packages/edit-post/src/components/view-link/index.js b/packages/editor/src/components/post-view-link/index.js similarity index 73% rename from packages/edit-post/src/components/view-link/index.js rename to packages/editor/src/components/post-view-link/index.js index 3ebf78b851e1f7..57866488ff103b 100644 --- a/packages/edit-post/src/components/view-link/index.js +++ b/packages/editor/src/components/post-view-link/index.js @@ -4,17 +4,16 @@ import { __ } from '@wordpress/i18n'; import { Button } from '@wordpress/components'; import { external } from '@wordpress/icons'; -import { store as editorStore } from '@wordpress/editor'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect } from '@wordpress/data'; /** * Internal dependencies */ -import { store as editPostStore } from '../../store'; +import { store as editorStore } from '../../store'; -export default function ViewLink() { - const { permalink, isPublished, label, showIconLabels } = useSelect( +export default function PostViewLink( { showIconLabels } ) { + const { hasLoaded, permalink, isPublished, label } = useSelect( ( select ) => { // Grab post type to retrieve the view_item label. const postTypeSlug = select( editorStore ).getCurrentPostType(); @@ -24,15 +23,14 @@ export default function ViewLink() { permalink: select( editorStore ).getPermalink(), isPublished: select( editorStore ).isCurrentPostPublished(), label: postType?.labels.view_item, - showIconLabels: - select( editPostStore ).isFeatureActive( 'showIconLabels' ), + hasLoaded: !! postType, }; }, [] ); // Only render the view button if the post is published and has a permalink. - if ( ! isPublished || ! permalink ) { + if ( ! isPublished || ! permalink || ! hasLoaded ) { return null; } diff --git a/packages/editor/src/private-apis.js b/packages/editor/src/private-apis.js index da86d138bb2fd8..fab84cdd53946c 100644 --- a/packages/editor/src/private-apis.js +++ b/packages/editor/src/private-apis.js @@ -7,6 +7,7 @@ import { lock } from './lock-unlock'; import { EntitiesSavedStatesExtensible } from './components/entities-saved-states'; import useBlockEditorSettings from './components/provider/use-block-editor-settings'; import PostPanelRow from './components/post-panel-row'; +import PostViewLink from './components/post-view-link'; import PreviewDropdown from './components/preview-dropdown'; import PluginPostExcerpt from './components/post-excerpt/plugin'; @@ -16,6 +17,7 @@ lock( privateApis, { ExperimentalEditorProvider, EntitiesSavedStatesExtensible, PostPanelRow, + PostViewLink, PreviewDropdown, PluginPostExcerpt, From 3a0016bf17758ee16fb39c7bc88918eb02e73bee Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Tue, 19 Dec 2023 09:56:16 +0000 Subject: [PATCH 257/325] DataViews: Add "see revisions" action to templates. (#57175) --- .../src/components/page-templates/index.js | 2 ++ .../page-templates/template-actions.js | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index f534768a237c42..a41f08e1754b8d 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -45,6 +45,7 @@ import { useResetTemplateAction, deleteTemplateAction, renameTemplateAction, + seeRevisionsAction, } from './template-actions'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; @@ -332,6 +333,7 @@ export default function DataviewsTemplates() { resetTemplateAction, deleteTemplateAction, renameTemplateAction, + seeRevisionsAction, ], [ resetTemplateAction ] ); diff --git a/packages/edit-site/src/components/page-templates/template-actions.js b/packages/edit-site/src/components/page-templates/template-actions.js index 9f5897e31fb93e..b9ea4474d34ef6 100644 --- a/packages/edit-site/src/components/page-templates/template-actions.js +++ b/packages/edit-site/src/components/page-templates/template-actions.js @@ -15,6 +15,7 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; +import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -207,3 +208,23 @@ export const renameTemplateAction = { ); }, }; + +export const seeRevisionsAction = { + id: 'see-revisions', + label: __( 'See revisions' ), + isEligible: ( template ) => { + if ( template?._links && template?._links[ 'predecessor-version' ] ) { + const predecessorVersions = + template._links[ 'predecessor-version' ]; + return predecessorVersions.length > 0; + } + return false; + }, + callback( template ) { + const lastRevisionId = + template?._links[ 'predecessor-version' ][ 0 ].id; + document.location.href = addQueryArgs( 'revision.php', { + revision: lastRevisionId, + } ); + }, +}; From 9b800eb7a3967f563cb908d45800c3a7fa04167f Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Tue, 19 Dec 2023 09:58:52 +0000 Subject: [PATCH 258/325] DataViews: make secondary actions trigger always visible. (#57174) --- packages/dataviews/src/item-actions.js | 34 ++++++++++++-------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js index 1b0bd5f213ca8e..17690f0064112a 100644 --- a/packages/dataviews/src/item-actions.js +++ b/packages/dataviews/src/item-actions.js @@ -120,9 +120,6 @@ export default function ItemActions( { item, actions, isCompact } ) { { primaryActions: [], secondaryActions: [] } ); }, [ actions, item ] ); - if ( ! primaryActions.length && ! secondaryActions.length ) { - return null; - } if ( isCompact ) { return ( <CompactItemActions @@ -161,23 +158,22 @@ export default function ItemActions( { item, actions, isCompact } ) { /> ); } ) } - { !! secondaryActions.length && ( - <DropdownMenu - trigger={ - <Button - size="compact" - icon={ moreVertical } - label={ __( 'Actions' ) } - /> - } - placement="bottom-end" - > - <ActionsDropdownMenuGroup - actions={ secondaryActions } - item={ item } + <DropdownMenu + trigger={ + <Button + size="compact" + icon={ moreVertical } + label={ __( 'Actions' ) } + disabled={ ! secondaryActions.length } /> - </DropdownMenu> - ) } + } + placement="bottom-end" + > + <ActionsDropdownMenuGroup + actions={ secondaryActions } + item={ item } + /> + </DropdownMenu> </HStack> ); } From 7339ba620b32ac72aace92392a411c5fb2fe3b31 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Tue, 19 Dec 2023 12:05:10 +0200 Subject: [PATCH 259/325] Font size picker: Fix Reset button focus loss (#57196) --- packages/components/CHANGELOG.md | 8 +++----- packages/components/src/font-size-picker/index.tsx | 13 +++++++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 01a073567e2f22..9614494f0f0e07 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,18 +4,16 @@ ### Bug Fix +- `FontSizePicker`: Fix Reset button focus loss ([#57196](https://github.com/WordPress/gutenberg/pull/57196)). - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). +- `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). +- `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). ### Experimental - `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). - `Tabs`: make sure `Tab`s are associated to the right `TabPanel`s, regardless of the order they're rendered in ([#57033](https://github.com/WordPress/gutenberg/pull/57033)). -### Bug Fix - -- `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). -- `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). - ## 25.14.0 (2023-12-13) ### Enhancements diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index d79bc870d33588..9d977f43db9597 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -123,6 +123,7 @@ const UnforwardedFontSizePicker = ( ); const isValueUnitRelative = !! valueUnit && [ 'em', 'rem' ].includes( valueUnit ); + const isDisabled = value === undefined; return ( <Container ref={ ref } className="components-font-size-picker"> @@ -276,10 +277,14 @@ const UnforwardedFontSizePicker = ( { withReset && ( <FlexItem> <Button - disabled={ value === undefined } - onClick={ () => { - onChange?.( undefined ); - } } + aria-disabled={ isDisabled } + onClick={ + isDisabled + ? undefined + : () => { + onChange?.( undefined ); + } + } variant="secondary" __next40pxDefaultSize size={ From 5405d5539d312688a80ebad69c7d743144392d6c Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 19 Dec 2023 12:09:50 +0100 Subject: [PATCH 260/325] Commands: Allow disabling and enabling comments (#57205) --- packages/commands/CHANGELOG.md | 4 ++++ packages/commands/src/hooks/use-command-loader.js | 4 ++++ packages/commands/src/hooks/use-command.js | 4 ++++ packages/commands/src/store/actions.js | 8 +++++--- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/commands/CHANGELOG.md b/packages/commands/CHANGELOG.md index f162c1d28e4919..8c697a90b20bee 100644 --- a/packages/commands/CHANGELOG.md +++ b/packages/commands/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +## Enhancements + +- Support conditional commands and commands loaders using the "disabled" config. + ## 0.19.0 (2023-12-13) ## 0.18.0 (2023-11-29) diff --git a/packages/commands/src/hooks/use-command-loader.js b/packages/commands/src/hooks/use-command-loader.js index 2c554cf17d974a..5143cbb0d5ef20 100644 --- a/packages/commands/src/hooks/use-command-loader.js +++ b/packages/commands/src/hooks/use-command-loader.js @@ -82,6 +82,9 @@ export default function useCommandLoader( loader ) { const { registerCommandLoader, unregisterCommandLoader } = useDispatch( commandsStore ); useEffect( () => { + if ( loader.disabled ) { + return; + } registerCommandLoader( { name: loader.name, hook: loader.hook, @@ -94,6 +97,7 @@ export default function useCommandLoader( loader ) { loader.name, loader.hook, loader.context, + loader.disabled, registerCommandLoader, unregisterCommandLoader, ] ); diff --git a/packages/commands/src/hooks/use-command.js b/packages/commands/src/hooks/use-command.js index 87092712072c4b..416ffbd7f73c68 100644 --- a/packages/commands/src/hooks/use-command.js +++ b/packages/commands/src/hooks/use-command.js @@ -38,6 +38,9 @@ export default function useCommand( command ) { }, [ command.callback ] ); useEffect( () => { + if ( command.disabled ) { + return; + } registerCommand( { name: command.name, context: command.context, @@ -55,6 +58,7 @@ export default function useCommand( command ) { command.searchLabel, command.icon, command.context, + command.disabled, registerCommand, unregisterCommand, ] ); diff --git a/packages/commands/src/store/actions.js b/packages/commands/src/store/actions.js index f6d9105dd2b906..2926bb84ab6079 100644 --- a/packages/commands/src/store/actions.js +++ b/packages/commands/src/store/actions.js @@ -11,6 +11,7 @@ * @property {string=} context Command context. * @property {JSX.Element} icon Command icon. * @property {Function} callback Command callback. + * @property {boolean} disabled Whether to disable the command. */ /** @@ -22,9 +23,10 @@ * * @typedef {Object} WPCommandLoaderConfig * - * @property {string} name Command loader name. - * @property {string=} context Command loader context. - * @property {WPCommandLoaderHook} hook Command loader hook. + * @property {string} name Command loader name. + * @property {string=} context Command loader context. + * @property {WPCommandLoaderHook} hook Command loader hook. + * @property {boolean} disabled Whether to disable the command loader. */ /** From 17d8f0d0b5c7ad80a18e4b49b49c38292856ed94 Mon Sep 17 00:00:00 2001 From: Jorge Costa <jorge.costa@developer.pt> Date: Tue, 19 Dec 2023 11:20:06 +0000 Subject: [PATCH 261/325] Code Quality: Update: Reuse view revisions action on templates and pages. (#57208) --- .../src/components/page-templates/index.js | 4 ++-- .../page-templates/template-actions.js | 21 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index a41f08e1754b8d..30e7797ec0b2d2 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -45,8 +45,8 @@ import { useResetTemplateAction, deleteTemplateAction, renameTemplateAction, - seeRevisionsAction, } from './template-actions'; +import { postRevisionsAction } from '../actions'; import usePatternSettings from '../page-patterns/use-pattern-settings'; import { unlock } from '../../lock-unlock'; import PostPreview from '../post-preview'; @@ -333,7 +333,7 @@ export default function DataviewsTemplates() { resetTemplateAction, deleteTemplateAction, renameTemplateAction, - seeRevisionsAction, + postRevisionsAction, ], [ resetTemplateAction ] ); diff --git a/packages/edit-site/src/components/page-templates/template-actions.js b/packages/edit-site/src/components/page-templates/template-actions.js index b9ea4474d34ef6..9f5897e31fb93e 100644 --- a/packages/edit-site/src/components/page-templates/template-actions.js +++ b/packages/edit-site/src/components/page-templates/template-actions.js @@ -15,7 +15,6 @@ import { __experimentalHStack as HStack, __experimentalVStack as VStack, } from '@wordpress/components'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies @@ -208,23 +207,3 @@ export const renameTemplateAction = { ); }, }; - -export const seeRevisionsAction = { - id: 'see-revisions', - label: __( 'See revisions' ), - isEligible: ( template ) => { - if ( template?._links && template?._links[ 'predecessor-version' ] ) { - const predecessorVersions = - template._links[ 'predecessor-version' ]; - return predecessorVersions.length > 0; - } - return false; - }, - callback( template ) { - const lastRevisionId = - template?._links[ 'predecessor-version' ][ 0 ].id; - document.location.href = addQueryArgs( 'revision.php', { - revision: lastRevisionId, - } ); - }, -}; From 62578e4c348f1c672881cab54d9c791710dc5052 Mon Sep 17 00:00:00 2001 From: George Mamadashvili <georgemamadashvili@gmail.com> Date: Tue, 19 Dec 2023 15:23:16 +0400 Subject: [PATCH 262/325] Tag Cloud: Replace 'withSelect' HoC with 'useSelect' (#57194) --- packages/block-library/src/tag-cloud/edit.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/tag-cloud/edit.js b/packages/block-library/src/tag-cloud/edit.js index a52c68bc6a7390..75ff3709522fbd 100644 --- a/packages/block-library/src/tag-cloud/edit.js +++ b/packages/block-library/src/tag-cloud/edit.js @@ -13,7 +13,7 @@ import { __experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue, Disabled, } from '@wordpress/components'; -import { withSelect } from '@wordpress/data'; +import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; import { InspectorControls, @@ -40,7 +40,7 @@ const MAX_TAGS = 100; const MIN_FONT_SIZE = 0.1; const MAX_FONT_SIZE = 100; -function TagCloudEdit( { attributes, setAttributes, taxonomies } ) { +function TagCloudEdit( { attributes, setAttributes } ) { const { taxonomy, showTagCounts, @@ -53,6 +53,10 @@ function TagCloudEdit( { attributes, setAttributes, taxonomies } ) { const units = useCustomUnits( { availableUnits: availableUnits || [ '%', 'px', 'em', 'rem' ], } ); + const taxonomies = useSelect( + ( select ) => select( coreStore ).getTaxonomies( { per_page: -1 } ), + [] + ); const getTaxonomyOptions = () => { const selectOption = { @@ -174,8 +178,4 @@ function TagCloudEdit( { attributes, setAttributes, taxonomies } ) { ); } -export default withSelect( ( select ) => { - return { - taxonomies: select( coreStore ).getTaxonomies( { per_page: -1 } ), - }; -} )( TagCloudEdit ); +export default TagCloudEdit; From 009cf9d2fbbb2ba4dc17325cb8f670e4957169f5 Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Tue, 19 Dec 2023 11:31:38 +0000 Subject: [PATCH 263/325] Dataviews: Simplify pagination (#57071) * Pagination layout * styles * Tooltip position * Styling * Hide pagination * remove leftover character --------- Co-authored-by: ntsekouras <ntsekouras@outlook.com> --- packages/dataviews/src/pagination.js | 195 +++++++++++---------------- packages/dataviews/src/style.scss | 3 +- 2 files changed, 78 insertions(+), 120 deletions(-) diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index 1c41691a13d0a3..97f38553b0bf99 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -4,12 +4,11 @@ import { Button, __experimentalHStack as HStack, - __experimentalText as Text, __experimentalNumberControl as NumberControl, } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; -import { sprintf, __, _x, _n } from '@wordpress/i18n'; -import { chevronRight, chevronLeft, previous, next } from '@wordpress/icons'; +import { sprintf, __, _x } from '@wordpress/i18n'; +import { chevronRight, chevronLeft } from '@wordpress/icons'; function Pagination( { view, @@ -20,124 +19,82 @@ function Pagination( { return null; } return ( - <HStack - expanded={ false } - spacing={ 3 } - justify="space-between" - className="dataviews-pagination" - > - <Text variant="muted"> - { - // translators: %s: Total number of entries. - sprintf( - // translators: %s: Total number of entries. - _n( '%s item', '%s items', totalItems ), - totalItems - ) - } - </Text> - { !! totalItems && totalPages !== 1 && ( - <HStack expanded={ false } spacing={ 3 }> - <HStack - justify="flex-start" - expanded={ false } - spacing={ 1 } - > - <Button - onClick={ () => - onChangeView( { ...view, page: 1 } ) - } - disabled={ view.page === 1 } - __experimentalIsFocusable - label={ __( 'First page' ) } - icon={ previous } - showTooltip - size="compact" - /> - <Button - onClick={ () => - onChangeView( { ...view, page: view.page - 1 } ) - } - disabled={ view.page === 1 } - __experimentalIsFocusable - label={ __( 'Previous page' ) } - icon={ chevronLeft } - showTooltip - size="compact" - /> - </HStack> - <HStack - justify="flex-start" - expanded={ false } - spacing={ 2 } - > - { createInterpolateElement( - sprintf( - // translators: %1$s: Current page number, %2$s: Total number of pages. - _x( '<CurrenPageControl /> of %2$s', 'paging' ), - view.page, - totalPages + !! totalItems && + totalPages !== 1 && ( + <HStack + expanded={ false } + spacing={ 3 } + justify="space-between" + className="dataviews-pagination" + > + <HStack justify="flex-start" expanded={ false } spacing={ 2 }> + { createInterpolateElement( + sprintf( + // translators: %1$s: Current page number, %2$s: Total number of pages. + _x( + 'Page <CurrenPageControl /> of %2$s', + 'paging' ), - { - CurrenPageControl: ( - <NumberControl - aria-label={ __( 'Current page' ) } - min={ 1 } - max={ totalPages } - onChange={ ( value ) => { - const _value = +value; - if ( - ! _value || - _value < 1 || - _value > totalPages - ) { - return; - } - onChangeView( { - ...view, - page: _value, - } ); - } } - step="1" - value={ view.page } - isDragEnabled={ false } - spinControls="none" - /> - ), - } - ) } - </HStack> - <HStack - justify="flex-start" - expanded={ false } - spacing={ 1 } - > - <Button - onClick={ () => - onChangeView( { ...view, page: view.page + 1 } ) - } - disabled={ view.page >= totalPages } - __experimentalIsFocusable - label={ __( 'Next page' ) } - icon={ chevronRight } - showTooltip - size="compact" - /> - <Button - onClick={ () => - onChangeView( { ...view, page: totalPages } ) - } - disabled={ view.page >= totalPages } - __experimentalIsFocusable - label={ __( 'Last page' ) } - icon={ next } - showTooltip - size="compact" - /> - </HStack> + view.page, + totalPages + ), + { + CurrenPageControl: ( + <NumberControl + aria-label={ __( 'Current page' ) } + min={ 1 } + max={ totalPages } + onChange={ ( value ) => { + const _value = +value; + if ( + ! _value || + _value < 1 || + _value > totalPages + ) { + return; + } + onChangeView( { + ...view, + page: _value, + } ); + } } + step="1" + value={ view.page } + isDragEnabled={ false } + spinControls="none" + /> + ), + } + ) } + </HStack> + <HStack expanded={ false } spacing={ 1 }> + <Button + onClick={ () => + onChangeView( { ...view, page: view.page - 1 } ) + } + disabled={ view.page === 1 } + __experimentalIsFocusable + label={ __( 'Previous page' ) } + icon={ chevronLeft } + showTooltip + size="compact" + tooltipPosition="top" + /> + <Button + onClick={ () => + onChangeView( { ...view, page: view.page + 1 } ) + } + disabled={ view.page >= totalPages } + __experimentalIsFocusable + label={ __( 'Next page' ) } + icon={ chevronRight } + showTooltip + size="compact" + tooltipPosition="top" + /> </HStack> - ) } - </HStack> + </HStack> + ) ); } diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index f5e476ecebd15e..b35e11deae7f4f 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -44,7 +44,8 @@ margin-top: auto; position: sticky; bottom: 0; - background-color: $white; + background-color: rgba($white, 0.8); + backdrop-filter: blur(6px); padding: $grid-unit-15 $grid-unit-40; border-top: $border-width solid $gray-100; color: $gray-700; From caf213c92161d318489ce8d7464df47eb69a5621 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr <jsnajdr@gmail.com> Date: Tue, 19 Dec 2023 12:37:32 +0100 Subject: [PATCH 264/325] useInputRules: remove unneeded check for inputRule (#57164) * useInputRules: remove unneeded check for inputRule * Merge conditionals into one Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> --------- Co-authored-by: Marin Atanasov <8436925+tyxla@users.noreply.github.com> --- .../block-editor/src/components/rich-text/use-input-rules.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/use-input-rules.js b/packages/block-editor/src/components/rich-text/use-input-rules.js index 5640a85f5f2695..37e61997f14986 100644 --- a/packages/block-editor/src/components/rich-text/use-input-rules.js +++ b/packages/block-editor/src/components/rich-text/use-input-rules.js @@ -142,8 +142,8 @@ export function useInputRules( props ) { return; } - if ( __unstableAllowPrefixTransformations && inputRule ) { - if ( inputRule() ) return; + if ( __unstableAllowPrefixTransformations && inputRule() ) { + return; } const value = getValue(); From 8ba81732f6c29a14c3ce074434dec89ea9139546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Tue, 19 Dec 2023 12:43:53 +0100 Subject: [PATCH 265/325] DataViews: align filter implementations (#57059) --- packages/dataviews/src/add-filter.js | 134 ++++++++++------------- packages/dataviews/src/filter-summary.js | 77 ++++++------- packages/dataviews/src/view-table.js | 112 +++++++++---------- 3 files changed, 147 insertions(+), 176 deletions(-) diff --git a/packages/dataviews/src/add-filter.js b/packages/dataviews/src/add-filter.js index 8df218c0603703..c403baa9c13d34 100644 --- a/packages/dataviews/src/add-filter.js +++ b/packages/dataviews/src/add-filter.js @@ -73,6 +73,9 @@ export default function AddFilter( { filters, view, onChangeView } ) { const filterInView = view.filters.find( ( f ) => f.field === filter.field ); + const otherFilters = view.filters.filter( + ( f ) => f.field !== filter.field + ); const activeElement = filter.elements.find( ( element ) => element.value === filterInView?.value ); @@ -107,50 +110,45 @@ export default function AddFilter( { filters, view, onChangeView } ) { > <WithSeparators> <DropdownMenuGroup> - { filter.elements.map( ( element ) => ( - <DropdownMenuItem - key={ element.value } - role="menuitemradio" - aria-checked={ - activeElement?.value === - element.value - } - prefix={ - activeElement?.value === - element.value && ( - <Icon icon={ check } /> - ) - } - onSelect={ ( event ) => { - event.preventDefault(); - onChangeView( - ( currentView ) => ( { - ...currentView, + { filter.elements.map( ( element ) => { + const isActive = + activeElement?.value === + element.value; + return ( + <DropdownMenuItem + key={ element.value } + role="menuitemradio" + aria-checked={ isActive } + prefix={ + isActive && ( + <Icon + icon={ check } + /> + ) + } + onSelect={ ( event ) => { + event.preventDefault(); + onChangeView( { + ...view, page: 1, filters: [ - ...currentView.filters.filter( - ( f ) => - f.field !== - filter.field - ), + ...otherFilters, { field: filter.field, operator: activeOperator, - value: - activeElement?.value === - element.value - ? undefined - : element.value, + value: isActive + ? undefined + : element.value, }, ], - } ) - ); - } } - > - { element.label } - </DropdownMenuItem> - ) ) } + } ); + } } + > + { element.label } + </DropdownMenuItem> + ); + } ) } </DropdownMenuGroup> { filter.operators.length > 1 && ( <DropdownSubMenu @@ -191,25 +189,19 @@ export default function AddFilter( { filters, view, onChangeView } ) { } onSelect={ ( event ) => { event.preventDefault(); - onChangeView( - ( currentView ) => ( { - ...currentView, - page: 1, - filters: [ - ...view.filters.filter( - ( f ) => - f.field !== - filter.field - ), - { - field: filter.field, - operator: - OPERATOR_IN, - value: filterInView?.value, - }, - ], - } ) - ); + onChangeView( { + ...view, + page: 1, + filters: [ + ...otherFilters, + { + field: filter.field, + operator: + OPERATOR_IN, + value: filterInView?.value, + }, + ], + } ); } } > { __( 'Is' ) } @@ -229,25 +221,19 @@ export default function AddFilter( { filters, view, onChangeView } ) { } onSelect={ ( event ) => { event.preventDefault(); - onChangeView( - ( currentView ) => ( { - ...currentView, - page: 1, - filters: [ - ...view.filters.filter( - ( f ) => - f.field !== - filter.field - ), - { - field: filter.field, - operator: - OPERATOR_NOT_IN, - value: filterInView?.value, - }, - ], - } ) - ); + onChangeView( { + ...view, + page: 1, + filters: [ + ...otherFilters, + { + field: filter.field, + operator: + OPERATOR_NOT_IN, + value: filterInView?.value, + }, + ], + } ); } } > { __( 'Is not' ) } diff --git a/packages/dataviews/src/filter-summary.js b/packages/dataviews/src/filter-summary.js index 3c30c6837103a7..098e73db5eeb30 100644 --- a/packages/dataviews/src/filter-summary.js +++ b/packages/dataviews/src/filter-summary.js @@ -74,9 +74,13 @@ function WithSeparators( { children } ) { export default function FilterSummary( { filter, view, onChangeView } ) { const filterInView = view.filters.find( ( f ) => f.field === filter.field ); + const otherFilters = view.filters.filter( + ( f ) => f.field !== filter.field + ); const activeElement = filter.elements.find( ( element ) => element.value === filterInView?.value ); + const activeOperator = filterInView?.operator || filter.operators[ 0 ]; return ( <DropdownMenu @@ -95,40 +99,28 @@ export default function FilterSummary( { filter, view, onChangeView } ) { <WithSeparators> <DropdownMenuGroup> { filter.elements.map( ( element ) => { + const isActive = activeElement?.value === element.value; return ( <DropdownMenuItem key={ element.value } role="menuitemradio" - aria-checked={ - activeElement?.value === element.value - } - prefix={ - activeElement?.value === element.value && ( - <Icon icon={ check } /> - ) - } + aria-checked={ isActive } + prefix={ isActive && <Icon icon={ check } /> } onSelect={ () => - onChangeView( ( currentView ) => ( { - ...currentView, + onChangeView( { + ...view, page: 1, filters: [ - ...view.filters.filter( - ( f ) => - f.field !== filter.field - ), + ...otherFilters, { field: filter.field, - operator: - filterInView?.operator || - filter.operators[ 0 ], - value: - activeElement?.value === - element.value - ? undefined - : element.value, + operator: activeOperator, + value: isActive + ? undefined + : element.value, }, ], - } ) ) + } ) } > { element.label } @@ -142,9 +134,10 @@ export default function FilterSummary( { filter, view, onChangeView } ) { <DropdownSubMenuTrigger suffix={ <> - { filterInView.operator === OPERATOR_IN - ? __( 'Is' ) - : __( 'Is not' ) } + { activeOperator === OPERATOR_IN && + __( 'Is' ) } + { activeOperator === OPERATOR_NOT_IN && + __( 'Is not' ) } <Icon icon={ chevronRightSmall } />{ ' ' } </> } @@ -156,29 +149,25 @@ export default function FilterSummary( { filter, view, onChangeView } ) { <DropdownMenuItem key="in-filter" role="menuitemradio" - aria-checked={ - filterInView?.operator === OPERATOR_IN - } + aria-checked={ activeOperator === OPERATOR_IN } prefix={ - filterInView?.operator === OPERATOR_IN && ( + activeOperator === OPERATOR_IN && ( <Icon icon={ check } /> ) } onSelect={ () => - onChangeView( ( currentView ) => ( { - ...currentView, + onChangeView( { + ...view, page: 1, filters: [ - ...view.filters.filter( - ( f ) => f.field !== filter.field - ), + ...otherFilters, { field: filter.field, operator: OPERATOR_IN, value: filterInView?.value, }, ], - } ) ) + } ) } > { __( 'Is' ) } @@ -186,29 +175,25 @@ export default function FilterSummary( { filter, view, onChangeView } ) { <DropdownMenuItem key="not-in-filter" role="menuitemradio" - aria-checked={ - filterInView?.operator === OPERATOR_NOT_IN - } + aria-checked={ activeOperator === OPERATOR_NOT_IN } prefix={ - filterInView?.operator === OPERATOR_NOT_IN && ( + activeOperator === OPERATOR_NOT_IN && ( <Icon icon={ check } /> ) } onSelect={ () => - onChangeView( ( currentView ) => ( { - ...currentView, + onChangeView( { + ...view, page: 1, filters: [ - ...view.filters.filter( - ( f ) => f.field !== filter.field - ), + ...otherFilters, { field: filter.field, operator: OPERATOR_NOT_IN, value: filterInView?.value, }, ], - } ) ) + } ) } > { __( 'Is not' ) } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index c5323bebea0ef5..e08449b76491df 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -40,32 +40,36 @@ const sortingItemsInfo = { }; const sortArrows = { asc: '↑', desc: '↓' }; +const sanitizeOperators = ( field ) => { + let operators = field.filterBy?.operators; + if ( ! operators || ! Array.isArray( operators ) ) { + operators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; + } + return operators.filter( ( operator ) => + [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + ); +}; + function HeaderMenu( { field, view, onChangeView } ) { - const isSortable = field.enableSorting !== false; const isHidable = field.enableHiding !== false; + + const isSortable = field.enableSorting !== false; const isSorted = view.sort?.field === field.id; - let filter, filterInView; - const otherFilters = []; - if ( field.type === ENUMERATION_TYPE ) { - let columnOperators = field.filterBy?.operators; - if ( ! columnOperators || ! Array.isArray( columnOperators ) ) { - columnOperators = [ OPERATOR_IN, OPERATOR_NOT_IN ]; - } - const operators = columnOperators.filter( ( operator ) => - [ OPERATOR_IN, OPERATOR_NOT_IN ].includes( operator ) + + let filter, filterInView, activeElement, activeOperator, otherFilters; + const operators = sanitizeOperators( field ); + if ( field.type === ENUMERATION_TYPE && operators.length > 0 ) { + filter = { + field: field.id, + operators, + elements: field.elements || [], + }; + filterInView = view.filters.find( ( f ) => f.field === filter.field ); + otherFilters = view.filters.filter( ( f ) => f.field !== filter.field ); + activeElement = filter.elements.find( + ( element ) => element.value === filterInView?.value ); - if ( operators.length > 0 ) { - filter = { - field: field.id, - operators, - elements: field.elements || [], - }; - filterInView = { - field: filter.field, - operator: filter.operators[ 0 ], - value: undefined, - }; - } + activeOperator = filterInView?.operator || filter.operators[ 0 ]; } const isFilterable = !! filter; @@ -73,18 +77,6 @@ function HeaderMenu( { field, view, onChangeView } ) { return field.header; } - if ( isFilterable ) { - const columnFilters = view.filters; - columnFilters.forEach( ( columnFilter ) => { - if ( columnFilter.field === filter.field ) { - filterInView = { - ...columnFilter, - }; - } else { - otherFilters.push( columnFilter ); - } - } ); - } return ( <DropdownMenu align="start" @@ -164,7 +156,19 @@ function HeaderMenu( { field, view, onChangeView } ) { <DropdownSubMenuTrigger prefix={ <Icon icon={ funnel } /> } suffix={ - <Icon icon={ chevronRightSmall } /> + <> + { activeElement && + activeOperator === + OPERATOR_IN && + __( 'Is' ) } + { activeElement && + activeOperator === + OPERATOR_NOT_IN && + __( 'Is not' ) } + { activeElement && ' ' } + { activeElement?.label } + <Icon icon={ chevronRightSmall } /> + </> } > { __( 'Filter by' ) } @@ -174,17 +178,9 @@ function HeaderMenu( { field, view, onChangeView } ) { <WithSeparators> <DropdownMenuGroup> { filter.elements.map( ( element ) => { - let isActive = false; - if ( filterInView ) { - // Intentionally use loose comparison, so it does type conversion. - // This covers the case where a top-level filter for the same field converts a number into a string. - /* eslint-disable eqeqeq */ - isActive = - element.value == - filterInView.value; - /* eslint-enable eqeqeq */ - } - + const isActive = + activeElement?.value === + element.value; return ( <DropdownMenuItem key={ element.value } @@ -198,12 +194,13 @@ function HeaderMenu( { field, view, onChangeView } ) { onSelect={ () => { onChangeView( { ...view, + page: 1, filters: [ ...otherFilters, { field: filter.field, operator: - filterInView?.operator, + activeOperator, value: isActive ? undefined : element.value, @@ -223,10 +220,12 @@ function HeaderMenu( { field, view, onChangeView } ) { <DropdownSubMenuTrigger suffix={ <> - { filterInView.operator === - OPERATOR_IN - ? __( 'Is' ) - : __( 'Is not' ) } + { activeOperator === + OPERATOR_IN && + __( 'Is' ) } + { activeOperator === + OPERATOR_NOT_IN && + __( 'Is not' ) } <Icon icon={ chevronRightSmall @@ -243,11 +242,10 @@ function HeaderMenu( { field, view, onChangeView } ) { key="in-filter" role="menuitemradio" aria-checked={ - filterInView?.operator === - OPERATOR_IN + activeOperator === OPERATOR_IN } prefix={ - filterInView?.operator === + activeOperator === OPERATOR_IN && ( <Icon icon={ check } /> ) @@ -255,6 +253,7 @@ function HeaderMenu( { field, view, onChangeView } ) { onSelect={ () => onChangeView( { ...view, + page: 1, filters: [ ...otherFilters, { @@ -273,11 +272,11 @@ function HeaderMenu( { field, view, onChangeView } ) { key="not-in-filter" role="menuitemradio" aria-checked={ - filterInView?.operator === + activeOperator === OPERATOR_NOT_IN } prefix={ - filterInView?.operator === + activeOperator === OPERATOR_NOT_IN && ( <Icon icon={ check } /> ) @@ -285,6 +284,7 @@ function HeaderMenu( { field, view, onChangeView } ) { onSelect={ () => onChangeView( { ...view, + page: 1, filters: [ ...otherFilters, { From e31781a697d34846aeead62203aab22a8e7aeafb Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Tue, 19 Dec 2023 12:45:38 +0100 Subject: [PATCH 266/325] ListView: Replace prop drilldown by a stable ref in store (#57198) --- package-lock.json | 6 ++- .../components/header/header-toolbar/index.js | 9 ++-- .../edit-post/src/components/header/index.js | 10 +---- .../edit-post/src/components/layout/index.js | 10 +---- .../secondary-sidebar/list-view-sidebar.js | 10 +++-- .../edit-site/src/components/editor/index.js | 10 +---- .../header-edit-mode/document-tools/index.js | 41 ++++++++++--------- .../src/components/header-edit-mode/index.js | 3 +- .../edit-site/src/components/layout/index.js | 11 +---- .../secondary-sidebar/list-view-sidebar.js | 9 ++-- packages/edit-widgets/package.json | 3 +- .../components/header/document-tools/index.js | 24 ++++++----- .../src/components/header/index.js | 6 +-- .../src/components/layout/interface.js | 17 ++------ .../src/components/secondary-sidebar/index.js | 6 +-- .../secondary-sidebar/list-view-sidebar.js | 10 +++-- packages/edit-widgets/src/store/index.js | 4 ++ .../src/store/private-selectors.js | 16 ++++++++ .../editor/src/store/private-selectors.js | 13 ++++++ 19 files changed, 112 insertions(+), 106 deletions(-) create mode 100644 packages/edit-widgets/src/store/private-selectors.js diff --git a/package-lock.json b/package-lock.json index c01f19ad332288..7425d75403ad10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55359,7 +55359,8 @@ "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/url": "file:../url", "@wordpress/widgets": "file:../widgets", - "classnames": "^2.3.1" + "classnames": "^2.3.1", + "rememo": "^4.0.2" }, "engines": { "node": ">=12" @@ -70610,7 +70611,8 @@ "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/url": "file:../url", "@wordpress/widgets": "file:../widgets", - "classnames": "^2.3.1" + "classnames": "^2.3.1", + "rememo": "^4.0.2" } }, "@wordpress/editor": { diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index d585102c06db92..c524a8842ad4cc 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -32,7 +32,7 @@ const preventDefault = ( event ) => { event.preventDefault(); }; -function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { +function HeaderToolbar( { hasFixedToolbar } ) { const inserterButton = useRef(); const { setIsInserterOpened, setIsListViewOpened } = useDispatch( editorStore ); @@ -43,10 +43,12 @@ function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { showIconLabels, isListViewOpen, listViewShortcut, + listViewToggleRef, } = useSelect( ( select ) => { const { hasInserterItems, getBlockRootClientId, getBlockSelectionEnd } = select( blockEditorStore ); - const { getEditorSettings, isListViewOpened } = select( editorStore ); + const { getEditorSettings, isListViewOpened, getListViewToggleRef } = + unlock( select( editorStore ) ); const { getEditorMode, isFeatureActive } = select( editPostStore ); const { getShortcutRepresentation } = select( keyboardShortcutsStore ); @@ -65,6 +67,7 @@ function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { listViewShortcut: getShortcutRepresentation( 'core/edit-post/toggle-list-view' ), + listViewToggleRef: getListViewToggleRef(), }; }, [] ); @@ -103,7 +106,7 @@ function HeaderToolbar( { hasFixedToolbar, setListViewToggleElement } ) { showTooltip={ ! showIconLabels } variant={ showIconLabels ? 'tertiary' : undefined } aria-expanded={ isListViewOpen } - ref={ setListViewToggleElement } + ref={ listViewToggleRef } size="compact" /> </> diff --git a/packages/edit-post/src/components/header/index.js b/packages/edit-post/src/components/header/index.js index 67349bd6404038..1e0a1f31956130 100644 --- a/packages/edit-post/src/components/header/index.js +++ b/packages/edit-post/src/components/header/index.js @@ -55,10 +55,7 @@ const slideX = { hover: { x: 0, transition: { type: 'tween', delay: 0.2 } }, }; -function Header( { - setEntitiesSavedStatesCallback, - setListViewToggleElement, -} ) { +function Header( { setEntitiesSavedStatesCallback } ) { const isWideViewport = useViewportMatch( 'large' ); const isLargeViewport = useViewportMatch( 'medium' ); const blockToolbarRef = useRef(); @@ -111,10 +108,7 @@ function Header( { transition={ { type: 'tween', delay: 0.8 } } className="edit-post-header__toolbar" > - <HeaderToolbar - hasFixedToolbar={ hasFixedToolbar } - setListViewToggleElement={ setListViewToggleElement } - /> + <HeaderToolbar hasFixedToolbar={ hasFixedToolbar } /> { hasFixedToolbar && isLargeViewport && ( <> <div diff --git a/packages/edit-post/src/components/layout/index.js b/packages/edit-post/src/components/layout/index.js index 3895c2566b1948..70d5554064edf6 100644 --- a/packages/edit-post/src/components/layout/index.js +++ b/packages/edit-post/src/components/layout/index.js @@ -232,9 +232,6 @@ function Layout() { const [ entitiesSavedStatesCallback, setEntitiesSavedStatesCallback ] = useState( false ); - const [ listViewToggleElement, setListViewToggleElement ] = - useState( null ); - const closeEntitiesSavedStates = useCallback( ( arg ) => { if ( typeof entitiesSavedStatesCallback === 'function' ) { @@ -268,11 +265,7 @@ function Layout() { return <InserterSidebar />; } if ( mode === 'visual' && isListViewOpened ) { - return ( - <ListViewSidebar - listViewToggleElement={ listViewToggleElement } - /> - ); + return <ListViewSidebar />; } return null; @@ -313,7 +306,6 @@ function Layout() { setEntitiesSavedStatesCallback={ setEntitiesSavedStatesCallback } - setListViewToggleElement={ setListViewToggleElement } /> } editorNotices={ <EditorNotices /> } diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index fa692d48690046..02690d9115d7ab 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -4,7 +4,7 @@ import { __experimentalListView as ListView } from '@wordpress/block-editor'; import { Button, TabPanel } from '@wordpress/components'; import { useFocusOnMount, useMergeRefs } from '@wordpress/compose'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { focus } from '@wordpress/dom'; import { useCallback, useRef, useState } from '@wordpress/element'; import { __, _x } from '@wordpress/i18n'; @@ -17,9 +17,11 @@ import { store as editorStore } from '@wordpress/editor'; * Internal dependencies */ import ListViewOutline from './list-view-outline'; +import { unlock } from '../../lock-unlock'; -export default function ListViewSidebar( { listViewToggleElement } ) { +export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editorStore ); + const { getListViewToggleRef } = unlock( useSelect( editorStore ) ); // This hook handles focus when the sidebar first renders. const focusOnMountRef = useFocusOnMount( 'firstElement' ); @@ -27,8 +29,8 @@ export default function ListViewSidebar( { listViewToggleElement } ) { // When closing the list view, focus should return to the toggle button. const closeListView = useCallback( () => { setIsListViewOpened( false ); - listViewToggleElement?.focus(); - }, [ listViewToggleElement, setIsListViewOpened ] ); + getListViewToggleRef().current?.focus(); + }, [ getListViewToggleRef, setIsListViewOpened ] ); const closeOnEscape = useCallback( ( event ) => { diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index db92c36d75af74..9a1931b2ede398 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -84,7 +84,7 @@ const blockRemovalRules = { ), }; -export default function Editor( { listViewToggleElement, isLoading } ) { +export default function Editor( { isLoading } ) { const { record: editedPost, getTitle, @@ -251,13 +251,7 @@ export default function Editor( { listViewToggleElement, isLoading } ) { secondarySidebar={ isEditMode && ( ( shouldShowInserter && <InserterSidebar /> ) || - ( shouldShowListView && ( - <ListViewSidebar - listViewToggleElement={ - listViewToggleElement - } - /> - ) ) ) + ( shouldShowListView && <ListViewSidebar /> ) ) } sidebar={ isEditMode && diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index b67f842128e26d..9db8e091265e2f 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -35,27 +35,30 @@ export default function DocumentTools( { hasFixedToolbar, isDistractionFree, showIconLabels, - setListViewToggleElement, } ) { const inserterButton = useRef(); - const { isInserterOpen, isListViewOpen, listViewShortcut, isVisualMode } = - useSelect( ( select ) => { - const { getEditorMode } = select( editSiteStore ); - const { getShortcutRepresentation } = select( - keyboardShortcutsStore - ); - const { isInserterOpened, isListViewOpened } = - select( editorStore ); + const { + isInserterOpen, + isListViewOpen, + listViewShortcut, + isVisualMode, + listViewToggleRef, + } = useSelect( ( select ) => { + const { getEditorMode } = select( editSiteStore ); + const { getShortcutRepresentation } = select( keyboardShortcutsStore ); + const { isInserterOpened, isListViewOpened, getListViewToggleRef } = + unlock( select( editorStore ) ); - return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - listViewShortcut: getShortcutRepresentation( - 'core/edit-site/toggle-list-view' - ), - isVisualMode: getEditorMode() === 'visual', - }; - }, [] ); + return { + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + listViewShortcut: getShortcutRepresentation( + 'core/edit-site/toggle-list-view' + ), + isVisualMode: getEditorMode() === 'visual', + listViewToggleRef: getListViewToggleRef(), + }; + }, [] ); const { __unstableSetEditorMode } = useDispatch( blockEditorStore ); const { setDeviceType, setIsInserterOpened, setIsListViewOpened } = useDispatch( editorStore ); @@ -161,7 +164,7 @@ export default function DocumentTools( { /* translators: button label text should, if possible, be under 16 characters. */ label={ __( 'List View' ) } onClick={ toggleListView } - ref={ setListViewToggleElement } + ref={ listViewToggleRef } shortcut={ listViewShortcut } showTooltip={ ! showIconLabels } variant={ diff --git a/packages/edit-site/src/components/header-edit-mode/index.js b/packages/edit-site/src/components/header-edit-mode/index.js index 4b9d48d797952d..0d24865e74bf6a 100644 --- a/packages/edit-site/src/components/header-edit-mode/index.js +++ b/packages/edit-site/src/components/header-edit-mode/index.js @@ -44,7 +44,7 @@ import { FOCUSABLE_ENTITIES } from '../../utils/constants'; const { PostViewLink, PreviewDropdown } = unlock( editorPrivateApis ); -export default function HeaderEditMode( { setListViewToggleElement } ) { +export default function HeaderEditMode() { const { templateType, isDistractionFree, @@ -137,7 +137,6 @@ export default function HeaderEditMode( { setListViewToggleElement } ) { blockEditorMode={ blockEditorMode } isDistractionFree={ isDistractionFree } showIconLabels={ showIconLabels } - setListViewToggleElement={ setListViewToggleElement } /> { isTopToolbar && ( <> diff --git a/packages/edit-site/src/components/layout/index.js b/packages/edit-site/src/components/layout/index.js index 6d62ba1df07f42..c6ad3cf565b122 100644 --- a/packages/edit-site/src/components/layout/index.js +++ b/packages/edit-site/src/components/layout/index.js @@ -129,8 +129,6 @@ export default function Layout() { const isEditorLoading = useIsSiteEditorLoading(); const [ isResizableFrameOversized, setIsResizableFrameOversized ] = useState( false ); - const [ listViewToggleElement, setListViewToggleElement ] = - useState( null ); // This determines which animation variant should apply to the header. // There is also a `isDistractionFreeHovering` state that gets priority @@ -258,11 +256,7 @@ export default function Layout() { ease: 'easeOut', } } > - <Header - setListViewToggleElement={ - setListViewToggleElement - } - /> + <Header /> </NavigableRegion> ) } </AnimatePresence> @@ -367,9 +361,6 @@ export default function Layout() { } } > <Editor - listViewToggleElement={ - listViewToggleElement - } isLoading={ isEditorLoading } diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index 3ec7814beebe32..3b837ba6a91713 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -4,7 +4,7 @@ import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; import { useFocusOnMount, useMergeRefs } from '@wordpress/compose'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback, useRef, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { closeSmall } from '@wordpress/icons'; @@ -20,8 +20,9 @@ import { unlock } from '../../lock-unlock'; const { PrivateListView } = unlock( blockEditorPrivateApis ); -export default function ListViewSidebar( { listViewToggleElement } ) { +export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editorStore ); + const { getListViewToggleRef } = unlock( useSelect( editorStore ) ); // This hook handles focus when the sidebar first renders. const focusOnMountRef = useFocusOnMount( 'firstElement' ); @@ -29,8 +30,8 @@ export default function ListViewSidebar( { listViewToggleElement } ) { // When closing the list view, focus should return to the toggle button. const closeListView = useCallback( () => { setIsListViewOpened( false ); - listViewToggleElement?.focus(); - }, [ listViewToggleElement, setIsListViewOpened ] ); + getListViewToggleRef().current?.focus(); + }, [ getListViewToggleRef, setIsListViewOpened ] ); const closeOnEscape = useCallback( ( event ) => { diff --git a/packages/edit-widgets/package.json b/packages/edit-widgets/package.json index 1ef98df31269f6..974a0ba02905fe 100644 --- a/packages/edit-widgets/package.json +++ b/packages/edit-widgets/package.json @@ -53,7 +53,8 @@ "@wordpress/reusable-blocks": "file:../reusable-blocks", "@wordpress/url": "file:../url", "@wordpress/widgets": "file:../widgets", - "classnames": "^2.3.1" + "classnames": "^2.3.1", + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js index 8cac7590be5e88..06376bbd762916 100644 --- a/packages/edit-widgets/src/components/header/document-tools/index.js +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -24,7 +24,7 @@ import { unlock } from '../../../lock-unlock'; const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); -function DocumentTools( { setListViewToggleElement } ) { +function DocumentTools() { const isMediumViewport = useViewportMatch( 'medium' ); const inserterButton = useRef(); const widgetAreaClientId = useLastSelectedWidgetArea(); @@ -35,14 +35,18 @@ function DocumentTools( { setListViewToggleElement } ) { ), [ widgetAreaClientId ] ); - const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { - const { isInserterOpened, isListViewOpened } = - select( editWidgetsStore ); - return { - isInserterOpen: isInserterOpened(), - isListViewOpen: isListViewOpened(), - }; - }, [] ); + const { isInserterOpen, isListViewOpen, listViewToggleRef } = useSelect( + ( select ) => { + const { isInserterOpened, isListViewOpened, getListViewToggleRef } = + unlock( select( editWidgetsStore ) ); + return { + isInserterOpen: isInserterOpened(), + isListViewOpen: isListViewOpened(), + listViewToggleRef: getListViewToggleRef(), + }; + }, + [] + ); const { setIsWidgetAreaOpen, setIsInserterOpened, setIsListViewOpened } = useDispatch( editWidgetsStore ); const { selectBlock } = useDispatch( blockEditorStore ); @@ -119,7 +123,7 @@ function DocumentTools( { setListViewToggleElement } ) { /* translators: button label text should, if possible, be under 16 characters. */ label={ __( 'List View' ) } onClick={ toggleListView } - ref={ setListViewToggleElement } + ref={ listViewToggleRef } /> </> ) } diff --git a/packages/edit-widgets/src/components/header/index.js b/packages/edit-widgets/src/components/header/index.js index 9d4cb4cb60103a..0aadec83d5d2f3 100644 --- a/packages/edit-widgets/src/components/header/index.js +++ b/packages/edit-widgets/src/components/header/index.js @@ -17,7 +17,7 @@ import DocumentTools from './document-tools'; import SaveButton from '../save-button'; import MoreMenu from '../more-menu'; -function Header( { setListViewToggleElement } ) { +function Header() { const isLargeViewport = useViewportMatch( 'medium' ); const blockToolbarRef = useRef(); const { hasFixedToolbar } = useSelect( @@ -47,9 +47,7 @@ function Header( { setListViewToggleElement } ) { { __( 'Widgets' ) } </VisuallyHidden> ) } - <DocumentTools - setListViewToggleElement={ setListViewToggleElement } - /> + <DocumentTools /> { hasFixedToolbar && isLargeViewport && ( <> <div className="selected-block-tools-wrapper"> diff --git a/packages/edit-widgets/src/components/layout/interface.js b/packages/edit-widgets/src/components/layout/interface.js index 2cb1eebcfab73b..987e3868de1337 100644 --- a/packages/edit-widgets/src/components/layout/interface.js +++ b/packages/edit-widgets/src/components/layout/interface.js @@ -3,7 +3,7 @@ */ import { useViewportMatch } from '@wordpress/compose'; import { BlockBreadcrumb } from '@wordpress/block-editor'; -import { useEffect, useState } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { InterfaceSkeleton, @@ -68,9 +68,6 @@ function Interface( { blockEditorSettings } ) { [] ); - const [ listViewToggleElement, setListViewToggleElement ] = - useState( null ); - // Inserter and Sidebars are mutually exclusive useEffect( () => { if ( hasSidebarEnabled && ! isHugeViewport ) { @@ -97,16 +94,8 @@ function Interface( { blockEditorSettings } ) { ...interfaceLabels, secondarySidebar: secondarySidebarLabel, } } - header={ - <Header setListViewToggleElement={ setListViewToggleElement } /> - } - secondarySidebar={ - hasSecondarySidebar && ( - <SecondarySidebar - listViewToggleElement={ listViewToggleElement } - /> - ) - } + header={ <Header /> } + secondarySidebar={ hasSecondarySidebar && <SecondarySidebar /> } sidebar={ hasSidebarEnabled && ( <ComplementaryArea.Slot scope="core/edit-widgets" /> diff --git a/packages/edit-widgets/src/components/secondary-sidebar/index.js b/packages/edit-widgets/src/components/secondary-sidebar/index.js index 20488e4478b980..49e240bd147cb2 100644 --- a/packages/edit-widgets/src/components/secondary-sidebar/index.js +++ b/packages/edit-widgets/src/components/secondary-sidebar/index.js @@ -13,7 +13,7 @@ import { store as editWidgetsStore } from '../../store'; import InserterSidebar from './inserter-sidebar'; import ListViewSidebar from './list-view-sidebar'; -export default function SecondarySidebar( { listViewToggleElement } ) { +export default function SecondarySidebar() { const { isInserterOpen, isListViewOpen } = useSelect( ( select ) => { const { isInserterOpened, isListViewOpened } = select( editWidgetsStore ); @@ -27,9 +27,7 @@ export default function SecondarySidebar( { listViewToggleElement } ) { return <InserterSidebar />; } if ( isListViewOpen ) { - return ( - <ListViewSidebar listViewToggleElement={ listViewToggleElement } /> - ); + return <ListViewSidebar />; } return null; } diff --git a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js index 5104a587d9e2cf..6aa2123a6774ad 100644 --- a/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-widgets/src/components/secondary-sidebar/list-view-sidebar.js @@ -4,7 +4,7 @@ import { __experimentalListView as ListView } from '@wordpress/block-editor'; import { Button } from '@wordpress/components'; import { useFocusOnMount, useMergeRefs } from '@wordpress/compose'; -import { useDispatch } from '@wordpress/data'; +import { useDispatch, useSelect } from '@wordpress/data'; import { useCallback, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { closeSmall } from '@wordpress/icons'; @@ -14,9 +14,11 @@ import { ESCAPE } from '@wordpress/keycodes'; * Internal dependencies */ import { store as editWidgetsStore } from '../../store'; +import { unlock } from '../../lock-unlock'; -export default function ListViewSidebar( { listViewToggleElement } ) { +export default function ListViewSidebar() { const { setIsListViewOpened } = useDispatch( editWidgetsStore ); + const { getListViewToggleRef } = unlock( useSelect( editWidgetsStore ) ); // Use internal state instead of a ref to make sure that the component // re-renders when the dropZoneElement updates. @@ -27,8 +29,8 @@ export default function ListViewSidebar( { listViewToggleElement } ) { // When closing the list view, focus should return to the toggle button. const closeListView = useCallback( () => { setIsListViewOpened( false ); - listViewToggleElement?.focus(); - }, [ listViewToggleElement, setIsListViewOpened ] ); + getListViewToggleRef().current?.focus(); + }, [ getListViewToggleRef, setIsListViewOpened ] ); const closeOnEscape = useCallback( ( event ) => { diff --git a/packages/edit-widgets/src/store/index.js b/packages/edit-widgets/src/store/index.js index b8542b2fa5cf19..8768b45954b930 100644 --- a/packages/edit-widgets/src/store/index.js +++ b/packages/edit-widgets/src/store/index.js @@ -11,7 +11,9 @@ import reducer from './reducer'; import * as resolvers from './resolvers'; import * as selectors from './selectors'; import * as actions from './actions'; +import * as privateSelectors from './private-selectors'; import { STORE_NAME } from './constants'; +import { unlock } from '../lock-unlock'; /** * Block editor data store configuration. @@ -47,3 +49,5 @@ apiFetch.use( function ( options, next ) { return next( options ); } ); + +unlock( store ).registerPrivateSelectors( privateSelectors ); diff --git a/packages/edit-widgets/src/store/private-selectors.js b/packages/edit-widgets/src/store/private-selectors.js new file mode 100644 index 00000000000000..29911091622fbb --- /dev/null +++ b/packages/edit-widgets/src/store/private-selectors.js @@ -0,0 +1,16 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + +/** + * WordPress dependencies + */ +import { createRef } from '@wordpress/element'; + +export const getListViewToggleRef = createSelector( + () => { + return createRef(); + }, + () => [] +); diff --git a/packages/editor/src/store/private-selectors.js b/packages/editor/src/store/private-selectors.js index 0ab97dea67ce55..b05dcae93c9472 100644 --- a/packages/editor/src/store/private-selectors.js +++ b/packages/editor/src/store/private-selectors.js @@ -1,8 +1,14 @@ +/** + * External dependencies + */ +import createSelector from 'rememo'; + /** * WordPress dependencies */ import { store as blockEditorStore } from '@wordpress/block-editor'; import { createRegistrySelector } from '@wordpress/data'; +import { createRef } from '@wordpress/element'; /** * Internal dependencies @@ -45,3 +51,10 @@ export const getInsertionPoint = createRegistrySelector( return EMPTY_INSERTION_POINT; } ); + +export const getListViewToggleRef = createSelector( + () => { + return createRef(); + }, + () => [] +); From 395b18f1c6850fc43271edd2d0114bdf79ed7bd7 Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Tue, 19 Dec 2023 05:55:58 -0600 Subject: [PATCH 267/325] Remove unnecessary isDisabled option on useShortcut for block popover (#56907) --- .../block-tools/block-toolbar-popover.js | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js index a50e5dc42b3712..5af9359d7aade5 100644 --- a/packages/block-editor/src/components/block-tools/block-toolbar-popover.js +++ b/packages/block-editor/src/components/block-tools/block-toolbar-popover.js @@ -38,16 +38,10 @@ export default function BlockToolbarPopover( { const { stopTyping } = useDispatch( blockEditorStore ); const isToolbarForced = useRef( false ); - useShortcut( - 'core/block-editor/focus-toolbar', - () => { - isToolbarForced.current = true; - stopTyping( true ); - }, - { - isDisabled: false, - } - ); + useShortcut( 'core/block-editor/focus-toolbar', () => { + isToolbarForced.current = true; + stopTyping( true ); + } ); useEffect( () => { isToolbarForced.current = false; From 1ef449f586fb9e5828e1172cba44700a1d4a7b8f Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Tue, 19 Dec 2023 06:22:52 -0600 Subject: [PATCH 268/325] Refactor useCanContextualToolbarShow for simplicity and clarity (#56914) * Refactor useCanContextualToolbarShow for simplicity and clarity - Rename to useCanBlockToolbarBeFocused: Each usage of useCanContextualTool barShow ended up as one blockToolbarCanBeFocused const. - The scenarios of when the block toolbar can be focused was much simpler t han the implemented checks. Refactored for the scenarios in which it can be focused. * Update packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js Co-authored-by: Andrei Draganescu <me@andreidraganescu.info> --------- Co-authored-by: Ben Dwyer <ben@scruffian.com> Co-authored-by: Andrei Draganescu <me@andreidraganescu.info> --- packages/block-editor/src/private-apis.js | 4 +- .../utils/use-can-block-toolbar-be-focused.js | 48 +++++++++++ .../use-should-contextual-toolbar-show.js | 85 ------------------- .../components/header/header-toolbar/index.js | 15 +--- .../header-edit-mode/document-tools/index.js | 13 +-- .../components/header/document-tools/index.js | 13 +-- 6 files changed, 57 insertions(+), 121 deletions(-) create mode 100644 packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js delete mode 100644 packages/block-editor/src/utils/use-should-contextual-toolbar-show.js diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index ff86f07aa4caa4..74bf4af421dfbb 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -9,7 +9,7 @@ import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; -import { useShouldContextualToolbarShow } from './utils/use-should-contextual-toolbar-show'; +import { useCanBlockToolbarBeFocused } from './utils/use-can-block-toolbar-be-focused'; import { cleanEmptyObject, useStyleOverride } from './hooks/utils'; import BlockQuickNavigation from './components/block-quick-navigation'; import { LayoutStyle } from './components/block-list/layout'; @@ -39,7 +39,7 @@ lock( privateApis, { PrivateListView, ResizableBoxPopover, BlockInfo, - useShouldContextualToolbarShow, + useCanBlockToolbarBeFocused, cleanEmptyObject, useStyleOverride, BlockQuickNavigation, diff --git a/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js b/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js new file mode 100644 index 00000000000000..f118c88dc2b1d4 --- /dev/null +++ b/packages/block-editor/src/utils/use-can-block-toolbar-be-focused.js @@ -0,0 +1,48 @@ +/** + * WordPress dependencies + */ +import { useSelect } from '@wordpress/data'; +import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../store'; +import { unlock } from '../lock-unlock'; + +/** + * Returns true if the block toolbar should be able to receive focus. + * + * @return {boolean} Whether the block toolbar should be able to receive focus + */ +export function useCanBlockToolbarBeFocused() { + return useSelect( ( select ) => { + const { + __unstableGetEditorMode, + getBlock, + getSettings, + getSelectedBlockClientId, + getFirstMultiSelectedBlockClientId, + } = unlock( select( blockEditorStore ) ); + + const selectedBlockId = + getFirstMultiSelectedBlockClientId() || getSelectedBlockClientId(); + const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( + getBlock( selectedBlockId ) || {} + ); + + // Fixed Toolbar can be focused when: + // - a block is selected + // - fixed toolbar is on + // Block Toolbar Popover can be focused when: + // - a block is selected + // - we are in edit mode + // - it is not an empty default block + return ( + !! selectedBlockId && + ( getSettings().hasFixedToolbar || + ( __unstableGetEditorMode() === 'edit' && + ! isEmptyDefaultBlock ) ) + ); + }, [] ); +} diff --git a/packages/block-editor/src/utils/use-should-contextual-toolbar-show.js b/packages/block-editor/src/utils/use-should-contextual-toolbar-show.js deleted file mode 100644 index 1aae7b99cbf140..00000000000000 --- a/packages/block-editor/src/utils/use-should-contextual-toolbar-show.js +++ /dev/null @@ -1,85 +0,0 @@ -/** - * WordPress dependencies - */ -import { useSelect } from '@wordpress/data'; -import { isUnmodifiedDefaultBlock } from '@wordpress/blocks'; -import { useViewportMatch } from '@wordpress/compose'; - -/** - * Internal dependencies - */ -import { store as blockEditorStore } from '../store'; -import { unlock } from '../lock-unlock'; - -/** - * Returns true if the contextual block toolbar should show, or false if it should be hidden. - * - * @return {boolean} Whether the block toolbar is hidden. - */ -export function useShouldContextualToolbarShow() { - const isLargeViewport = useViewportMatch( 'medium' ); - - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useSelect( - ( select ) => { - const { - __unstableGetEditorMode, - isMultiSelecting, - isTyping, - isBlockInterfaceHidden, - getBlock, - getSettings, - isNavigationMode, - getSelectedBlockClientId, - getFirstMultiSelectedBlockClientId, - } = unlock( select( blockEditorStore ) ); - - const isEditMode = __unstableGetEditorMode() === 'edit'; - const hasFixedToolbar = getSettings().hasFixedToolbar; - const isDistractionFree = getSettings().isDistractionFree; - const selectedBlockId = - getFirstMultiSelectedBlockClientId() || - getSelectedBlockClientId(); - const hasSelectedBlockId = !! selectedBlockId; - const isEmptyDefaultBlock = isUnmodifiedDefaultBlock( - getBlock( selectedBlockId ) || {} - ); - - const _shouldShowContextualToolbar = - isEditMode && - ! hasFixedToolbar && - ( ! isDistractionFree || isNavigationMode() ) && - isLargeViewport && - ! isMultiSelecting() && - ! isTyping() && - hasSelectedBlockId && - ! isEmptyDefaultBlock && - ! isBlockInterfaceHidden(); - - const _canFocusHiddenToolbar = - isEditMode && - hasSelectedBlockId && - ! _shouldShowContextualToolbar && - ! hasFixedToolbar && - ! isDistractionFree && - ! isEmptyDefaultBlock; - - return { - shouldShowContextualToolbar: _shouldShowContextualToolbar, - canFocusHiddenToolbar: _canFocusHiddenToolbar, - fixedToolbarCanBeFocused: - ( hasFixedToolbar || ! isLargeViewport ) && selectedBlockId, - }; - }, - [ isLargeViewport ] - ); - - return { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - }; -} diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index c524a8842ad4cc..e1d059578809e0 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -26,7 +26,7 @@ import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; import { store as editPostStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); +const { useCanBlockToolbarBeFocused } = unlock( blockEditorPrivateApis ); const preventDefault = ( event ) => { event.preventDefault(); @@ -73,17 +73,8 @@ function HeaderToolbar( { hasFixedToolbar } ) { const isLargeViewport = useViewportMatch( 'medium' ); const isWideViewport = useViewportMatch( 'wide' ); - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); - // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; + const blockToolbarCanBeFocused = useCanBlockToolbarBeFocused(); + /* translators: accessibility text for the editor toolbar */ const toolbarAriaLabel = __( 'Document tools' ); diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index 9db8e091265e2f..837c825b2060a9 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -24,7 +24,7 @@ import RedoButton from '../undo-redo/redo'; import { store as editSiteStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); +const { useCanBlockToolbarBeFocused } = unlock( blockEditorPrivateApis ); const preventDefault = ( event ) => { event.preventDefault(); @@ -82,17 +82,8 @@ export default function DocumentTools( { [ setIsListViewOpened, isListViewOpen ] ); - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; + const blockToolbarCanBeFocused = useCanBlockToolbarBeFocused(); /* translators: button label text should, if possible, be under 16 characters. */ const longLabel = _x( diff --git a/packages/edit-widgets/src/components/header/document-tools/index.js b/packages/edit-widgets/src/components/header/document-tools/index.js index 06376bbd762916..a9799ac993f9ab 100644 --- a/packages/edit-widgets/src/components/header/document-tools/index.js +++ b/packages/edit-widgets/src/components/header/document-tools/index.js @@ -22,7 +22,7 @@ import useLastSelectedWidgetArea from '../../../hooks/use-last-selected-widget-a import { store as editWidgetsStore } from '../../../store'; import { unlock } from '../../../lock-unlock'; -const { useShouldContextualToolbarShow } = unlock( blockEditorPrivateApis ); +const { useCanBlockToolbarBeFocused } = unlock( blockEditorPrivateApis ); function DocumentTools() { const isMediumViewport = useViewportMatch( 'medium' ); @@ -75,17 +75,8 @@ function DocumentTools() { [ setIsListViewOpened, isListViewOpen ] ); - const { - shouldShowContextualToolbar, - canFocusHiddenToolbar, - fixedToolbarCanBeFocused, - } = useShouldContextualToolbarShow(); // If there's a block toolbar to be focused, disable the focus shortcut for the document toolbar. - // There's a fixed block toolbar when the fixed toolbar option is enabled or when the browser width is less than the large viewport. - const blockToolbarCanBeFocused = - shouldShowContextualToolbar || - canFocusHiddenToolbar || - fixedToolbarCanBeFocused; + const blockToolbarCanBeFocused = useCanBlockToolbarBeFocused(); return ( <NavigableToolbar From 6c62c4a96d3b7fa3f45cc4e26713388ee2e7b5ef Mon Sep 17 00:00:00 2001 From: James Koster <james@jameskoster.co.uk> Date: Tue, 19 Dec 2023 12:51:21 +0000 Subject: [PATCH 269/325] Reduce clearance around the Frame in the site editor (#57023) --- packages/base-styles/_variables.scss | 2 +- packages/edit-site/src/components/layout/style.scss | 2 +- packages/edit-site/src/components/page/style.scss | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/base-styles/_variables.scss b/packages/base-styles/_variables.scss index d47f6e95b6265e..0638911b94321e 100644 --- a/packages/base-styles/_variables.scss +++ b/packages/base-styles/_variables.scss @@ -62,7 +62,7 @@ $modal-width-small: 384px; $modal-width-medium: 512px; $modal-width-large: 840px; $spinner-size: 16px; -$canvas-padding: $grid-unit-30; +$canvas-padding: $grid-unit-20; /** diff --git a/packages/edit-site/src/components/layout/style.scss b/packages/edit-site/src/components/layout/style.scss index 72b8e4db49716f..53d3ed82a88bc2 100644 --- a/packages/edit-site/src/components/layout/style.scss +++ b/packages/edit-site/src/components/layout/style.scss @@ -20,7 +20,7 @@ } @include break-medium { - width: calc(#{$nav-sidebar-width} - #{$canvas-padding}); + width: calc(#{$nav-sidebar-width} - #{$grid-unit-30}); } .edit-site-layout.is-full-canvas & { diff --git a/packages/edit-site/src/components/page/style.scss b/packages/edit-site/src/components/page/style.scss index 72ecbb4e2b7d77..fd69544bb6cd7f 100644 --- a/packages/edit-site/src/components/page/style.scss +++ b/packages/edit-site/src/components/page/style.scss @@ -7,7 +7,7 @@ margin-top: $header-height; @include break-medium() { border-radius: 8px; - margin: $grid-unit-30 $grid-unit-30 $grid-unit-30 0; + margin: $canvas-padding $canvas-padding $canvas-padding 0; } } From 76166441ffb73f96e4b1ba9fa0f13e5a1cba04db Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Tue, 19 Dec 2023 15:10:25 +0200 Subject: [PATCH 270/325] Fix content lock UI regression (#56974) --- packages/block-editor/src/hooks/index.js | 3 ++- packages/block-editor/src/hooks/utils.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/hooks/index.js b/packages/block-editor/src/hooks/index.js index 26d1d1ad12bc0b..385b9fe6b1511e 100644 --- a/packages/block-editor/src/hooks/index.js +++ b/packages/block-editor/src/hooks/index.js @@ -23,7 +23,7 @@ import border from './border'; import position from './position'; import layout from './layout'; import childLayout from './layout-child'; -import './content-lock-ui'; +import contentLockUI from './content-lock-ui'; import './metadata'; import customFields from './custom-fields'; import blockHooks from './block-hooks'; @@ -38,6 +38,7 @@ createBlockEditFilter( duotone, position, layout, + contentLockUI, window.__experimentalConnections ? customFields : null, blockHooks, blockRenaming, diff --git a/packages/block-editor/src/hooks/utils.js b/packages/block-editor/src/hooks/utils.js index cd342af00d1a55..27c5c346458d21 100644 --- a/packages/block-editor/src/hooks/utils.js +++ b/packages/block-editor/src/hooks/utils.js @@ -418,12 +418,14 @@ export function createBlockEditFilter( features ) { neededProps[ key ] = props.attributes[ key ]; } } + return ( <Edit // We can use the index because the array length // is fixed per page load right now. key={ i } name={ props.name } + isSelected={ props.isSelected } clientId={ props.clientId } setAttributes={ props.setAttributes } __unstableParentLayout={ From e47ba1b47ee6e8080d2462072d10e1a211640a24 Mon Sep 17 00:00:00 2001 From: Hidekazu Ishikawa <kurudrive@gmail.com> Date: Tue, 19 Dec 2023 22:30:24 +0900 Subject: [PATCH 271/325] Fix DayButton dot position and expand Button area (#55502) * Fix DatButton dot position and expand Button area * add change log * Move changelog --------- Co-authored-by: Lena Morita <lena@jaguchi.com> --- packages/components/CHANGELOG.md | 4 ++++ packages/components/src/date-time/date/styles.ts | 6 +++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 9614494f0f0e07..6b38ba0bd885aa 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,10 @@ - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). - `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). +### Enhancements + +- `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). + ### Experimental - `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). diff --git a/packages/components/src/date-time/date/styles.ts b/packages/components/src/date-time/date/styles.ts index 5500206abd40a2..992dab34544cea 100644 --- a/packages/components/src/date-time/date/styles.ts +++ b/packages/components/src/date-time/date/styles.ts @@ -84,8 +84,8 @@ export const DayButton = styled( Button, { &&& { border-radius: 100%; - height: ${ space( 7 ) }; - width: ${ space( 7 ) }; + height: ${ space( 8 ) }; + width: ${ space( 8 ) }; ${ ( props ) => props.isSelected && @@ -108,7 +108,7 @@ export const DayButton = styled( Button, { ::before { background: ${ props.isSelected ? COLORS.white : COLORS.theme.accent }; border-radius: 2px; - bottom: 0; + bottom: 2px; content: " "; height: 4px; left: 50%; From a76800c9e1f0199400e5420e8e784222cc82c168 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Tue, 19 Dec 2023 16:11:53 +0200 Subject: [PATCH 272/325] Font size picker: use Button API for keeping focus on reset (#57221) --- packages/components/CHANGELOG.md | 1 + packages/components/src/font-size-picker/index.tsx | 13 +++++-------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 6b38ba0bd885aa..1db9d8cca26e08 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -4,6 +4,7 @@ ### Bug Fix +- `FontSizePicker`: Use Button API for keeping focus on reset ([#57221](https://github.com/WordPress/gutenberg/pull/57221)). - `FontSizePicker`: Fix Reset button focus loss ([#57196](https://github.com/WordPress/gutenberg/pull/57196)). - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). diff --git a/packages/components/src/font-size-picker/index.tsx b/packages/components/src/font-size-picker/index.tsx index 9d977f43db9597..e14c6514d25173 100644 --- a/packages/components/src/font-size-picker/index.tsx +++ b/packages/components/src/font-size-picker/index.tsx @@ -277,14 +277,11 @@ const UnforwardedFontSizePicker = ( { withReset && ( <FlexItem> <Button - aria-disabled={ isDisabled } - onClick={ - isDisabled - ? undefined - : () => { - onChange?.( undefined ); - } - } + disabled={ isDisabled } + __experimentalIsFocusable + onClick={ () => { + onChange?.( undefined ); + } } variant="secondary" __next40pxDefaultSize size={ From b653242415f2a50c7ff1af7393c0122c7e6bd052 Mon Sep 17 00:00:00 2001 From: Nik Tsekouras <ntsekouras@outlook.com> Date: Tue, 19 Dec 2023 16:25:00 +0200 Subject: [PATCH 273/325] DataViews: Use SelectControl for selecting a page (#57215) * DataViews: Use SelectControl for selecting a page * Use compact size --------- Co-authored-by: James Koster <james@jameskoster.co.uk> --- packages/dataviews/src/pagination.js | 31 ++++++++++++---------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/dataviews/src/pagination.js b/packages/dataviews/src/pagination.js index 97f38553b0bf99..2c8734cdf56b84 100644 --- a/packages/dataviews/src/pagination.js +++ b/packages/dataviews/src/pagination.js @@ -4,7 +4,7 @@ import { Button, __experimentalHStack as HStack, - __experimentalNumberControl as NumberControl, + SelectControl, } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { sprintf, __, _x } from '@wordpress/i18n'; @@ -40,28 +40,23 @@ function Pagination( { ), { CurrenPageControl: ( - <NumberControl + <SelectControl aria-label={ __( 'Current page' ) } - min={ 1 } - max={ totalPages } - onChange={ ( value ) => { - const _value = +value; - if ( - ! _value || - _value < 1 || - _value > totalPages - ) { - return; - } + value={ view.page } + options={ Array.from( + Array( totalPages ) + ).map( ( _, i ) => { + const page = i + 1; + return { value: page, label: page }; + } ) } + onChange={ ( newValue ) => { onChangeView( { ...view, - page: _value, + page: +newValue, } ); } } - step="1" - value={ view.page } - isDragEnabled={ false } - spinControls="none" + size={ 'compact' } + __nextHasNoMarginBottom /> ), } From 709a3817c317a89e7ddbaea9a669d2634f1328ff Mon Sep 17 00:00:00 2001 From: Jarda Snajdr <jsnajdr@gmail.com> Date: Tue, 19 Dec 2023 15:31:37 +0100 Subject: [PATCH 274/325] Disable webpack perf hints when compiling packages (#57155) * Disable webpack perf hints when compiling packages * Rewrite reduce to fromEntries in packages webpack config --- tools/webpack/packages.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tools/webpack/packages.js b/tools/webpack/packages.js index 86554d5f139098..1bbe63f064c305 100644 --- a/tools/webpack/packages.js +++ b/tools/webpack/packages.js @@ -129,10 +129,10 @@ const vendorsCopyConfig = Object.entries( vendors ).flatMap( module.exports = { ...baseConfig, name: 'packages', - entry: gutenbergPackages.reduce( ( memo, packageName ) => { - return { - ...memo, - [ packageName ]: { + entry: Object.fromEntries( + gutenbergPackages.map( ( packageName ) => [ + packageName, + { import: `./packages/${ packageName }`, library: { name: [ 'wp', camelCaseDash( packageName ) ], @@ -142,8 +142,8 @@ module.exports = { : undefined, }, }, - }; - }, {} ), + ] ) + ), output: { devtoolNamespace: 'wp', filename: './build/[name]/index.min.js', @@ -157,6 +157,9 @@ module.exports = { return `webpack://${ info.namespace }/${ info.resourcePath }`; }, }, + performance: { + hints: false, // disable warnings about package sizes + }, plugins: [ ...plugins, new DependencyExtractionWebpackPlugin( { injectPolyfill: true } ), From 57e463dcec17e3a538a73326c736bc38608ae0b5 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 19 Dec 2023 10:16:58 -0500 Subject: [PATCH 275/325] Components: replace `TabPanel` with `Tabs` in the editor Global Styles color palette (#57126) --- .../global-styles/screen-color-palette.js | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/edit-site/src/components/global-styles/screen-color-palette.js b/packages/edit-site/src/components/global-styles/screen-color-palette.js index 0e5b9c5d960fc1..587373324f0ff3 100644 --- a/packages/edit-site/src/components/global-styles/screen-color-palette.js +++ b/packages/edit-site/src/components/global-styles/screen-color-palette.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __ } from '@wordpress/i18n'; -import { TabPanel } from '@wordpress/components'; +import { privateApis as componentsPrivateApis } from '@wordpress/components'; /** * Internal dependencies @@ -10,6 +10,9 @@ import { TabPanel } from '@wordpress/components'; import ColorPalettePanel from './color-palette-panel'; import GradientPalettePanel from './gradients-palette-panel'; import ScreenHeader from './header'; +import { unlock } from '../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); function ScreenColorPalette( { name } ) { return ( @@ -20,31 +23,18 @@ function ScreenColorPalette( { name } ) { 'Palettes are used to provide default color options for blocks and various design tools. Here you can edit the colors with their labels.' ) } /> - <TabPanel - tabs={ [ - { - name: 'solid', - title: 'Solid', - value: 'solid', - }, - { - name: 'gradient', - title: 'Gradient', - value: 'gradient', - }, - ] } - > - { ( tab ) => ( - <> - { tab.value === 'solid' && ( - <ColorPalettePanel name={ name } /> - ) } - { tab.value === 'gradient' && ( - <GradientPalettePanel name={ name } /> - ) } - </> - ) } - </TabPanel> + <Tabs> + <Tabs.TabList> + <Tabs.Tab tabId="solid">Solid</Tabs.Tab> + <Tabs.Tab tabId="gradient">Gradient</Tabs.Tab> + </Tabs.TabList> + <Tabs.TabPanel tabId="solid" focusable={ false }> + <ColorPalettePanel name={ name } /> + </Tabs.TabPanel> + <Tabs.TabPanel tabId="gradient" focusable={ false }> + <GradientPalettePanel name={ name } /> + </Tabs.TabPanel> + </Tabs> </> ); } From c8de118f05734e608514371bb7ad78f73ce39dee Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Tue, 19 Dec 2023 16:22:21 +0100 Subject: [PATCH 276/325] Docs/getting started readme (#57223) * Update getting started section with new block development tutorials and ways to stay informed * Revamped README Getting Started and adapted README Handbook --- docs/README.md | 8 ++-- docs/getting-started/README.md | 70 +++++++++++++--------------------- 2 files changed, 30 insertions(+), 48 deletions(-) diff --git a/docs/README.md b/docs/README.md index 222b54209c7d62..ba6b35a761f6e0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -27,20 +27,18 @@ Besides offering an [enhanced editing experience](https://wordpress.org/gutenber This handbook is focused on block development and is divided into five sections, each serving a different purpose. -- [**Getting Started**](https://developer.wordpress.org/block-editor/getting-started/) - For those just starting out with block development, this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/create-block/). Its [Glossary of terms](https://developer.wordpress.org/block-editor/getting-started/glossary/) and [FAQs](https://developer.wordpress.org/block-editor/getting-started/faq/) should answer any outstanding questions you may have. +- [**Getting Started**](https://developer.wordpress.org/block-editor/getting-started/) - For those just starting out with block development, this is where you can get set up with a [development environment](https://developer.wordpress.org/block-editor/getting-started/devenv/) and learn the [fundamentals of block development](https://developer.wordpress.org/block-editor/getting-started/fundamentals/). Its [Quick Start Guide](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) and [Tutorial: Build your first block](https://developer.wordpress.org/block-editor/getting-started/tutorial/) are probably the best places to start learning block development. -- [**How-to Guides**](https://developer.wordpress.org/block-editor/how-to-guides/) - Here, you can build on what you learned in the Getting Started section and learn how to solve particular problems you might encounter. You can also get tutorials and example code that you can reuse for projects such as [building a full-featured block](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/) or [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/). In addition, you can learn [How to use JavaScript with the Block Editor](https://developer.wordpress.org/block-editor/how-to-guides/javascript/). +- [**How-to Guides**](https://developer.wordpress.org/block-editor/how-to-guides/) - Here, you can build on what you learned in the Getting Started section and learn how to solve particular problems you might encounter. You can also get tutorials and example code that you can reuse for projects such as [working with WordPress’ data](https://developer.wordpress.org/block-editor/how-to-guides/data-basics/) or [Curating the Editor Experience](https://developer.wordpress.org/block-editor/how-to-guides/curating-the-editor-experience/). - [**Reference Guides**](https://developer.wordpress.org/block-editor/reference-guides/) - This section is the heart of the handbook and is where you can get down to the nitty-gritty and look up the details of the particular API you’re working with or need information on. Among other things, the [Block API Reference](https://developer.wordpress.org/block-editor/reference-guides/block-api/) covers most of what you will want to do with a block, and each [component](https://developer.wordpress.org/block-editor/reference-guides/components/) and [package](https://developer.wordpress.org/block-editor/reference-guides/packages/) is also documented here. _Components are also documented via [Storybook](https://wordpress.github.io/gutenberg/?path=/story/docs-introduction--page)._ - - [**Explanations**](https://developer.wordpress.org/block-editor/explanations/) - This section enables you to go deeper and reinforce your practical knowledge with a theoretical understanding of the [Architecture](https://developer.wordpress.org/block-editor/explanations/architecture/) of the block editor. - [**Contributor Guide**](https://developer.wordpress.org/block-editor/contributors/) - Gutenberg is open source software, and anyone is welcome to contribute to the project. This section details how to contribute and can help you choose in which way you want to contribute, whether with [code](https://developer.wordpress.org/block-editor/contributors/code/), [design](https://developer.wordpress.org/block-editor/contributors/design/), [documentation](https://developer.wordpress.org/block-editor/contributors/documentation/), or in some other way. - ## Further resources This handbook should be considered the canonical resource for all things related to block development. However, there are other resources that can help you. @@ -48,7 +46,7 @@ This handbook should be considered the canonical resource for all things related - [**WordPress Developer Blog**](https://developer.wordpress.org/news/) - An ever-growing resource of technical articles covering specific topics related to block development and a wide variety of use cases. The blog is also an excellent way to [keep up with the latest developments in WordPress](https://developer.wordpress.org/news/tag/roundup/). - [**Learn WordPress**](https://learn.wordpress.org/) - The WordPress hub for learning resources where you can find courses like [Introduction to Block Development: Build your first custom block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/), [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) or [Using the WordPress Data Layer](https://learn.wordpress.org/course/using-the-wordpress-data-layer/) - [**WordPress.tv**](https://wordpress.tv/) - A hub of WordPress-related videos (from talks at WordCamps to recordings of online workshops) curated and moderated by the WordPress.org community. You’re sure to find something to aid your learning about [block development](https://wordpress.tv/?s=block%20development&sort=newest) or the [Block Editor](https://wordpress.tv/?s=block%20editor&sort=relevance) here. -- [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [gutenberg-examples](https://github.com/WordPress/gutenberg-examples) repository is another useful reference._ +- [**Gutenberg repository**](https://github.com/WordPress/gutenberg/) - Development of the block editor project is carried out in this GitHub repository. It contains the code of interesting packages such as [`block-library`](https://github.com/WordPress/gutenberg/tree/trunk/packages/block-library/src) (core blocks) or [`components`](https://github.com/WordPress/gutenberg/tree/trunk/packages/components) (common UI elements). _The [block-development-examples](https://github.com/WordPress/block-development-examples) repository is another useful reference._ - [**End User Documentation**](https://wordpress.org/documentation/) - This documentation site is targeted to the end user (not developers), where you can also find documentation about the [Block Editor](https://wordpress.org/documentation/category/block-editor/) and [working with blocks](https://wordpress.org/documentation/article/work-with-blocks/). ## Are you in the right place? diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 136caa5607e487..b45a740c2a58cd 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -1,6 +1,21 @@ # Getting Started -Welcome! Let's get started building with blocks. Blocks are at the core of extending WordPress. You can create custom blocks, your own block patterns, or combine them together to build a block theme. At a high level, here are a few ways to begin your journey but read on to explore more: +Welcome! Let's get started building with blocks. Blocks are at the core of extending WordPress. You can create custom blocks, your own block patterns, or combine them together to build a block theme. + +## Navigating this chapter + +For those starting with block development, this section is the perfect starting point as it provides the knowledge you need to start creating your own custom blocks. + +- [**Block Development Environment**](https://developer.wordpress.org/block-editor/getting-started/devenv/) - Set up the right development environment to create blocks and get introduced to basic tools for block development such as [`wp-env`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/), [`create-block`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) and [`wp-scripts`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-create-block/) +- [**Quick Start Guide**](https://developer.wordpress.org/block-editor/getting-started/quick-start-guide/) - Get a block up and running in less than 1 min. +- [**Tutorial: Build your first block**](https://developer.wordpress.org/block-editor/getting-started/tutorial/) - The tutorial will guide you, step by step, through the complete process of creating a fully functional custom block. +- [**Fundamentals of Block Development**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/) - This section provides an introduction to the most relevant concepts in Block Development. +- [**Glossary**](https://developer.wordpress.org/block-editor/getting-started/glossary/) - Glossary of terms related to the Block Editor and Full Site Editing +- [**Frequently Asked Questions**](https://developer.wordpress.org/block-editor/getting-started/faq/) - Set of questions (and answers) that have come up from the last few years of Gutenberg development. + +## Getting Started on the WordPress project and Gutenberg + +At a high level, here are a few ways to begin your journey but read on to explore more: - Learn more about where this work is going by [reviewing the long term roadmap](https://wordpress.org/about/roadmap/). - Explore the [GitHub repo](https://github.com/WordPress/gutenberg/) to see the latest issues and PRs folks are working on, especially [Good First Issues](https://github.com/WordPress/gutenberg/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22). @@ -9,15 +24,21 @@ Welcome! Let's get started building with blocks. Blocks are at the core of exten - Expand your knowledge by reviewing more developer docs at the overall [developer.wordpress.org resource](https://developer.wordpress.org/). - Subscribe to [updates on Make Core](https://make.wordpress.org/core/), the main site where ongoing project updates happen. -## Tutorials +### Ways to Stay Informed + +New features and changes are important to keep up to date on as the Gutenberg project continues. Each person will have their own unique needs in keeping up with a project of this scale. What follows is more of a catalogue of ways to keep up rather than a recommendation for how to do so. -[Development Environment](/docs/getting-started/devenv/README.md) - A guide to setup your local environment for JavaScript development for creating plugins, themes, and the tools you will need to extend WordPress or contribute to the block editor. +- [Keeping up with Gutenberg](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/) - compilation of Gutenberg-related team posts of Core, Core-Editor, Core-js, Core-css, Design, Meta, and Themes, and other teams. +- [“What’s New In Gutenberg?” release posts](https://make.wordpress.org/core/tag/gutenberg-new/). These updates are wrangled by the Core Editor team and focus on what’s been released in each biweekly Gutenberg release. They include the most relevant features released and a full changelog. +- [Core Editor meetings](https://make.wordpress.org/core/tag/core-editor-summary/). These meetings are wrangled by volunteer members in the #core-editor Slack channel. [Agendas](https://make.wordpress.org/core/tag/core-editor-summary/) and [summaries](https://make.wordpress.org/core/tag/core-editor-summary/) are shared on the [Make Core blog](https://make.wordpress.org/core/). They focus on task coordination and relevant discussions around Gutenberg releases. There is an Open Floor period in each chat where people can suggest topics to discuss. +- Checking in on [issues](https://github.com/WordPress/gutenberg/issues) and [PRs](https://github.com/WordPress/gutenberg/pulls) on GitHub. This will give you a nearly real-time understanding of what’s being worked on by the developers and designers. -[Create a Block Tutorial](/docs/getting-started/create-block/README.md) - Learn how to create your first block for the WordPress block editor. -## Learn WordPress Courses +## Additional Resources -At [Learn WordPress](https://learn.wordpress.org/), you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/) to learn more about developing for the Block Editor. Here is a selection of current offerings. +The [block-development-examples](https://github.com/wptrainingteam/block-development-examples) repo is the central hub of examples for block development referenced from this handbook. + +At [Learn WordPress](https://learn.wordpress.org/), you can find [tutorials](https://learn.wordpress.org/tutorials/), [courses](https://learn.wordpress.org/courses/), and [online workshops](https://learn.wordpress.org/online-workshops/) to learn more about developing for the Block Editor. Here is a selection of current offerings: - [Intro to Block Development: Build Your First Custom Block](https://learn.wordpress.org/course/introduction-to-block-development-build-your-first-custom-block/) - [Converting a Shortcode to a Block](https://learn.wordpress.org/course/converting-a-shortcode-to-a-block/) @@ -25,40 +46,3 @@ At [Learn WordPress](https://learn.wordpress.org/), you can find [tutorials](htt - [Registering Block Patterns](https://learn.wordpress.org/workshop/registering-block-patterns/) - [Intro to Gutenberg Block Development](https://learn.wordpress.org/workshop/intro-to-gutenberg-block-development/) - [Intro to Publishing with the Block Editor](https://learn.wordpress.org/workshop/intro-to-publishing-with-the-block-editor/) - -## Ways to Stay Informed - -New features and changes are important to keep up to date on as the Gutenberg project continues. Each person will have their own unique needs in keeping up with a project of this scale. What follows is more of a catalogue of ways to keep up rather than a recommendation for how to do so. - -**Yearly:** - -The [WordPress.org Roadmap](https://wordpress.org/about/roadmap/) with Four Phases of Gutenberg updated by project leadership. This is the highest level overview of the changes coming to WordPress. - -**Quarterly:** - -[Quarterly Updates](https://make.wordpress.org/updates/tag/quarterly-updates/) from Contribution Teams. These updates give an overview on what each team is working on, struggling with, and how to get involved. - -**Monthly:** - -[“What’s Next In Gutenberg?” posts](https://make.wordpress.org/core/tag/gutenberg-next/). These updates are wrangled by the Core Editor team and highlight areas of work aligned with the Gutenberg roadmap for contributors to help, how to get involved, and more. - -[Block Based Themes Meeting](https://make.wordpress.org/themes/tags/block-based-meeting/). These meetings are currently wrangled in the #themereview Slack channel and are dedicated to sharing FSE changes that will specifically impact themes. Agendas and summaries are shared on the [Make Themes blog](https://make.wordpress.org/themes/). - -**Biweekly:** - -[“What’s New In Gutenberg?” release posts](https://make.wordpress.org/core/tag/gutenberg-new/). These updates are wrangled by the Core Editor team and focus on what’s been released in each biweekly Gutenberg release. They include the most relevant features released and a full changelog. - -**Weekly:** - -[Core Editor meetings](https://make.wordpress.org/core/tag/core-editor-summary/). These meetings are wrangled by volunteer members in the #core-editor Slack channel. [Agendas](https://make.wordpress.org/core/tag/core-editor-summary/) and [summaries](https://make.wordpress.org/core/tag/core-editor-summary/) are shared on the [Make Core blog](https://make.wordpress.org/core/). They focus on task coordination and relevant discussions around Gutenberg releases. There is an Open Floor period in each chat where people can suggest topics to discuss. - -[Weekly Theme Related Gutenberg Updates](https://make.wordpress.org/themes/tags/gutenberg-themes-roundup/). These posts are focused on themes, including everything from current discussions to recent changes, as well as helpful resources for theme authors. - -**Daily:** - -Checking in on [issues](https://github.com/WordPress/gutenberg/issues) and [PRs](https://github.com/WordPress/gutenberg/pulls) on GitHub. This will give you a nearly real-time understanding of what’s being worked on by the developers and designers. - -- [Glossary](/docs/getting-started/glossary.md) -- [Frequently Asked Questions](/docs/getting-started/faq.md) -- [Project History](/docs/explanations/history.md) -- [Gutenberg related Make posts](https://make.wordpress.org/core/handbook/references/keeping-up-with-gutenberg-index/) \ No newline at end of file From a685503124e667dce362d2a2e5289d9d6d88cd59 Mon Sep 17 00:00:00 2001 From: arthur791004 <arthur.chu@automattic.com> Date: Tue, 19 Dec 2023 23:53:26 +0800 Subject: [PATCH 277/325] Save Button: Fix the translation of the Activate button (#57147) * Save Button: Add the translators to the activation string * Use __() function for the translation --- .../src/components/save-button/index.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/edit-site/src/components/save-button/index.js b/packages/edit-site/src/components/save-button/index.js index d4c2969920c52a..e7eac94de5f314 100644 --- a/packages/edit-site/src/components/save-button/index.js +++ b/packages/edit-site/src/components/save-button/index.js @@ -60,13 +60,26 @@ export default function SaveButton( { const getLabel = () => { if ( isPreviewingTheme() ) { if ( isSaving ) { - return sprintf( 'Activating %s', previewingThemeName ); + return sprintf( + /* translators: %s: The name of theme to be activated. */ + __( 'Activating %s' ), + previewingThemeName + ); } else if ( disabled ) { return __( 'Saved' ); } else if ( isDirty ) { - return sprintf( 'Activate %s & Save', previewingThemeName ); + return sprintf( + /* translators: %s: The name of theme to be activated. */ + __( 'Activate %s & Save' ), + previewingThemeName + ); } - return sprintf( 'Activate %s', previewingThemeName ); + + return sprintf( + /* translators: %s: The name of theme to be activated. */ + __( 'Activate %s' ), + previewingThemeName + ); } if ( isSaving ) { From b24532c5f3b3b83a1afb8d39273c290f277a39e6 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Tue, 19 Dec 2023 16:53:49 +0100 Subject: [PATCH 278/325] Update block toolbar and settings sidebar image (#57203) * Update block toolbar and remove settings sidebar image * Update docs/getting-started/fundamentals/block-in-the-editor.md Co-authored-by: Nick Diego <nick.diego@automattic.com> --------- Co-authored-by: Nick Diego <nick.diego@automattic.com> --- docs/getting-started/fundamentals/block-in-the-editor.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/getting-started/fundamentals/block-in-the-editor.md b/docs/getting-started/fundamentals/block-in-the-editor.md index 99f5d26304a7ee..56ba72c283bdf7 100644 --- a/docs/getting-started/fundamentals/block-in-the-editor.md +++ b/docs/getting-started/fundamentals/block-in-the-editor.md @@ -48,9 +48,9 @@ A good workflow when using a component for the Block Editor is: To simplify block customization and ensure a consistent experience for users, there are a number of built-in UI patterns to help generate the editor preview. -### Block Toolbar +![Diagram showing the Block Toolbar and the Settings Sidebar when a Paragraph block is selected](https://developer.wordpress.org/files/2023/12/block-toolbar-settings-sidebar.png) -<img alt="Screenshot of the rich text toolbar applied to a Paragraph block inside the block editor" src="https://developer.wordpress.org/files/2023/12/toolbar-text.png" width="60%"> +### Block Toolbar When the user selects a block, a number of control buttons may be shown in a toolbar above the selected block. Some of these block-level controls may be included automatically but you can also customize the toolbar to include controls specific to your block type. If the return value of your block type's `edit` function includes a `BlockControls` element, those controls will be shown in the selected block's toolbar. @@ -98,8 +98,6 @@ Note that `BlockControls` is only visible when the block is currently selected a ### Settings Sidebar -<img alt="Screenshot of the inspector panel focused on the settings for a Paragraph block" src="https://developer.wordpress.org/files/2023/12/settings-sidebar.png" width="60%"> - The Settings Sidebar is used to display less-often-used settings or settings that require more screen space. The Settings Sidebar should be used for **block-level settings only**. If you have settings that affects only selected content inside a block (example: the "bold" setting for selected text inside a paragraph): **do not place it inside the Settings Sidebar**. The Settings Sidebar is displayed even when editing a block in HTML mode, so it should only contain block-level settings. From c305b3508dcb66a615fd44a0a14333b89a9cc333 Mon Sep 17 00:00:00 2001 From: Andrew Hayward <andrew.hayward@automattic.com> Date: Tue, 19 Dec 2023 16:11:24 +0000 Subject: [PATCH 279/325] Adding unit tests for `useCompositeState` to `Composite` component (#56645) * Tests to allow for migration away from reakit while maintaining reakit functionality --- packages/components/CHANGELOG.md | 4 + .../components/src/composite/test/index.tsx | 576 ++++++++++++++++++ 2 files changed, 580 insertions(+) create mode 100644 packages/components/src/composite/test/index.tsx diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 1db9d8cca26e08..d0acdbd43c6e93 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -89,6 +89,10 @@ - `Slot`: add `style` prop to `bubblesVirtually` version ([#56428](https://github.com/WordPress/gutenberg/pull/56428)) - Introduce experimental new version of `CustomSelectControl` based on `ariakit` ([#55790](https://github.com/WordPress/gutenberg/pull/55790)) +### Code Quality + +- `Composite`: add unit tests for `useCompositeState` ([#56645](https://github.com/WordPress/gutenberg/pull/56645)). + ## 25.12.0 (2023-11-16) ### Bug Fix diff --git a/packages/components/src/composite/test/index.tsx b/packages/components/src/composite/test/index.tsx new file mode 100644 index 00000000000000..02fe6c3d1d60ab --- /dev/null +++ b/packages/components/src/composite/test/index.tsx @@ -0,0 +1,576 @@ +/** + * External dependencies + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +/** + * Internal dependencies + */ +import { + Composite as ReakitComposite, + CompositeGroup as ReakitCompositeGroup, + CompositeItem as ReakitCompositeItem, + useCompositeState as ReakitUseCompositeState, +} from '..'; + +const COMPOSITE_SUITES = { + reakit: { + Composite: ReakitComposite, + CompositeGroup: ReakitCompositeGroup, + CompositeItem: ReakitCompositeItem, + useCompositeState: ReakitUseCompositeState, + }, +}; + +type InitialState = Parameters< typeof ReakitUseCompositeState >[ 0 ]; + +// It was decided not to test the full API, instead opting +// to cover basic usage, with a view to adding broader support +// for the original API should the need arise. As such we are +// only testing here for standard usage. +// See https://github.com/WordPress/gutenberg/pull/56645 + +describe.each( Object.entries( COMPOSITE_SUITES ) )( + 'Validate %s implementation', + ( _, { Composite, CompositeGroup, CompositeItem, useCompositeState } ) => { + function useSpreadProps( initialState?: InitialState ) { + return useCompositeState( initialState ); + } + + function useStateProps( initialState?: InitialState ) { + return { + state: useCompositeState( initialState ), + }; + } + + function OneDimensionalTest( { ...props } ) { + return ( + <Composite + { ...props } + aria-label={ expect.getState().currentTestName } + > + <CompositeItem { ...props }>Item 1</CompositeItem> + <CompositeItem { ...props }>Item 2</CompositeItem> + <CompositeItem { ...props }>Item 3</CompositeItem> + </Composite> + ); + } + + function getOneDimensionalItems() { + return { + item1: screen.getByText( 'Item 1' ), + item2: screen.getByText( 'Item 2' ), + item3: screen.getByText( 'Item 3' ), + }; + } + + function TwoDimensionalTest( { ...props } ) { + return ( + <Composite + { ...props } + aria-label={ expect.getState().currentTestName } + > + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item A1</CompositeItem> + <CompositeItem { ...props }>Item A2</CompositeItem> + <CompositeItem { ...props }>Item A3</CompositeItem> + </CompositeGroup> + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item B1</CompositeItem> + <CompositeItem { ...props }>Item B2</CompositeItem> + <CompositeItem { ...props }>Item B3</CompositeItem> + </CompositeGroup> + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item C1</CompositeItem> + <CompositeItem { ...props }>Item C2</CompositeItem> + <CompositeItem { ...props }>Item C3</CompositeItem> + </CompositeGroup> + </Composite> + ); + } + + function getTwoDimensionalItems() { + return { + itemA1: screen.getByText( 'Item A1' ), + itemA2: screen.getByText( 'Item A2' ), + itemA3: screen.getByText( 'Item A3' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemB3: screen.getByText( 'Item B3' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + itemC3: screen.getByText( 'Item C3' ), + }; + } + + function ShiftTest( { ...props } ) { + return ( + <Composite + { ...props } + aria-label={ expect.getState().currentTestName } + > + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item A1</CompositeItem> + </CompositeGroup> + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item B1</CompositeItem> + <CompositeItem { ...props }>Item B2</CompositeItem> + </CompositeGroup> + <CompositeGroup role="row" { ...props }> + <CompositeItem { ...props }>Item C1</CompositeItem> + <CompositeItem { ...props } disabled> + Item C2 + </CompositeItem> + </CompositeGroup> + </Composite> + ); + } + + function getShiftTestItems() { + return { + itemA1: screen.getByText( 'Item A1' ), + itemB1: screen.getByText( 'Item B1' ), + itemB2: screen.getByText( 'Item B2' ), + itemC1: screen.getByText( 'Item C1' ), + itemC2: screen.getByText( 'Item C2' ), + }; + } + + describe.each( [ + [ 'With spread state', useSpreadProps ], + [ 'With `state` prop', useStateProps ], + ] )( '%s', ( __, useProps ) => { + function useOneDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + <OneDimensionalTest { ...useProps( initialState ) } /> + ); + render( <Test /> ); + return getOneDimensionalItems(); + } + + test( 'Renders as a single tab stop', async () => { + const user = userEvent.setup(); + const Test = () => ( + <> + <button>Before</button> + <OneDimensionalTest { ...useProps() } /> + <button>After</button> + </> + ); + render( <Test /> ); + + await user.tab(); + expect( screen.getByText( 'Before' ) ).toHaveFocus(); + await user.tab(); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + await user.tab(); + expect( screen.getByText( 'After' ) ).toHaveFocus(); + await user.tab( { shift: true } ); + expect( screen.getByText( 'Item 1' ) ).toHaveFocus(); + } ); + + test( 'Excludes disabled items', async () => { + const user = userEvent.setup(); + const Test = () => { + const props = useProps(); + return ( + <Composite + { ...props } + aria-label={ expect.getState().currentTestName } + > + <CompositeItem { ...props }>Item 1</CompositeItem> + <CompositeItem { ...props } disabled> + Item 2 + </CompositeItem> + <CompositeItem { ...props }>Item 3</CompositeItem> + </Composite> + ); + }; + render( <Test /> ); + + const { item1, item2, item3 } = getOneDimensionalItems(); + + expect( item2 ).toBeDisabled(); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).not.toHaveFocus(); + expect( item3 ).toHaveFocus(); + } ); + + test( 'Includes focusable disabled items', async () => { + const user = userEvent.setup(); + const Test = () => { + const props = useProps(); + return ( + <Composite + { ...props } + aria-label={ expect.getState().currentTestName } + > + <CompositeItem { ...props }>Item 1</CompositeItem> + <CompositeItem { ...props } disabled focusable> + Item 2 + </CompositeItem> + <CompositeItem { ...props }>Item 3</CompositeItem> + </Composite> + ); + }; + render( <Test /> ); + const { item1, item2, item3 } = getOneDimensionalItems(); + + expect( item2 ).toBeEnabled(); + expect( item2 ).toHaveAttribute( 'aria-disabled', 'true' ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + expect( item3 ).not.toHaveFocus(); + } ); + + test( 'Supports `baseId`', async () => { + const { item1, item2, item3 } = useOneDimensionalTest( { + baseId: 'test-id', + } ); + + expect( item1.id ).toMatch( 'test-id-1' ); + expect( item2.id ).toMatch( 'test-id-2' ); + expect( item3.id ).toMatch( 'test-id-3' ); + } ); + + test( 'Supports `currentId`', async () => { + const user = userEvent.setup(); + const { item2 } = useOneDimensionalTest( { + baseId: 'test-id', + currentId: 'test-id-2', + } ); + + await user.tab(); + expect( item2 ).toHaveFocus(); + } ); + } ); + + describe.each( [ + [ + 'When LTR', + false, + { previous: 'ArrowLeft', next: 'ArrowRight' }, + ], + [ 'When RTL', true, { previous: 'ArrowRight', next: 'ArrowLeft' } ], + ] )( '%s', ( _when, rtl, { previous, next } ) => { + function useOneDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + <OneDimensionalTest { ...useStateProps( initialState ) } /> + ); + render( <Test /> ); + return getOneDimensionalItems(); + } + + function useTwoDimensionalTest( initialState?: InitialState ) { + const Test = () => ( + <TwoDimensionalTest { ...useStateProps( initialState ) } /> + ); + render( <Test /> ); + return getTwoDimensionalItems(); + } + + function useShiftTest( shift: boolean ) { + const Test = () => ( + <ShiftTest { ...useStateProps( { rtl, shift } ) } /> + ); + render( <Test /> ); + return getShiftTestItems(); + } + + describe( 'In one dimension', () => { + test( 'All directions work with no orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only left/right work with horizontal orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + orientation: 'horizontal', + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Only up/down work with vertical orientation', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + orientation: 'vertical', + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( item1 ).toHaveFocus(); + } ); + + test( 'Focus wraps with loop enabled', async () => { + const user = userEvent.setup(); + const { item1, item2, item3 } = useOneDimensionalTest( { + rtl, + loop: true, + } ); + + await user.tab(); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( item1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( item3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( item1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( item3 ).toHaveFocus(); + } ); + } ); + + describe( 'In two dimensions', () => { + test( 'All directions work as standard', async () => { + const user = userEvent.setup(); + const { + itemA1, + itemA2, + itemA3, + itemB1, + itemB2, + itemC1, + itemC3, + } = useTwoDimensionalTest( { rtl } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[End]' ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[PageDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[Home]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[PageUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '{Control>}[Home]{/Control}' ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus wraps around rows/columns with loop enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { rtl, loop: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus moves between rows/columns with wrap enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemA2, itemA3, itemB1, itemC1, itemC3 } = + useTwoDimensionalTest( { rtl, wrap: true } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA2 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '{Control>}[End]{/Control}' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemC3 ).toHaveFocus(); + } ); + + test( 'Focus wraps around start/end with loop and wrap enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemC3 } = useTwoDimensionalTest( { + rtl, + loop: true, + wrap: true, + } ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( `[${ previous }]` ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + expect( itemC3 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemA1 ).toHaveFocus(); + } ); + + test( 'Focus shifts if vertical neighbour unavailable when shift enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2, itemC1 } = + useShiftTest( true ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemC1 ).toHaveFocus(); + } ); + + test( 'Focus does not shift if vertical neighbour unavailable when shift not enabled', async () => { + const user = userEvent.setup(); + const { itemA1, itemB1, itemB2 } = useShiftTest( false ); + + await user.tab(); + expect( itemA1 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + expect( itemB1 ).toHaveFocus(); + await user.keyboard( `[${ next }]` ); + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowUp]' ); + // A2 doesn't exist + expect( itemB2 ).toHaveFocus(); + await user.keyboard( '[ArrowDown]' ); + // C2 is disabled + expect( itemB2 ).toHaveFocus(); + } ); + } ); + } ); + } +); From 1dedeab82c51ccdd1062a29aeac248a9dcf02689 Mon Sep 17 00:00:00 2001 From: Lena Morita <lena@jaguchi.com> Date: Wed, 20 Dec 2023 01:37:04 +0900 Subject: [PATCH 280/325] Snackbar: Remove `__unstableHTML` prop from TS (#57218) * Snackbar: Remove `__unstableHTML` prop from TS * Add changelog --- packages/components/CHANGELOG.md | 1 + packages/components/src/snackbar/types.ts | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index d0acdbd43c6e93..eccd9e639e5f20 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -9,6 +9,7 @@ - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). - `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). +- `Snackbar`: Remove erroneous `__unstableHTML` prop from TypeScript definitions ([#57218](https://github.com/WordPress/gutenberg/pull/57218)). ### Enhancements diff --git a/packages/components/src/snackbar/types.ts b/packages/components/src/snackbar/types.ts index 71ded92e5b7d11..539c4c3ebdf65e 100644 --- a/packages/components/src/snackbar/types.ts +++ b/packages/components/src/snackbar/types.ts @@ -28,7 +28,8 @@ type SnackbarOnlyProps = { listRef?: MutableRefObject< HTMLDivElement | null >; }; -export type SnackbarProps = NoticeProps & SnackbarOnlyProps; +export type SnackbarProps = Omit< NoticeProps, '__unstableHTML' > & + SnackbarOnlyProps; export type SnackbarListProps = { notices: Array< From 85f8b264029ce8ed897af140770d885cd51a4709 Mon Sep 17 00:00:00 2001 From: Yuliyan Slavchev <yuliyan.slavchev@gmail.com> Date: Tue, 19 Dec 2023 19:11:41 +0200 Subject: [PATCH 281/325] Editor: Use visibility selector for PostTemplatePanel (#57224) --- packages/editor/src/components/post-template/panel.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/editor/src/components/post-template/panel.js b/packages/editor/src/components/post-template/panel.js index 8fcaeec8f3a3b2..bbad1cb135fd98 100644 --- a/packages/editor/src/components/post-template/panel.js +++ b/packages/editor/src/components/post-template/panel.js @@ -23,8 +23,7 @@ export default function PostTemplatePanel() { }; }, [] ); - const isVisible = true; - useSelect( ( select ) => { + const isVisible = useSelect( ( select ) => { const postTypeSlug = select( editorStore ).getCurrentPostType(); const postType = select( coreStore ).getPostType( postTypeSlug ); if ( ! postType?.viewable ) { From b13ae584892ff493c0aedc8c0218b7b62e534cf4 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Tue, 19 Dec 2023 20:12:36 +0100 Subject: [PATCH 282/325] Add new section on markup representation of a block (#57230) --- docs/getting-started/fundamentals/README.md | 1 + .../markup-representation-block.md | 44 +++++++++++++++++++ docs/manifest.json | 6 +++ docs/toc.json | 3 ++ 4 files changed, 54 insertions(+) create mode 100644 docs/getting-started/fundamentals/markup-representation-block.md diff --git a/docs/getting-started/fundamentals/README.md b/docs/getting-started/fundamentals/README.md index 4fde0f3a0d1009..799ff89aa39419 100644 --- a/docs/getting-started/fundamentals/README.md +++ b/docs/getting-started/fundamentals/README.md @@ -9,4 +9,5 @@ In this section, you will learn: 1. [**Registration of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block) - How a block is registered in both the server and the client. 1. [**Block wrapper**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-wrapper) - How to set proper attributes to the block's markup wrapper. 1. [**The block in the Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/block-in-the-editor) - The block as a React component loaded in the Block Editor and its possibilities. +1. [**Markup representation of a block**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block) - How blocks are represented in the DB or in templates. 1. [**Javascript in the Block Editor**](https://developer.wordpress.org/block-editor/getting-started/fundamentals/javascript-in-the-block-editor) - How to work with Javascript for the Block Editor. \ No newline at end of file diff --git a/docs/getting-started/fundamentals/markup-representation-block.md b/docs/getting-started/fundamentals/markup-representation-block.md new file mode 100644 index 00000000000000..20289f8f228ce8 --- /dev/null +++ b/docs/getting-started/fundamentals/markup-representation-block.md @@ -0,0 +1,44 @@ +# Markup representation of a block + +When stored, in the database (DB) or in templates as HTML files, blocks are represented using a [specific HTML grammar](https://developer.wordpress.org/block-editor/explanations/architecture/key-concepts/#data-and-attributes), which is technically valid HTML based on HTML comments that act as explicit block delimiters + +These are some of the rules for the markup used to represent a block: +- All core block comments start with a prefix and the block name: `wp:blockname` +- For custom blocks, `blockname` is `namespace/blockname` +- The comment can be a single line, self-closing, or wrapper for HTML content. +- Custom block settings and attributes are stored as a JSON object inside the block comment. + +_Example: Markup representation of an `image` core block_ + +``` +<!-- wp:image --> +<figure class="wp-block-image"><img src="source.jpg" alt="" /></figure> +<!-- /wp:image --> +``` + +The [markup representation of a block is parsed for the Block Editor](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/) and the block's output for the front end: +- In the editor, WordPress parses this block markup, captures its data and loads its `edit` version +- In the front end, WordPress parses this block markup, captures its data and generates its final HTML markup + +Whenever a block is saved, the `save` function, defined when the [block is registered in the client](https://developer.wordpress.org/block-editor/getting-started/fundamentals/registration-of-a-block/#registration-of-the-block-with-javascript-client-side), is called to return the markup that will be saved into the database within the block delimiter's comment. If `save` is `null` (common case for blocks with dynamic rendering), only a single line block delimiter's comment is stored, along with any attributes + +The Post Editor checks that the markup created by the `save` function is identical to the block's markup saved to the database: +- If there are any differences, the Post Editor trigger a **block validation error**. +- Block validation errors usually happen when a block’s `save` function is updated to change the markup produced by the block. +- A block developer can mitigate these issues by adding a [**block deprecation**](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-deprecation/) to register the change in the block. + +The markup of a **block with dynamic rendering** is expected to change so the markup of these blocks is not saved to the database. What is saved in the DB as representation of the block, for blocks with dynamic rendering, is a single line of HTML consisting on just the block delimiter's comment (including block attributes values). That HTML is not subject to the Post Editor’s validation. + +_Example: Markup representation of a block with dynamic rendering (`save` = `null`) and attributes_ + + +```html +<!-- wp:latest-posts {"postsToShow":4,"displayPostDate":true} /--> +``` + +## Additional Resources + +- [Data Flow and Data Format](https://developer.wordpress.org/block-editor/explanations/architecture/data-flow/) +- [Static vs. dynamic blocks: What’s the difference?](https://developer.wordpress.org/news/2023/02/27/static-vs-dynamic-blocks-whats-the-difference/) +- [Block deprecation – a tutorial](https://developer.wordpress.org/news/2023/03/10/block-deprecation-a-tutorial/) +- [Introduction to Templates > Block markup](https://developer.wordpress.org/themes/templates/introduction-to-templates/#block-markup) | Theme Handbook \ No newline at end of file diff --git a/docs/manifest.json b/docs/manifest.json index fb6d8550fa7ec9..7bb1e847ce03fd 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -89,6 +89,12 @@ "markdown_source": "../docs/getting-started/fundamentals/block-in-the-editor.md", "parent": "fundamentals" }, + { + "title": "Markup representation of a block", + "slug": "markup-representation-block", + "markdown_source": "../docs/getting-started/fundamentals/markup-representation-block.md", + "parent": "fundamentals" + }, { "title": "Working with Javascript for the Block Editor", "slug": "javascript-in-the-block-editor", diff --git a/docs/toc.json b/docs/toc.json index 4b4f5bbd69a5f6..961fc88fae4f52 100644 --- a/docs/toc.json +++ b/docs/toc.json @@ -39,6 +39,9 @@ { "docs/getting-started/fundamentals/block-in-the-editor.md": [] }, + { + "docs/getting-started/fundamentals/markup-representation-block.md": [] + }, { "docs/getting-started/fundamentals/javascript-in-the-block-editor.md": [] } From c868408adeb8db7b440b8015fea674aa714bed5e Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Tue, 19 Dec 2023 14:14:26 -0600 Subject: [PATCH 283/325] Fix block lock toolbar item stealing focus when mounted with StrictMode (#57185) --- .../src/components/block-lock/toolbar.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/block-lock/toolbar.js b/packages/block-editor/src/components/block-lock/toolbar.js index 5ba08b08846a4a..14a941a9011b6d 100644 --- a/packages/block-editor/src/components/block-lock/toolbar.js +++ b/packages/block-editor/src/components/block-lock/toolbar.js @@ -23,6 +23,7 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { const lockButtonRef = useRef( null ); const isFirstRender = useRef( true ); + const hasModalOpened = useRef( false ); const shouldHideBlockLockUI = ! canLock || ( canEdit && canMove && canRemove ); @@ -36,7 +37,19 @@ export default function BlockLockToolbar( { clientId, wrapperRef } ) { return; } - if ( ! isModalOpen && shouldHideBlockLockUI ) { + if ( isModalOpen && ! hasModalOpened.current ) { + hasModalOpened.current = true; + } + + // We only want to allow this effect to happen if the modal has been opened. + // The issue is when we're returning focus from the block lock modal to a toolbar, + // so it can only happen after a modal has been opened. Without this, the toolbar + // will steal focus on rerenders. + if ( + hasModalOpened.current && + ! isModalOpen && + shouldHideBlockLockUI + ) { focus.focusable .find( wrapperRef.current, { sequential: false, From 50f2ae28bcb095ce7e2fe0b81e09149666159292 Mon Sep 17 00:00:00 2001 From: tellthemachines <tellthemachines@users.noreply.github.com> Date: Wed, 20 Dec 2023 09:30:16 +1100 Subject: [PATCH 284/325] Allow default duotone styles in classic themes. (#57191) --- lib/class-wp-theme-json-resolver-gutenberg.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/class-wp-theme-json-resolver-gutenberg.php b/lib/class-wp-theme-json-resolver-gutenberg.php index 950d9e00e6243f..189d411db2257f 100644 --- a/lib/class-wp-theme-json-resolver-gutenberg.php +++ b/lib/class-wp-theme-json-resolver-gutenberg.php @@ -322,9 +322,6 @@ public static function get_theme_data( $deprecated = array(), $options = array() } $theme_support_data['settings']['color']['defaultGradients'] = $default_gradients; - // Classic themes without a theme.json don't support global duotone. - $theme_support_data['settings']['color']['defaultDuotone'] = false; - // Allow themes to enable all border settings via theme_support. if ( current_theme_supports( 'border' ) ) { $theme_support_data['settings']['border']['color'] = true; From c0f6e23f1c4cb49eb6fe49b810fdb824015a23ec Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 19 Dec 2023 17:40:54 -0500 Subject: [PATCH 285/325] Components: add unit test `__experimentalExpandOnFocus` unit tests for `FormTokenField` (#57122) * add unit test * add tests for suggestion visibility after selection --- .../src/form-token-field/test/index.tsx | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/packages/components/src/form-token-field/test/index.tsx b/packages/components/src/form-token-field/test/index.tsx index 76e308d5993beb..961214a574c90d 100644 --- a/packages/components/src/form-token-field/test/index.tsx +++ b/packages/components/src/form-token-field/test/index.tsx @@ -741,6 +741,103 @@ describe( 'FormTokenField', () => { ] ); } ); + it( 'should render suggestions after a selection is made when the `__experimentalExpandOnFocus` prop is set to `true`', async () => { + const user = userEvent.setup(); + + const onFocusSpy = jest.fn(); + + const suggestions = [ 'Green', 'Emerald', 'Seaweed' ]; + + render( + <> + <FormTokenFieldWithState + onFocus={ onFocusSpy } + suggestions={ suggestions } + __experimentalExpandOnFocus + /> + </> + ); + + const input = screen.getByRole( 'combobox' ); + + await user.type( input, 'ee' ); + + expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [ + 'Green', + 'Seaweed', + ] ); + + // Select the first suggestion ("Green") + await user.keyboard( '[ArrowDown][Enter]' ); + + expect( screen.getByRole( 'listbox' ) ).toBeVisible(); + } ); + + it( 'should not render suggestions after a selection is made when the `__experimentalExpandOnFocus` prop is set to `false` or not defined', async () => { + const user = userEvent.setup(); + + const onFocusSpy = jest.fn(); + + const suggestions = [ 'Green', 'Emerald', 'Seaweed' ]; + + render( + <> + <FormTokenFieldWithState + onFocus={ onFocusSpy } + suggestions={ suggestions } + /> + </> + ); + + const input = screen.getByRole( 'combobox' ); + + await user.type( input, 'ee' ); + + expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [ + 'Green', + 'Seaweed', + ] ); + + // Select the first suggestion ("Green") + await user.keyboard( '[ArrowDown][Enter]' ); + + expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument(); + } ); + + it( 'should not render suggestions after the input is blurred', async () => { + const user = userEvent.setup(); + + const onFocusSpy = jest.fn(); + + const suggestions = [ 'Green', 'Emerald', 'Seaweed' ]; + + render( + <> + <FormTokenFieldWithState + onFocus={ onFocusSpy } + suggestions={ suggestions } + /> + </> + ); + + const input = screen.getByRole( 'combobox' ); + + await user.type( input, 'ee' ); + + expectVisibleSuggestionsToBe( screen.getByRole( 'listbox' ), [ + 'Green', + 'Seaweed', + ] ); + + // Select the first suggestion ("Green") + await user.keyboard( '[ArrowDown][Enter]' ); + + // Click the body, blurring the input. + await user.click( document.body ); + + expect( screen.queryByRole( 'listbox' ) ).not.toBeInTheDocument(); + } ); + it( 'should not render suggestions if the text input is not matching any of the suggestions', async () => { const user = userEvent.setup(); From 6630e0a6721ce507e39bf8ee4696695be27b803a Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Tue, 19 Dec 2023 18:15:30 -0500 Subject: [PATCH 286/325] Components: replace `TabPanel` with `Tabs` in the Block Inspector (#56995) * implement `Tabs` * focusable false * add `show-icon-labels` support * address feedback --- .../inspector-controls-tabs/index.js | 67 +++++++++++-------- .../inspector-controls-tabs/style.scss | 2 +- 2 files changed, 40 insertions(+), 29 deletions(-) diff --git a/packages/block-editor/src/components/inspector-controls-tabs/index.js b/packages/block-editor/src/components/inspector-controls-tabs/index.js index de192050d05cb2..944ce6f3220937 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/index.js +++ b/packages/block-editor/src/components/inspector-controls-tabs/index.js @@ -1,7 +1,10 @@ /** * WordPress dependencies */ -import { TabPanel } from '@wordpress/components'; +import { + Button, + privateApis as componentsPrivateApis, +} from '@wordpress/components'; /** * Internal dependencies @@ -11,6 +14,9 @@ import SettingsTab from './settings-tab'; import StylesTab from './styles-tab'; import InspectorControls from '../inspector-controls'; import useIsListViewTabDisabled from './use-is-list-view-tab-disabled'; +import { unlock } from '../../lock-unlock'; + +const { Tabs } = unlock( componentsPrivateApis ); export default function InspectorControlsTabs( { blockName, @@ -26,34 +32,39 @@ export default function InspectorControlsTabs( { const initialTabName = ! useIsListViewTabDisabled( blockName ) ? TAB_LIST_VIEW.name : undefined; - return ( - <TabPanel - className="block-editor-block-inspector__tabs" - tabs={ tabs } - initialTabName={ initialTabName } - key={ clientId } - > - { ( tab ) => { - if ( tab.name === TAB_SETTINGS.name ) { - return ( - <SettingsTab showAdvancedControls={ !! blockName } /> - ); - } - if ( tab.name === TAB_STYLES.name ) { - return ( - <StylesTab - blockName={ blockName } - clientId={ clientId } - hasBlockStyles={ hasBlockStyles } + return ( + <div className="block-editor-block-inspector__tabs"> + <Tabs initialTabId={ initialTabName } key={ clientId }> + <Tabs.TabList> + { tabs.map( ( tab ) => ( + <Tabs.Tab + key={ tab.name } + tabId={ tab.name } + render={ + <Button + icon={ tab.icon } + label={ tab.title } + className={ tab.className } + /> + } /> - ); - } - - if ( tab.name === TAB_LIST_VIEW.name ) { - return <InspectorControls.Slot group="list" />; - } - } } - </TabPanel> + ) ) } + </Tabs.TabList> + <Tabs.TabPanel tabId={ TAB_SETTINGS.name } focusable={ false }> + <SettingsTab showAdvancedControls={ !! blockName } /> + </Tabs.TabPanel> + <Tabs.TabPanel tabId={ TAB_STYLES.name } focusable={ false }> + <StylesTab + blockName={ blockName } + clientId={ clientId } + hasBlockStyles={ hasBlockStyles } + /> + </Tabs.TabPanel> + <Tabs.TabPanel tabId={ TAB_LIST_VIEW.name } focusable={ false }> + <InspectorControls.Slot group="list" /> + </Tabs.TabPanel> + </Tabs> + </div> ); } diff --git a/packages/block-editor/src/components/inspector-controls-tabs/style.scss b/packages/block-editor/src/components/inspector-controls-tabs/style.scss index da83073a45590a..6db9395af62ef6 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/style.scss +++ b/packages/block-editor/src/components/inspector-controls-tabs/style.scss @@ -1,5 +1,5 @@ .show-icon-labels { - .block-editor-block-inspector__tabs .components-tab-panel__tabs { + .block-editor-block-inspector__tabs [role="tablist"] { .components-button.has-icon { // Hide the button icons when labels are set to display... svg { From 6bbf33c1eeb634ed404ef8505318852308f0a627 Mon Sep 17 00:00:00 2001 From: Damon Cook <colorful-tones@users.noreply.github.com> Date: Tue, 19 Dec 2023 19:17:25 -0500 Subject: [PATCH 287/325] Fix vertical overflow when inserter is open in post and site editor (#57127) * Toggle classname for inserter * Adjust overflow when inserter is open * Revert "Toggle classname for inserter" This reverts commit 95f0a7264e179564a97f42fe8a7c8e1f56550793. * Revert "Adjust overflow when inserter is open" This reverts commit 40fba9e36cbc2f834d6850862fce6f1cd202b9d9. * Fix vertical overflow for inserter main area Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --------- Co-authored-by: Aki Hamano <54422211+t-hamano@users.noreply.github.com> --- packages/block-editor/src/components/inserter/style.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/block-editor/src/components/inserter/style.scss b/packages/block-editor/src/components/inserter/style.scss index 97a3d877b7e72a..dd0cc50e31fcb4 100644 --- a/packages/block-editor/src/components/inserter/style.scss +++ b/packages/block-editor/src/components/inserter/style.scss @@ -22,6 +22,8 @@ $block-inserter-tabs-height: 44px; flex-direction: column; height: 100%; gap: $grid-unit-20; + overflow-y: hidden; + &.show-as-tabs { gap: 0; } From b080c298ac6449b70ab469baa70dc327b71d6e2f Mon Sep 17 00:00:00 2001 From: Glen Davies <glendaviesnz@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:51:29 +1300 Subject: [PATCH 288/325] Gallery block: combine useSelect calls (#57240) --- packages/block-library/src/gallery/edit.js | 50 +++++++++++----------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/block-library/src/gallery/edit.js b/packages/block-library/src/gallery/edit.js index e73e1e76b9c5f0..03fc15d19eedcd 100644 --- a/packages/block-library/src/gallery/edit.js +++ b/packages/block-library/src/gallery/edit.js @@ -74,6 +74,8 @@ const MOBILE_CONTROL_PROPS_RANGE_CONTROL = Platform.isNative ? { type: 'stepper' } : {}; +const EMPTY_ARRAY = []; + function GalleryEdit( props ) { const { setAttributes, @@ -97,33 +99,29 @@ function GalleryEdit( props ) { const { createSuccessNotice, createErrorNotice } = useDispatch( noticesStore ); - const { getBlock, getSettings, preferredStyle } = useSelect( ( select ) => { - const settings = select( blockEditorStore ).getSettings(); - const preferredStyleVariations = - settings.__experimentalPreferredStyleVariations; - return { - getBlock: select( blockEditorStore ).getBlock, - getSettings: select( blockEditorStore ).getSettings, - preferredStyle: preferredStyleVariations?.value?.[ 'core/image' ], - }; - }, [] ); - - const innerBlockImages = useSelect( - ( select ) => { - const innerBlocks = - select( blockEditorStore ).getBlock( clientId )?.innerBlocks ?? - []; - return innerBlocks; - }, - [ clientId ] - ); - - const wasBlockJustInserted = useSelect( + const { + getBlock, + getSettings, + preferredStyle, + innerBlockImages, + wasBlockJustInserted, + } = useSelect( ( select ) => { - return select( blockEditorStore ).wasBlockJustInserted( - clientId, - 'inserter_menu' - ); + const settings = select( blockEditorStore ).getSettings(); + const preferredStyleVariations = + settings.__experimentalPreferredStyleVariations; + return { + getBlock: select( blockEditorStore ).getBlock, + getSettings: select( blockEditorStore ).getSettings, + preferredStyle: + preferredStyleVariations?.value?.[ 'core/image' ], + innerBlockImages: + select( blockEditorStore ).getBlock( clientId ) + ?.innerBlocks ?? EMPTY_ARRAY, + wasBlockJustInserted: select( + blockEditorStore + ).wasBlockJustInserted( clientId, 'inserter_menu' ), + }; }, [ clientId ] ); From c86c37d692ccde93440d74cfe4bf433d63b52c5c Mon Sep 17 00:00:00 2001 From: tellthemachines <tellthemachines@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:35:25 +1100 Subject: [PATCH 289/325] Make sure theme color palette presets are output when appearance tools are enabled. (#57190) * Make sure theme color palette preset styles are output. * Check for color palette support * Also check for border support. --- lib/global-styles-and-settings.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/global-styles-and-settings.php b/lib/global-styles-and-settings.php index 03c8de10a89251..7a7a91e7368664 100644 --- a/lib/global-styles-and-settings.php +++ b/lib/global-styles-and-settings.php @@ -72,7 +72,13 @@ function gutenberg_get_global_stylesheet( $types = array() ) { * @see wp_add_global_styles_for_blocks */ $origins = array( 'default', 'theme', 'custom' ); - if ( ! $supports_theme_json ) { + /* + * If the theme doesn't have theme.json but supports both appearance tools and color palette, + * the 'theme' origin should be included so color palette presets are also output. + */ + if ( ! $supports_theme_json && ( current_theme_supports( 'appearance-tools' ) || current_theme_supports( 'border' ) ) && current_theme_supports( 'editor-color-palette' ) ) { + $origins = array( 'default', 'theme' ); + } elseif ( ! $supports_theme_json ) { $origins = array( 'default' ); } $styles_rest = $tree->get_stylesheet( $types, $origins ); From fda1efc7e733e4e2f0cdddb0ebb6843ac31db764 Mon Sep 17 00:00:00 2001 From: Mitchell Austin <mr.fye@oneandthesame.net> Date: Tue, 19 Dec 2023 22:28:45 -0800 Subject: [PATCH 290/325] `Modal`: Improve application of body class names (#55430) * Add unit tests for body class name effects * Fix and enhance body class attribute effect * Add changelog entry --- packages/components/CHANGELOG.md | 1 + packages/components/src/modal/index.tsx | 32 +++---- packages/components/src/modal/test/index.tsx | 91 +++++++++++++++++++- 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index eccd9e639e5f20..ecf4d3628c0839 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -14,6 +14,7 @@ ### Enhancements - `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). +- `Modal`: Improve application of body class names ([#55430](https://github.com/WordPress/gutenberg/pull/55430)). ### Experimental diff --git a/packages/components/src/modal/index.tsx b/packages/components/src/modal/index.tsx index 041c592166ab73..b1bee51805f782 100644 --- a/packages/components/src/modal/index.tsx +++ b/packages/components/src/modal/index.tsx @@ -43,12 +43,12 @@ import StyleProvider from '../style-provider'; import type { ModalProps } from './types'; // Used to track and dismiss the prior modal when another opens unless nested. -const level0Dismissers: MutableRefObject< - ModalProps[ 'onRequestClose' ] | undefined ->[] = []; -const ModalContext = createContext( level0Dismissers ); +const ModalContext = createContext< + MutableRefObject< ModalProps[ 'onRequestClose' ] | undefined >[] +>( [] ); -let isBodyOpenClassActive = false; +// Used to track body class names applied while modals are open. +const bodyOpenClasses = new Map< string, number >(); function UnforwardedModal( props: ModalProps, @@ -146,7 +146,7 @@ function UnforwardedModal( // one should remain open at a time and the list enables closing prior ones. const dismissers = useContext( ModalContext ); // Used for the tracking and dismissing any nested modals. - const nestedDismissers = useRef< typeof level0Dismissers >( [] ); + const nestedDismissers = useRef< typeof dismissers >( [] ); // Updates the stack tracking open modals at this level and calls // onRequestClose for any prior and/or nested modals as applicable. @@ -162,20 +162,22 @@ function UnforwardedModal( }; }, [ dismissers ] ); - const isLevel0 = dismissers === level0Dismissers; // Adds/removes the value of bodyOpenClassName to body element. useEffect( () => { - if ( ! isBodyOpenClassActive ) { - isBodyOpenClassActive = true; - document.body.classList.add( bodyOpenClassName ); - } + const theClass = bodyOpenClassName; + const oneMore = 1 + ( bodyOpenClasses.get( theClass ) ?? 0 ); + bodyOpenClasses.set( theClass, oneMore ); + document.body.classList.add( bodyOpenClassName ); return () => { - if ( isLevel0 && dismissers.length === 0 ) { - document.body.classList.remove( bodyOpenClassName ); - isBodyOpenClassActive = false; + const oneLess = bodyOpenClasses.get( theClass )! - 1; + if ( oneLess === 0 ) { + document.body.classList.remove( theClass ); + bodyOpenClasses.delete( theClass ); + } else { + bodyOpenClasses.set( theClass, oneLess ); } }; - }, [ bodyOpenClassName, dismissers, isLevel0 ] ); + }, [ bodyOpenClassName ] ); // Calls the isContentScrollable callback when the Modal children container resizes. useLayoutEffect( () => { diff --git a/packages/components/src/modal/test/index.tsx b/packages/components/src/modal/test/index.tsx index 9073735e94dbe9..99f68345eec90c 100644 --- a/packages/components/src/modal/test/index.tsx +++ b/packages/components/src/modal/test/index.tsx @@ -7,7 +7,7 @@ import userEvent from '@testing-library/user-event'; /** * WordPress dependencies */ -import { useState } from '@wordpress/element'; +import { useEffect, useState } from '@wordpress/element'; /** * Internal dependencies @@ -388,4 +388,93 @@ describe( 'Modal', () => { expect( opener ).toHaveFocus(); } ); } ); + + describe( 'Body class name', () => { + const overrideClass = 'is-any-open'; + const BodyClassDemo = () => { + const [ isAShown, setIsAShown ] = useState( false ); + const [ isA1Shown, setIsA1Shown ] = useState( false ); + const [ isBShown, setIsBShown ] = useState( false ); + const [ isClassOverriden, setIsClassOverriden ] = useState( false ); + useEffect( () => { + const toggles: ( e: KeyboardEvent ) => void = ( { + key, + metaKey, + } ) => { + if ( key === 'a' ) { + if ( metaKey ) return setIsA1Shown( ( v ) => ! v ); + return setIsAShown( ( v ) => ! v ); + } + if ( key === 'b' ) return setIsBShown( ( v ) => ! v ); + if ( key === 'c' ) + return setIsClassOverriden( ( v ) => ! v ); + }; + document.addEventListener( 'keydown', toggles ); + return () => + void document.removeEventListener( 'keydown', toggles ); + }, [] ); + return ( + <> + { isAShown && ( + <Modal + bodyOpenClassName={ + isClassOverriden ? overrideClass : 'is-A-open' + } + onRequestClose={ () => setIsAShown( false ) } + > + <p>Modal A contents</p> + { isA1Shown && ( + <Modal + title="Nested" + onRequestClose={ () => + setIsA1Shown( false ) + } + > + <p>Modal A1 contents</p> + </Modal> + ) } + </Modal> + ) } + { isBShown && ( + <Modal + bodyOpenClassName={ + isClassOverriden ? overrideClass : 'is-B-open' + } + onRequestClose={ () => setIsBShown( false ) } + > + <p>Modal B contents</p> + </Modal> + ) } + </> + ); + }; + + it( 'is added and removed when modal opens and closes including when closed due to another modal opening', async () => { + const user = userEvent.setup(); + + const { baseElement } = render( <BodyClassDemo /> ); + + await user.keyboard( 'a' ); // Opens modal A. + expect( baseElement ).toHaveClass( 'is-A-open' ); + + await user.keyboard( 'b' ); // Opens modal B > closes modal A. + expect( baseElement ).toHaveClass( 'is-B-open' ); + expect( baseElement ).not.toHaveClass( 'is-A-open' ); + + await user.keyboard( 'b' ); // Closes modal B. + expect( baseElement ).not.toHaveClass( 'is-B-open' ); + } ); + + it( 'is removed even when prop changes while nested modal is open', async () => { + const user = userEvent.setup(); + + const { baseElement } = render( <BodyClassDemo /> ); + + await user.keyboard( 'a' ); // Opens modal A. + await user.keyboard( '{Meta>}a{/Meta}' ); // Opens nested modal. + await user.keyboard( 'c' ); // Changes `bodyOpenClassName`. + await user.keyboard( 'a' ); // Closes modal A. + expect( baseElement ).not.toHaveClass( 'is-A-open' ); + } ); + } ); } ); From 95e140e4ea7cf24f73d9aed9a8ef26cc85be7a79 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 20 Dec 2023 08:56:06 +0200 Subject: [PATCH 291/325] Rich text: avoid block editor subscription if not selected (#57226) --- .../src/components/rich-text/index.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index a3b7b44e214a5b..ef21a8aa4ab239 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -113,8 +113,14 @@ export function RichTextWrapper( props = removeNativeProps( props ); const anchorRef = useRef(); - const { clientId } = useBlockEditContext(); + const { clientId, isSelected: isBlockSelected } = useBlockEditContext(); const selector = ( select ) => { + // Avoid subscribing to the block editor store if the block is not + // selected. + if ( ! isBlockSelected ) { + return { isSelected: false }; + } + const { getSelectionStart, getSelectionEnd } = select( blockEditorStore ); const selectionStart = getSelectionStart(); @@ -137,10 +143,12 @@ export function RichTextWrapper( isSelected, }; }; - // This selector must run on every render so the right selection state is - // retrieved from the store on merge. - // To do: fix this somehow. - const { selectionStart, selectionEnd, isSelected } = useSelect( selector ); + const { selectionStart, selectionEnd, isSelected } = useSelect( selector, [ + clientId, + identifier, + originalIsSelected, + isBlockSelected, + ] ); const { getSelectionStart, getSelectionEnd, getBlockRootClientId } = useSelect( blockEditorStore ); const { selectionChange } = useDispatch( blockEditorStore ); From b924779de8a91231f8109a4eaa0dd620d4182727 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 20 Dec 2023 08:20:34 +0100 Subject: [PATCH 292/325] Override all the labels of the pattern categories taxonomy (#57094) Co-authored-by: Nik Tsekouras <ntsekouras@outlook.com> --- lib/compat/wordpress-6.4/block-patterns.php | 36 ----------------- lib/compat/wordpress-6.5/block-patterns.php | 45 +++++++++++++++++++++ lib/load.php | 1 - 3 files changed, 45 insertions(+), 37 deletions(-) delete mode 100644 lib/compat/wordpress-6.4/block-patterns.php diff --git a/lib/compat/wordpress-6.4/block-patterns.php b/lib/compat/wordpress-6.4/block-patterns.php deleted file mode 100644 index 65c31fb7a22af4..00000000000000 --- a/lib/compat/wordpress-6.4/block-patterns.php +++ /dev/null @@ -1,36 +0,0 @@ -<?php -/** - * Overrides Core's wp-includes/block-patterns.php to add new wp_patterns taxonomy for WP 6.4. - * - * @package gutenberg - */ - -/** - * Adds a new taxonomy for organizing user created patterns. - * - * Note: This should be removed when the minimum required WP version is >= 6.4. - * - * @see https://github.com/WordPress/gutenberg/pull/53163 - * - * @return void - */ -function gutenberg_register_taxonomy_patterns() { - $args = array( - 'public' => false, - 'publicly_queryable' => false, - 'hierarchical' => false, - 'labels' => array( - 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), - 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), - ), - 'query_var' => false, - 'rewrite' => false, - 'show_ui' => true, - '_builtin' => true, - 'show_in_nav_menus' => false, - 'show_in_rest' => true, - 'show_admin_column' => true, - ); - register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); -} -add_action( 'init', 'gutenberg_register_taxonomy_patterns' ); diff --git a/lib/compat/wordpress-6.5/block-patterns.php b/lib/compat/wordpress-6.5/block-patterns.php index 4521d5e4e578f3..87756ea45c7d00 100644 --- a/lib/compat/wordpress-6.5/block-patterns.php +++ b/lib/compat/wordpress-6.5/block-patterns.php @@ -31,3 +31,48 @@ function gutenberg_register_media_pattern_categories() { ); } add_action( 'init', 'gutenberg_register_media_pattern_categories' ); + +/** + * Adds a new taxonomy for organizing user created patterns. + * + * @see https://github.com/WordPress/gutenberg/pull/53163 + * + * @return void + */ +function gutenberg_register_taxonomy_patterns() { + $args = array( + 'public' => false, + 'publicly_queryable' => false, + 'hierarchical' => false, + 'labels' => array( + 'name' => _x( 'Pattern Categories', 'taxonomy general name' ), + 'singular_name' => _x( 'Pattern Category', 'taxonomy singular name' ), + 'add_new_item' => __( 'Add New Category' ), + 'add_or_remove_items' => __( 'Add or remove pattern categories' ), + 'back_to_items' => __( '&larr; Go to pattern categories' ), + 'choose_from_most_used' => __( 'Choose from the most used pattern categories' ), + 'edit_item' => __( 'Edit Pattern Category' ), + 'item_link' => __( 'Pattern Category Link' ), + 'item_link_description' => __( 'A link to a pattern category.' ), + 'items_list' => __( 'Pattern Categories list' ), + 'items_list_navigation' => __( 'Pattern Categories list navigation' ), + 'new_item_name' => __( 'New Pattern Category Name' ), + 'no_terms' => __( 'No pattern categories' ), + 'not_found' => __( 'No pattern categories found.' ), + 'popular_items' => __( 'Popular Pattern Categories' ), + 'search_items' => __( 'Search Pattern Categories' ), + 'separate_items_with_commas' => __( 'Separate pattern categories with commas' ), + 'update_item' => __( 'Update Pattern Category' ), + 'view_item' => __( 'View Pattern Category' ), + ), + 'query_var' => false, + 'rewrite' => false, + 'show_ui' => true, + '_builtin' => true, + 'show_in_nav_menus' => false, + 'show_in_rest' => true, + 'show_admin_column' => true, + ); + register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); +} +add_action( 'init', 'gutenberg_register_taxonomy_patterns' ); diff --git a/lib/load.php b/lib/load.php index 59fb75541ac41e..ed108f764ada19 100644 --- a/lib/load.php +++ b/lib/load.php @@ -97,7 +97,6 @@ function gutenberg_is_experiment_enabled( $name ) { // WordPress 6.4 compat. require __DIR__ . '/compat/wordpress-6.4/blocks.php'; require __DIR__ . '/compat/wordpress-6.4/block-hooks.php'; -require __DIR__ . '/compat/wordpress-6.4/block-patterns.php'; require __DIR__ . '/compat/wordpress-6.4/script-loader.php'; require __DIR__ . '/compat/wordpress-6.4/kses.php'; From 6bf61f9127c299afcdf5a65ab5e45203432861c7 Mon Sep 17 00:00:00 2001 From: Brian Coords <bacoords@gmail.com> Date: Wed, 20 Dec 2023 00:01:52 -0800 Subject: [PATCH 293/325] Fixes heading hierarchy in block filters (#57239) --- docs/reference-guides/filters/block-filters.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index e269ba9ef19917..4c7e3df7cec128 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -116,7 +116,7 @@ wp.hooks.addFilter( ); ``` -#### `blocks.getSaveContent.extraProps` +### `blocks.getSaveContent.extraProps` A filter that applies to all blocks returning a WP Element in the `save` function. This filter is used to add extra props to the root element of the `save` function. For example: to add a className, an id, or any valid prop for this element. @@ -229,7 +229,7 @@ const withMyPluginControls = createHigherOrderComponent( ( BlockEdit ) => { }, 'withMyPluginControls' ); ``` -#### `editor.BlockListBlock` +### `editor.BlockListBlock` Used to modify the block's wrapper component containing the block's `edit` component and all toolbars. It receives the original `BlockListBlock` component and returns a new wrapped component. From 54e8aee4cdc090b4754a7895e94fb94082b29051 Mon Sep 17 00:00:00 2001 From: Mitchell Austin <mr.fye@oneandthesame.net> Date: Wed, 20 Dec 2023 00:42:04 -0800 Subject: [PATCH 294/325] Remove cruft in Button block editor styles (#30950) * Remove cruft in Button block editor styles * Remove unused z-index rule --- packages/base-styles/_z-index.scss | 1 - packages/block-library/src/button/editor.scss | 43 ------------------- 2 files changed, 44 deletions(-) diff --git a/packages/base-styles/_z-index.scss b/packages/base-styles/_z-index.scss index 7d1fd0796f109b..36d01c843c1c73 100644 --- a/packages/base-styles/_z-index.scss +++ b/packages/base-styles/_z-index.scss @@ -32,7 +32,6 @@ $z-layers: ( ".interface-interface-skeleton__header": 30, ".interface-interface-skeleton__content": 20, ".edit-widgets-header": 30, - ".block-library-button__inline-link .block-editor-url-input__suggestions": 6, // URL suggestions for button block above sibling inserter ".wp-block-cover__inner-container": 1, // InnerBlocks area inside cover image block. ".wp-block-cover.is-placeholder .components-placeholder.is-large": 1, // Cover block resizer component inside a large placeholder. ".wp-block-cover.has-background-dim::before": 1, // Overlay area inside block cover need to be higher than the video background. diff --git a/packages/block-library/src/button/editor.scss b/packages/block-library/src/button/editor.scss index 3c36586bbfabdf..c24021d17e38ec 100644 --- a/packages/block-library/src/button/editor.scss +++ b/packages/block-library/src/button/editor.scss @@ -28,49 +28,6 @@ } } -.wp-block-button__inline-link { - color: $gray-700; - height: 0; - overflow: hidden; - max-width: 290px; - - &-input__suggestions { - max-width: 290px; - } - - @include break-medium() { - max-width: 260px; - - &-input__suggestions { - max-width: 260px; - } - - } - @include break-large() { - max-width: 290px; - - &-input__suggestions { - max-width: 290px; - } - - } - - .is-selected & { - height: auto; - overflow: visible; - } -} - -.wp-button-label__width { - .components-button-group { - display: block; - } - - .components-base-control__field { - margin-bottom: 12px; - } -} - // Display "table" is used because the button container should only wrap the content and not takes the full width. div[data-type="core/button"] { display: table; From a312c1b49c5c286c527f1efe68b8451054736e8f Mon Sep 17 00:00:00 2001 From: Aki Hamano <54422211+t-hamano@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:45:30 +0900 Subject: [PATCH 295/325] Pattern Category: change show_tagcloud to false (#57212) --- lib/compat/wordpress-6.5/block-patterns.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/compat/wordpress-6.5/block-patterns.php b/lib/compat/wordpress-6.5/block-patterns.php index 87756ea45c7d00..f43acda2a1035c 100644 --- a/lib/compat/wordpress-6.5/block-patterns.php +++ b/lib/compat/wordpress-6.5/block-patterns.php @@ -72,6 +72,7 @@ function gutenberg_register_taxonomy_patterns() { 'show_in_nav_menus' => false, 'show_in_rest' => true, 'show_admin_column' => true, + 'show_tagcloud' => false, ); register_taxonomy( 'wp_pattern_category', array( 'wp_block' ), $args ); } From cf2950eef1b74c41af59283b3e7390711256a67c Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Wed, 20 Dec 2023 11:39:56 +0100 Subject: [PATCH 296/325] DropdownMenuV2: do not collapse suffix width (#57238) * DropdownMenuV2: do not collapse suffix width * CHANGELOG --- packages/components/CHANGELOG.md | 1 + packages/components/src/dropdown-menu-v2-ariakit/styles.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index ecf4d3628c0839..19feebd911b456 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -13,6 +13,7 @@ ### Enhancements +- `DropdownMenuV2`: do not collapse suffix width ([#57238](https://github.com/WordPress/gutenberg/pull/57238)). - `DateTimePicker`: Adjustment of the dot position on DayButton and expansion of the button area. ([#55502](https://github.com/WordPress/gutenberg/pull/55502)). - `Modal`: Improve application of body class names ([#55430](https://github.com/WordPress/gutenberg/pull/55430)). diff --git a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts index eaa249ae86b78c..ec6b2cb74d2172 100644 --- a/packages/components/src/dropdown-menu-v2-ariakit/styles.ts +++ b/packages/components/src/dropdown-menu-v2-ariakit/styles.ts @@ -269,8 +269,9 @@ export const DropdownMenuItemChildrenWrapper = styled.div` `; export const ItemSuffixWrapper = styled.span` - flex: 0; - width: max-content; + flex: 0 1 fit-content; + min-width: 0; + width: fit-content; display: flex; align-items: center; From d4886839847dbe1e7b25639a5dbbb6c8f1bc78ea Mon Sep 17 00:00:00 2001 From: Akira Tachibana <atachibana@unofficialtokyo.com> Date: Wed, 20 Dec 2023 20:01:27 +0900 Subject: [PATCH 297/325] Fix raw html tag is not shown. (#56906) --- docs/getting-started/fundamentals/block-wrapper.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/getting-started/fundamentals/block-wrapper.md b/docs/getting-started/fundamentals/block-wrapper.md index 6ecebbe8303864..1f2404eca9b031 100644 --- a/docs/getting-started/fundamentals/block-wrapper.md +++ b/docs/getting-started/fundamentals/block-wrapper.md @@ -5,7 +5,7 @@ Each block's markup is wrapped by a container HTML tag that needs to have the pr Ensuring proper attributes to the block wrapper is especially important when using custom styling or features like `supports`. <div class="callout callout-info"> -The use of <code>supports</code> generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data +The use of <code>supports</code> generates a set of properties that need to be manually added to the wrapping element of the block so they're properly stored as part of the block data. </div> A block can have three sets of markup defined, each one of them with a specific target and purpose: @@ -16,7 +16,7 @@ A block can have three sets of markup defined, each one of them with a specific - The one used to **dynamically render the markup of the block** returned to the front end on request, defined through the `render_callback` on [`register_block_type`](https://developer.wordpress.org/reference/functions/register_block_type/) or the [`render`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-metadata/#render) PHP file in `block.json` - If defined, this server-side generated markup will be returned to the front end, ignoring the markup stored in DB. -For the [`edit` React component and the `save` function](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/), the block wrapper element should be a native DOM element (like `<div>`) or a React component that forwards any additional props to native DOM elements. Using a <Fragment> or <ServerSideRender> component, for instance, would be invalid. +For the [`edit` React component and the `save` function](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/), the block wrapper element should be a native DOM element (like `<div>`) or a React component that forwards any additional props to native DOM elements. Using a `<Fragment>` or `<ServerSideRender>` component, for instance, would be invalid. ## The Edit component's markup @@ -60,7 +60,7 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b >Hello World - Block Editor</p> ``` -Any additional classes and attributes for the `Edit` component of the block should be passed as an argument of `useBlockProps` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/edit.js)). When you add `support` for any feature, they get added to the object returned by the `useBlockProps` hook. +Any additional classes and attributes for the `Edit` component of the block should be passed as an argument of `useBlockProps` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/edit.js)). When you add `supports` for any feature, they get added to the object returned by the `useBlockProps` hook. ## The Save component's markup @@ -89,7 +89,7 @@ _(see the [code above](https://github.com/WordPress/block-development-examples/b Any additional classes and attributes for the `save` function of the block should be passed as an argument of `useBlockProps.save()` (see [example](https://github.com/WordPress/block-development-examples/blob/trunk/plugins/stylesheets-79a4c3/src/save.js)). -When you add `support` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. +When you add `supports` for any feature, the proper classes get added to the object returned by the `useBlockProps.save()` hook. ```html <p class=" From 7fbe60458d2655ee444711a23c06577e50d00a2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:13:40 +0100 Subject: [PATCH 298/325] DataViews: improve preview (#57116) --- .../src/components/page-pages/index.js | 27 +++++++++++++++++-- .../src/components/page-templates/index.js | 27 ++++++++++++++++++- .../src/components/post-preview/index.js | 4 ++- 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 17736abdfc55c0..9c3f55fc78f3bc 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -13,6 +13,7 @@ import { dateI18n, getDate, getSettings } from '@wordpress/date'; import { privateApis as routerPrivateApis } from '@wordpress/router'; import { useSelect, useDispatch } from '@wordpress/data'; import { DataViews } from '@wordpress/dataviews'; +import { ENTER, SPACE } from '@wordpress/keycodes'; /** * Internal dependencies @@ -40,7 +41,7 @@ import { import PostPreview from '../post-preview'; import Media from '../media'; import { unlock } from '../../lock-unlock'; -const { useLocation } = unlock( routerPrivateApis ); +const { useLocation, useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; const defaultConfigPerViewType = { @@ -130,6 +131,7 @@ export default function PagePages() { const postType = 'page'; const [ view, setView ] = useView( postType ); const [ pageId, setPageId ] = useState( null ); + const history = useHistory(); const onSelectionChange = ( items ) => setPageId( items?.length === 1 ? items[ 0 ].id : null ); @@ -346,7 +348,28 @@ export default function PagePages() { </Page> { view.type === LAYOUT_LIST && ( <Page> - <div className="edit-site-page-pages-preview"> + <div + className="edit-site-page-pages-preview" + tabIndex={ 0 } + role="button" + onKeyDown={ ( event ) => { + const { keyCode } = event; + if ( keyCode === ENTER || keyCode === SPACE ) { + history.push( { + postId: pageId, + postType, + canvas: 'edit', + } ); + } + } } + onClick={ () => + history.push( { + postId: pageId, + postType, + canvas: 'edit', + } ) + } + > { pageId !== null ? ( <PostPreview postId={ pageId } diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 30e7797ec0b2d2..85027c0d47f3a8 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -18,12 +18,14 @@ import { __ } from '@wordpress/i18n'; import { useState, useMemo, useCallback } from '@wordpress/element'; import { useEntityRecords } from '@wordpress/core-data'; import { decodeEntities } from '@wordpress/html-entities'; +import { ENTER, SPACE } from '@wordpress/keycodes'; import { parse } from '@wordpress/blocks'; import { BlockPreview, privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { DataViews } from '@wordpress/dataviews'; +import { privateApis as routerPrivateApis } from '@wordpress/router'; /** * Internal dependencies @@ -54,6 +56,7 @@ import PostPreview from '../post-preview'; const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( blockEditorPrivateApis ); +const { useHistory } = unlock( routerPrivateApis ); const EMPTY_ARRAY = []; @@ -165,6 +168,7 @@ export default function DataviewsTemplates() { useEntityRecords( 'postType', TEMPLATE_POST_TYPE, { per_page: -1, } ); + const history = useHistory(); const onSelectionChange = ( items ) => setTemplateId( items?.length === 1 ? items[ 0 ].id : null ); @@ -390,7 +394,28 @@ export default function DataviewsTemplates() { </Page> { view.type === LAYOUT_LIST && ( <Page> - <div className="edit-site-template-pages-preview"> + <div + className="edit-site-template-pages-preview" + tabIndex={ 0 } + role="button" + onKeyDown={ ( event ) => { + const { keyCode } = event; + if ( keyCode === ENTER || keyCode === SPACE ) { + history.push( { + postId: templateId, + postType: TEMPLATE_POST_TYPE, + canvas: 'edit', + } ); + } + } } + onClick={ () => + history.push( { + postId: templateId, + postType: TEMPLATE_POST_TYPE, + canvas: 'edit', + } ) + } + > { templateId !== null ? ( <PostPreview postId={ templateId } diff --git a/packages/edit-site/src/components/post-preview/index.js b/packages/edit-site/src/components/post-preview/index.js index de66ef1aad7455..8f325c26275948 100644 --- a/packages/edit-site/src/components/post-preview/index.js +++ b/packages/edit-site/src/components/post-preview/index.js @@ -3,12 +3,14 @@ */ import Editor from '../editor'; import { useInitEditedEntity } from '../sync-state-with-url/use-init-edited-entity-from-url'; +import { useIsSiteEditorLoading } from '../layout/hooks'; export default function PostPreview( { postType, postId } ) { useInitEditedEntity( { postId, postType, } ); + const isEditorLoading = useIsSiteEditorLoading(); - return <Editor />; + return <Editor isLoading={ isEditorLoading } />; } From 45c9d1ab830ab083b7f07685084504c677b62bf2 Mon Sep 17 00:00:00 2001 From: Riad Benguella <benguella@gmail.com> Date: Wed, 20 Dec 2023 12:21:56 +0100 Subject: [PATCH 299/325] Editor: Unify the list view shortcut registration and definition (#57200) --- .../components/header/header-toolbar/index.js | 2 +- .../test/__snapshots__/index.js.snap | 929 ------------------ .../test/index.js | 15 - .../components/keyboard-shortcuts/index.js | 20 - .../secondary-sidebar/list-view-sidebar.js | 5 +- .../edit-site/src/components/editor/index.js | 10 +- .../header-edit-mode/document-tools/index.js | 2 +- .../keyboard-shortcuts/edit-mode.js | 26 - .../components/keyboard-shortcuts/register.js | 43 - .../secondary-sidebar/list-view-sidebar.js | 5 +- .../global-keyboard-shortcuts/index.js | 14 +- .../register-shortcuts.js | 10 + 12 files changed, 35 insertions(+), 1046 deletions(-) delete mode 100644 packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap diff --git a/packages/edit-post/src/components/header/header-toolbar/index.js b/packages/edit-post/src/components/header/header-toolbar/index.js index e1d059578809e0..e8786900e4f257 100644 --- a/packages/edit-post/src/components/header/header-toolbar/index.js +++ b/packages/edit-post/src/components/header/header-toolbar/index.js @@ -65,7 +65,7 @@ function HeaderToolbar( { hasFixedToolbar } ) { showIconLabels: isFeatureActive( 'showIconLabels' ), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( - 'core/edit-post/toggle-list-view' + 'core/editor/toggle-list-view' ), listViewToggleRef: getListViewToggleRef(), }; diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap deleted file mode 100644 index 79990664a2427b..00000000000000 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/__snapshots__/index.js.snap +++ /dev/null @@ -1,929 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`KeyboardShortcutHelpModal should match snapshot when the modal is active 1`] = ` -<div - aria-labelledby="components-modal-header-0" - class="components-modal__frame edit-post-keyboard-shortcut-help-modal" - role="dialog" - tabindex="-1" -> - <div - class="components-modal__content" - role="document" - > - <div - class="components-modal__header" - > - <div - class="components-modal__header-heading-container" - > - <h1 - class="components-modal__header-heading" - id="components-modal-header-0" - > - Keyboard shortcuts - </h1> - </div> - <button - aria-label="Close" - class="components-button has-icon" - type="button" - > - <svg - aria-hidden="true" - focusable="false" - height="24" - viewBox="0 0 24 24" - width="24" - xmlns="http://www.w3.org/2000/svg" - > - <path - d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z" - /> - </svg> - </button> - </div> - <div> - <section - class="edit-post-keyboard-shortcut-help-modal__section edit-post-keyboard-shortcut-help-modal__main-shortcuts" - > - <ul - class="edit-post-keyboard-shortcut-help-modal__shortcut-list" - role="list" - > - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - /> - </ul> - </section> - <section - class="edit-post-keyboard-shortcut-help-modal__section" - > - <h2 - class="edit-post-keyboard-shortcut-help-modal__section-title" - > - Global shortcuts - </h2> - <ul - class="edit-post-keyboard-shortcut-help-modal__shortcut-list" - role="list" - > - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Navigate to the nearest toolbar. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Alt + F10" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - F10 - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Save your changes. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + S" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - S - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Undo your last changes. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Z" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Z - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Redo your last undo. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Shift + Z" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Z - </kbd> - </kbd> - <kbd - aria-label="Control + Y" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Y - </kbd> - </kbd> - </div> - </li> - </ul> - </section> - <section - class="edit-post-keyboard-shortcut-help-modal__section" - > - <h2 - class="edit-post-keyboard-shortcut-help-modal__section-title" - > - Selection shortcuts - </h2> - <ul - class="edit-post-keyboard-shortcut-help-modal__shortcut-list" - role="list" - > - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Select all text when typing. Press again to select all blocks. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + A" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - A - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Clear selection. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="escape" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - escape - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Select text across multiple blocks. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Arrow" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Arrow - </kbd> - </kbd> - </div> - </li> - </ul> - </section> - <section - class="edit-post-keyboard-shortcut-help-modal__section" - > - <h2 - class="edit-post-keyboard-shortcut-help-modal__section-title" - > - Block shortcuts - </h2> - <ul - class="edit-post-keyboard-shortcut-help-modal__shortcut-list" - role="list" - > - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Duplicate the selected block(s). - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Shift + D" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - D - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Remove the selected block(s). - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Alt + Z" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Z - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Insert a new block before the selected block(s). - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Alt + T" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - T - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Insert a new block after the selected block(s). - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Alt + Y" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Y - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Delete selection. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="del" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - del - </kbd> - </kbd> - <kbd - aria-label="backspace" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - backspace - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Move the selected block(s) up. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Shift + Alt + T" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - T - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Move the selected block(s) down. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Shift + Alt + Y" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Y - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Change the block type after adding a new paragraph. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Forward-slash" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - / - </kbd> - </kbd> - </div> - </li> - </ul> - </section> - <section - class="edit-post-keyboard-shortcut-help-modal__section" - > - <h2 - class="edit-post-keyboard-shortcut-help-modal__section-title" - > - Text formatting - </h2> - <ul - class="edit-post-keyboard-shortcut-help-modal__shortcut-list" - role="list" - > - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Make the selected text bold. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + B" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - B - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Make the selected text italic. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + I" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - I - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Convert the selected text into a link. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + K" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - K - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Remove a link. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + Shift + K" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - K - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Insert a link to a post or page. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="[[" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - [[ - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Underline the selected text. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Control + U" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Ctrl - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - U - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Strikethrough the selected text. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Alt + D" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - D - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Make the selected text inline code. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Alt + X" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - X - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Convert the current heading to a paragraph. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Alt + 0" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - 0 - </kbd> - </kbd> - </div> - </li> - <li - class="edit-post-keyboard-shortcut-help-modal__shortcut" - > - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-description" - > - Convert the current paragraph or heading to a heading of level 1 to 6. - </div> - <div - class="edit-post-keyboard-shortcut-help-modal__shortcut-term" - > - <kbd - aria-label="Shift + Alt + 1-6" - class="edit-post-keyboard-shortcut-help-modal__shortcut-key-combination" - > - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Shift - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - Alt - </kbd> - + - <kbd - class="edit-post-keyboard-shortcut-help-modal__shortcut-key" - > - 1-6 - </kbd> - </kbd> - </div> - </li> - </ul> - </section> - </div> - </div> -</div> -`; diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js index 0380e648a17331..9ba2ebdf82597f 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/test/index.js @@ -16,21 +16,6 @@ import { KeyboardShortcutHelpModal } from '../index'; const noop = () => {}; describe( 'KeyboardShortcutHelpModal', () => { - it( 'should match snapshot when the modal is active', () => { - render( - <> - <EditorKeyboardShortcutsRegister /> - <KeyboardShortcutHelpModal isModalActive toggleModal={ noop } /> - </> - ); - - expect( - screen.getByRole( 'dialog', { - name: 'Keyboard shortcuts', - } ) - ).toMatchSnapshot(); - } ); - it( 'should not render the modal when inactive', () => { render( <> diff --git a/packages/edit-post/src/components/keyboard-shortcuts/index.js b/packages/edit-post/src/components/keyboard-shortcuts/index.js index c808d65ae5b165..0bdcd5613599a3 100644 --- a/packages/edit-post/src/components/keyboard-shortcuts/index.js +++ b/packages/edit-post/src/components/keyboard-shortcuts/index.js @@ -24,7 +24,6 @@ function KeyboardShortcuts() { select( editorStore ).getEditorSettings(); return ! richEditingEnabled || ! codeEditingEnabled; }, [] ); - const { isListViewOpened } = useSelect( editorStore ); const { switchEditorMode, openGeneralSidebar, @@ -33,7 +32,6 @@ function KeyboardShortcuts() { toggleDistractionFree, } = useDispatch( editPostStore ); const { registerShortcut } = useDispatch( keyboardShortcutsStore ); - const { setIsListViewOpened } = useDispatch( editorStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); const { getBlockName, @@ -101,16 +99,6 @@ function KeyboardShortcuts() { }, } ); - registerShortcut( { - name: 'core/edit-post/toggle-list-view', - category: 'global', - description: __( 'Open the block list view.' ), - keyCombination: { - modifier: 'access', - character: 'o', - }, - } ); - registerShortcut( { name: 'core/edit-post/toggle-sidebar', category: 'global', @@ -225,14 +213,6 @@ function KeyboardShortcuts() { } } ); - // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. - useShortcut( 'core/edit-post/toggle-list-view', ( event ) => { - if ( ! isListViewOpened() ) { - event.preventDefault(); - setIsListViewOpened( true ); - } - } ); - useShortcut( 'core/edit-post/transform-heading-to-paragraph', ( event ) => handleTextLevelShortcut( event, 0 ) ); diff --git a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js index 02690d9115d7ab..c1b4512454b150 100644 --- a/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-post/src/components/secondary-sidebar/list-view-sidebar.js @@ -106,10 +106,7 @@ export default function ListViewSidebar() { // This only fires when the sidebar is open because of the conditional rendering. // It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed. - useShortcut( - 'core/edit-post/toggle-list-view', - handleToggleListViewShortcut - ); + useShortcut( 'core/editor/toggle-list-view', handleToggleListViewShortcut ); /** * Render tab content for a given tab name. diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 9a1931b2ede398..6718fc705b50b9 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -23,6 +23,8 @@ import { store as interfaceStore, } from '@wordpress/interface'; import { + EditorKeyboardShortcutsRegister, + EditorKeyboardShortcuts, EditorNotices, EditorSnackbars, privateApis as editorPrivateApis, @@ -245,7 +247,13 @@ export default function Editor( { isLoading } ) { { editorMode === 'text' && isEditMode && ( <CodeEditor /> ) } - { isEditMode && <KeyboardShortcutsEditMode /> } + { isEditMode && ( + <> + <KeyboardShortcutsEditMode /> + <EditorKeyboardShortcutsRegister /> + <EditorKeyboardShortcuts /> + </> + ) } </> } secondarySidebar={ diff --git a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js index 837c825b2060a9..ec37cadfbc0de0 100644 --- a/packages/edit-site/src/components/header-edit-mode/document-tools/index.js +++ b/packages/edit-site/src/components/header-edit-mode/document-tools/index.js @@ -53,7 +53,7 @@ export default function DocumentTools( { isInserterOpen: isInserterOpened(), isListViewOpen: isListViewOpened(), listViewShortcut: getShortcutRepresentation( - 'core/edit-site/toggle-list-view' + 'core/editor/toggle-list-view' ), isVisualMode: getEditorMode() === 'visual', listViewToggleRef: getListViewToggleRef(), diff --git a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js index 8bc6497967902e..07e017349a0c61 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/edit-mode.js @@ -3,10 +3,8 @@ */ import { useShortcut } from '@wordpress/keyboard-shortcuts'; import { useDispatch, useSelect } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { store as interfaceStore } from '@wordpress/interface'; -import { store as editorStore } from '@wordpress/editor'; import { createBlock } from '@wordpress/blocks'; /** @@ -18,10 +16,6 @@ import { STORE_NAME } from '../../store/constants'; function KeyboardShortcutsEditMode() { const { getEditorMode } = useSelect( editSiteStore ); - const isListViewOpen = useSelect( - ( select ) => select( editorStore ).isListViewOpened(), - [] - ); const isBlockInspectorOpen = useSelect( ( select ) => select( interfaceStore ).getActiveComplementaryArea( @@ -29,12 +23,10 @@ function KeyboardShortcutsEditMode() { ) === SIDEBAR_BLOCK, [] ); - const { redo, undo } = useDispatch( coreStore ); const { switchEditorMode, toggleDistractionFree } = useDispatch( editSiteStore ); const { enableComplementaryArea, disableComplementaryArea } = useDispatch( interfaceStore ); - const { setIsListViewOpened } = useDispatch( editorStore ); const { replaceBlocks } = useDispatch( blockEditorStore ); const { getBlockName, getSelectedBlockClientId, getBlockAttributes } = useSelect( blockEditorStore ); @@ -67,24 +59,6 @@ function KeyboardShortcutsEditMode() { ); }; - useShortcut( 'core/edit-site/undo', ( event ) => { - undo(); - event.preventDefault(); - } ); - - useShortcut( 'core/edit-site/redo', ( event ) => { - redo(); - event.preventDefault(); - } ); - - // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. - useShortcut( 'core/edit-site/toggle-list-view', () => { - if ( isListViewOpen ) { - return; - } - setIsListViewOpened( true ); - } ); - useShortcut( 'core/edit-site/toggle-block-settings-sidebar', ( event ) => { // This shortcut has no known clashes, but use preventDefault to prevent any // obscure shortcuts from triggering. diff --git a/packages/edit-site/src/components/keyboard-shortcuts/register.js b/packages/edit-site/src/components/keyboard-shortcuts/register.js index 8dfd1e3e2a45bf..ef32cd920b6711 100644 --- a/packages/edit-site/src/components/keyboard-shortcuts/register.js +++ b/packages/edit-site/src/components/keyboard-shortcuts/register.js @@ -3,7 +3,6 @@ */ import { useEffect } from '@wordpress/element'; import { store as keyboardShortcutsStore } from '@wordpress/keyboard-shortcuts'; -import { isAppleOS } from '@wordpress/keycodes'; import { useDispatch } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; @@ -21,48 +20,6 @@ function KeyboardShortcutsRegister() { }, } ); - registerShortcut( { - name: 'core/edit-site/undo', - category: 'global', - description: __( 'Undo your last changes.' ), - keyCombination: { - modifier: 'primary', - character: 'z', - }, - } ); - - registerShortcut( { - name: 'core/edit-site/redo', - category: 'global', - description: __( 'Redo your last undo.' ), - keyCombination: { - modifier: 'primaryShift', - character: 'z', - }, - // Disable on Apple OS because it conflicts with the browser's - // history shortcut. It's a fine alias for both Windows and Linux. - // Since there's no conflict for Ctrl+Shift+Z on both Windows and - // Linux, we keep it as the default for consistency. - aliases: isAppleOS() - ? [] - : [ - { - modifier: 'primary', - character: 'y', - }, - ], - } ); - - registerShortcut( { - name: 'core/edit-site/toggle-list-view', - category: 'global', - description: __( 'Open the block list view.' ), - keyCombination: { - modifier: 'access', - character: 'o', - }, - } ); - registerShortcut( { name: 'core/edit-site/toggle-block-settings-sidebar', category: 'global', diff --git a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js index 3b837ba6a91713..d18abd0083f07b 100644 --- a/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js +++ b/packages/edit-site/src/components/secondary-sidebar/list-view-sidebar.js @@ -88,10 +88,7 @@ export default function ListViewSidebar() { // This only fires when the sidebar is open because of the conditional rendering. // It is the same shortcut to open but that is defined as a global shortcut and only fires when the sidebar is closed. - useShortcut( - 'core/edit-site/toggle-list-view', - handleToggleListViewShortcut - ); + useShortcut( 'core/editor/toggle-list-view', handleToggleListViewShortcut ); return ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions diff --git a/packages/editor/src/components/global-keyboard-shortcuts/index.js b/packages/editor/src/components/global-keyboard-shortcuts/index.js index 4b45fe449123f4..a62f542ff9974d 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/index.js +++ b/packages/editor/src/components/global-keyboard-shortcuts/index.js @@ -10,8 +10,10 @@ import { useDispatch, useSelect } from '@wordpress/data'; import { store as editorStore } from '../../store'; export default function EditorKeyboardShortcuts() { - const { redo, undo, savePost } = useDispatch( editorStore ); - const { isEditedPostDirty, isPostSavingLocked } = useSelect( editorStore ); + const { redo, undo, savePost, setIsListViewOpened } = + useDispatch( editorStore ); + const { isEditedPostDirty, isPostSavingLocked, isListViewOpened } = + useSelect( editorStore ); useShortcut( 'core/editor/undo', ( event ) => { undo(); @@ -45,5 +47,13 @@ export default function EditorKeyboardShortcuts() { savePost(); } ); + // Only opens the list view. Other functionality for this shortcut happens in the rendered sidebar. + useShortcut( 'core/editor/toggle-list-view', ( event ) => { + if ( ! isListViewOpened() ) { + event.preventDefault(); + setIsListViewOpened( true ); + } + } ); + return null; } diff --git a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js index 8e8f4c42ca6dd6..b1ed83bd33e4e0 100644 --- a/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js +++ b/packages/editor/src/components/global-keyboard-shortcuts/register-shortcuts.js @@ -53,6 +53,16 @@ function EditorKeyboardShortcutsRegister() { }, ], } ); + + registerShortcut( { + name: 'core/editor/toggle-list-view', + category: 'global', + description: __( 'Open the block list view.' ), + keyCombination: { + modifier: 'access', + character: 'o', + }, + } ); }, [ registerShortcut ] ); return <BlockEditorKeyboardShortcuts.Register />; From 1f6136d92914eef8e160231867714404b9557c2a Mon Sep 17 00:00:00 2001 From: Joen A <1204802+jasmussen@users.noreply.github.com> Date: Wed, 20 Dec 2023 12:35:08 +0100 Subject: [PATCH 300/325] More settings tip: add explicit font size. (#55835) --- .../src/components/inspector-controls-tabs/style.scss | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/block-editor/src/components/inspector-controls-tabs/style.scss b/packages/block-editor/src/components/inspector-controls-tabs/style.scss index 6db9395af62ef6..f25e89903a6efd 100644 --- a/packages/block-editor/src/components/inspector-controls-tabs/style.scss +++ b/packages/block-editor/src/components/inspector-controls-tabs/style.scss @@ -15,13 +15,14 @@ } .block-editor-inspector-controls-tabs__hint { - align-items: top; + align-items: flex-start; background: $gray-100; border-radius: $radius-block-ui; color: $gray-900; display: flex; flex-direction: row; margin: $grid-unit-20; + font-size: $default-font-size; } .block-editor-inspector-controls-tabs__hint-content { From 07e99ceafda38272ac66b74a5890276b620322a5 Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 20 Dec 2023 13:13:16 +0000 Subject: [PATCH 301/325] Bump plugin version to 17.3.0 --- gutenberg.php | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/gutenberg.php b/gutenberg.php index 20c51fdb6ead2a..69d5a6732a7e37 100644 --- a/gutenberg.php +++ b/gutenberg.php @@ -5,7 +5,7 @@ * Description: Printing since 1440. This is the development plugin for the block editor, site editor, and other future WordPress core functionality. * Requires at least: 6.3 * Requires PHP: 7.0 - * Version: 17.3.0-rc.1 + * Version: 17.3.0 * Author: Gutenberg Team * Text Domain: gutenberg * diff --git a/package-lock.json b/package-lock.json index 7425d75403ad10..478570140e51aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "17.3.0-rc.1", + "version": "17.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "17.3.0-rc.1", + "version": "17.3.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { diff --git a/package.json b/package.json index cab3288450cd75..e1dd7fba270773 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "17.3.0-rc.1", + "version": "17.3.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", From 7ecbbe82f6d192cd30257966af74e59582193dd2 Mon Sep 17 00:00:00 2001 From: Koen <77921155+koen12344@users.noreply.github.com> Date: Wed, 20 Dec 2023 14:20:49 +0100 Subject: [PATCH 302/325] Button: Restore descriptions for isPrimary, isSecondary, isTertiary, isLink but mark them as deprecated (#37690) --- packages/components/src/button/README.md | 32 ++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/components/src/button/README.md b/packages/components/src/button/README.md index cf2748d3846f73..3f577991dc44be 100644 --- a/packages/components/src/button/README.md +++ b/packages/components/src/button/README.md @@ -176,6 +176,14 @@ Renders a red text-based button style to indicate destructive behavior. - Required: No +#### `isLink`: `boolean` + +Deprecated: Renders a button with an anchor style. +Use `variant` prop with `link` value instead. + +- Required: No +- Default: `false` + #### `isPressed`: `boolean` Renders a pressed button style. @@ -184,6 +192,22 @@ If the native `aria-pressed` attribute is also set, it will take precedence. - Required: No +#### `isPrimary`: `boolean` + +Deprecated: Renders a primary button style. +Use `variant` prop with `primary` value instead. + +- Required: No +- Default: `false` + +#### `isSecondary`: `boolean` + +Deprecated: Renders a default button style. +Use `variant` prop with `secondary` value instead. + +- Required: No +- Default: `false` + #### `isSmall`: `boolean` Decreases the size of the button. @@ -192,6 +216,14 @@ Deprecated in favor of the `size` prop. If both props are defined, the `size` pr - Required: No +#### `isTertiary`: `boolean` + +Deprecated: Renders a text-based button style. +Use `variant` prop with `tertiary` value instead. + +- Required: No +- Default: `false` + #### `label`: `string` Sets the `aria-label` of the component, if none is provided. Sets the Tooltip content if `showTooltip` is provided. From 353b73264e9764b3291c753cc5eeb71f21a7e4dd Mon Sep 17 00:00:00 2001 From: Gutenberg Repository Automation <gutenberg@wordpress.org> Date: Wed, 20 Dec 2023 13:27:07 +0000 Subject: [PATCH 303/325] Update Changelog for 17.3.0 --- changelog.txt | 337 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 337 insertions(+) diff --git a/changelog.txt b/changelog.txt index c8aa6254923aab..0b70b68c9377ee 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,342 @@ == Changelog == += 17.3.0 = + + + +## Changelog + +### Bug Fixes + +- (edit-site)(use-init-edited-entity-from-url) Safely access `toString()` on `siteData`'s `page_on_front`. ([57035](https://github.com/WordPress/gutenberg/pull/57035)) + +#### Components +- Fix form token field suggestion list reopening after blurring the input. ([57002](https://github.com/WordPress/gutenberg/pull/57002)) + + + + +## Contributors + +The following contributors merged PRs in this release: + +@fullofcaffeine @talldan + += 17.3.0-rc.1 = + + +## Changelog + +### Enhancements + +- Components: Replace `TabPanel` with `Tabs` in the editor's `ColorPanel`. ([56878](https://github.com/WordPress/gutenberg/pull/56878)) +- Editor: Move the edit template blocks notification to editor package. ([56901](https://github.com/WordPress/gutenberg/pull/56901)) +- Editor: Unify the preview dropdown between post and site editors. ([56921](https://github.com/WordPress/gutenberg/pull/56921)) +- Editor: Use the same PostTemplatePanel between post and site editors. ([56817](https://github.com/WordPress/gutenberg/pull/56817)) +- Tabs: Replace `id` with new `tabId` prop. ([56883](https://github.com/WordPress/gutenberg/pull/56883)) +- Update main toolbar buttons to all be compact. ([56635](https://github.com/WordPress/gutenberg/pull/56635), [56729](https://github.com/WordPress/gutenberg/pull/56729)) +- Update preferences organization. ([56481](https://github.com/WordPress/gutenberg/pull/56481)) + +#### Components +- FocalPointPicker with __next40pxDefaultSize. ([56021](https://github.com/WordPress/gutenberg/pull/56021)) +- Font Library: Improve usability of font variant selection. ([56158](https://github.com/WordPress/gutenberg/pull/56158)) +- Tabs: Sync browser focus to selected tab in controlled mode. ([56658](https://github.com/WordPress/gutenberg/pull/56658)) +- Use consistent styling for duotone panels. ([56801](https://github.com/WordPress/gutenberg/pull/56801)) +- `BorderControl`: Fix button styles. ([56730](https://github.com/WordPress/gutenberg/pull/56730)) +- `DimensionControl`: Add __next40pxDefaultSize prop. ([56805](https://github.com/WordPress/gutenberg/pull/56805)) +- `FontSizePicker`: Add opt-in prop for 40px default size. ([56804](https://github.com/WordPress/gutenberg/pull/56804)) +- `QueryControls`: Add opt-in prop for 40px default size. ([56576](https://github.com/WordPress/gutenberg/pull/56576)) + +#### Block Library +- Control dimensions (margin and padding) of the list-item block. ([55874](https://github.com/WordPress/gutenberg/pull/55874)) +- Consistent default typography controls across blocks. ([55208](https://github.com/WordPress/gutenberg/pull/55208)) +- Social Icons: Add Gravatar service. ([56544](https://github.com/WordPress/gutenberg/pull/56544)) +- Tweak table block placeholder with __next40pxDefaultSize props. ([56935](https://github.com/WordPress/gutenberg/pull/56935)) + +#### Site Editor +- Merge the post only mode and the post editor. ([56671](https://github.com/WordPress/gutenberg/pull/56671)) +- Site Editor Sidebar: Add "Areas" details panel to all templates and update icon. ([55677](https://github.com/WordPress/gutenberg/pull/55677)) + +#### Block Editor +- Allow dragging between adjacent container blocks based on a threshold. ([56466](https://github.com/WordPress/gutenberg/pull/56466)) +- Components: Replace `TabPanel` with `Tabs` in the editor's `ColorGradientControl`. ([56351](https://github.com/WordPress/gutenberg/pull/56351)) + +#### Data Views +- Update data view layout. ([56786](https://github.com/WordPress/gutenberg/pull/56786)) + +#### Layout +- Match the front end layout classname in the editor. ([56774](https://github.com/WordPress/gutenberg/pull/56774)) + +#### Global Styles +- Global style revisions: Show change summary on selected item. ([56577](https://github.com/WordPress/gutenberg/pull/56577)) + +#### Icons +- Another round of HiDPI icon tweaks. ([56532](https://github.com/WordPress/gutenberg/pull/56532)) + +#### Media +- Update external images panel in post publish sidebar. ([55524](https://github.com/WordPress/gutenberg/pull/55524)) + +#### Post Editor +- Implement `Tabs` in editor settings. ([55360](https://github.com/WordPress/gutenberg/pull/55360)) + + +### Bug Fixes + +- Create-block-interactive-template: Add all files to the generated plugin zip. ([56943](https://github.com/WordPress/gutenberg/pull/56943)) +- Create-block-interactive-template: Prevent crash when Gutenberg plugin is not installed. ([56941](https://github.com/WordPress/gutenberg/pull/56941)) +- Fix end-to-end test: Update how we find the template title to match markup changes. ([56992](https://github.com/WordPress/gutenberg/pull/56992)) +- Fix: Fatal php error if a template was created by an author that was deleted. ([56990](https://github.com/WordPress/gutenberg/pull/56990)) +- Fix: PHP 8.1 deprecated warning strpos(). ([56171](https://github.com/WordPress/gutenberg/pull/56171)) +- Fix: Use span on template list titles. ([56955](https://github.com/WordPress/gutenberg/pull/56955)) +- Font Library: Add font family and font face preview keys to schema. ([56793](https://github.com/WordPress/gutenberg/pull/56793)) +- Remove unnecessary CSS for shrinking central header area. ([56220](https://github.com/WordPress/gutenberg/pull/56220)) +- Revert format types hook refactor. ([56859](https://github.com/WordPress/gutenberg/pull/56859)) +- Show template center UI when no block is selected. ([56217](https://github.com/WordPress/gutenberg/pull/56217)) +- setImmutably: Don't clone all objects. ([56612](https://github.com/WordPress/gutenberg/pull/56612)) + +#### Block Library +- Fix error when using a navigation block that returns an empty fallback result. ([56629](https://github.com/WordPress/gutenberg/pull/56629)) +- Fixture Tests: Correctly generate fixture files for form-related blocks. ([56719](https://github.com/WordPress/gutenberg/pull/56719)) +- Image: Fix resetting behaviour for alt image text. ([56809](https://github.com/WordPress/gutenberg/pull/56809)) +- Social Links Block: Prevent Theme Styles Distorting Size. ([56301](https://github.com/WordPress/gutenberg/pull/56301)) +- Update image block save to only save align none class. ([56449](https://github.com/WordPress/gutenberg/pull/56449)) + +#### Components +- DropdownMenuV2Ariakit: Prevent prefix collapsing if all radios or checkboxes are unselected. ([56720](https://github.com/WordPress/gutenberg/pull/56720)) +- FormToggle: Do not use "/" math operator. ([56672](https://github.com/WordPress/gutenberg/pull/56672)) +- PaletteEdit: Temporary custom gradient not saving. ([56896](https://github.com/WordPress/gutenberg/pull/56896)) +- `ToggleGroupControl`: React correctly to external controlled updates. ([56678](https://github.com/WordPress/gutenberg/pull/56678)) + +#### Block Editor +- Apply __next40pxDefaultSize to TextControl and Button component in renaming UIs. ([56933](https://github.com/WordPress/gutenberg/pull/56933)) +- Pattern inserter: Fix Broken preview layout. ([56814](https://github.com/WordPress/gutenberg/pull/56814)) +- Patterns: Keep synced pattern when added via drag and drop. ([56924](https://github.com/WordPress/gutenberg/pull/56924)) + +#### Design Tools +- Background image support: Fix duplicate output of styling rules. ([56997](https://github.com/WordPress/gutenberg/pull/56997)) +- Fix sticky position in classic themes with appearance tools support. ([56743](https://github.com/WordPress/gutenberg/pull/56743)) + +#### Post Editor +- Editor Canvas: Fix animation when device type changes. ([56970](https://github.com/WordPress/gutenberg/pull/56970)) +- Editor: Fix display of edit template blocks notification. ([56978](https://github.com/WordPress/gutenberg/pull/56978)) + +#### Site Editor +- Fix active edited post. ([56863](https://github.com/WordPress/gutenberg/pull/56863)) +- Show back button when editing navigation and template area in-place with no URL params. ([56741](https://github.com/WordPress/gutenberg/pull/56741)) + +#### Typography +- Fix order of typography sizes and families. ([56659](https://github.com/WordPress/gutenberg/pull/56659)) +- Font Library: Fix font uninstallation. ([56762](https://github.com/WordPress/gutenberg/pull/56762)) + +#### Navigation in Site View +- Navigation editor: Fix content mode. ([56856](https://github.com/WordPress/gutenberg/pull/56856)) + +#### Patterns +- Fix top position and height of Pattern Modal Sidebar. ([56787](https://github.com/WordPress/gutenberg/pull/56787)) + +#### Interactivity API +- Start using modules in the interactive create-block template. ([56694](https://github.com/WordPress/gutenberg/pull/56694)) + +#### Layout +- Fix input not showing when switching to "Fixed" width. ([56660](https://github.com/WordPress/gutenberg/pull/56660)) + +#### Data Views +- Align data view icon usage. ([56602](https://github.com/WordPress/gutenberg/pull/56602)) + +#### Block Styles +- Consolidate and resolve display issues between InserterPreviewPanel and BlockStylesPreviewPanel. ([56011](https://github.com/WordPress/gutenberg/pull/56011)) + +#### Inspector Controls +- Decode some characters if used in taxonomy name so it's displayed correctly in Query Loop filters. ([50376](https://github.com/WordPress/gutenberg/pull/50376)) + + +### Accessibility + +#### Data Views +- Add scroll padding to dataviews container. ([56946](https://github.com/WordPress/gutenberg/pull/56946)) +- Adding `aria-sort` to table view headers. ([56860](https://github.com/WordPress/gutenberg/pull/56860)) +- Fix: Use span instead of heading for the template titles. ([56785](https://github.com/WordPress/gutenberg/pull/56785)) + +#### Post Editor +- Avoid to show unnecessary Tooltip for the Post Schedule button. ([56759](https://github.com/WordPress/gutenberg/pull/56759)) + +#### Block Editor +- Increase right padding of URL field to take the Submit button into account. ([56685](https://github.com/WordPress/gutenberg/pull/56685)) + +#### Site Editor +- Shorter screen reader announcement after changing pages. ([56339](https://github.com/WordPress/gutenberg/pull/56339)) + +#### Components +- Use tooltip for the Timezone only when necessary. ([56214](https://github.com/WordPress/gutenberg/pull/56214)) + + +### Performance + +- Block editor: Make all BlockEdit hooks pure. ([56813](https://github.com/WordPress/gutenberg/pull/56813)) +- Block editor: Remove 4 useSelect in favour of context. ([56915](https://github.com/WordPress/gutenberg/pull/56915)) +- Block editor: hooks: Avoid BlockEdit filter for content locking UI. ([56957](https://github.com/WordPress/gutenberg/pull/56957)) +- Block editor: hooks: Share block settings. ([56852](https://github.com/WordPress/gutenberg/pull/56852)) +- Keycodes: Avoid regex for capital case. ([56822](https://github.com/WordPress/gutenberg/pull/56822)) +- Measure typing without inspector. ([56753](https://github.com/WordPress/gutenberg/pull/56753)) +- Media upload component: Lazy mount. ([56958](https://github.com/WordPress/gutenberg/pull/56958)) +- Paragraph: Store subscription for selected block only. ([56967](https://github.com/WordPress/gutenberg/pull/56967)) +- Perf: Reopen inspector for remaining tests. ([56780](https://github.com/WordPress/gutenberg/pull/56780)) +- useBlockProps: Combine store subscriptions. ([56847](https://github.com/WordPress/gutenberg/pull/56847)) + +#### Block Editor +- Improve opening inserter in post editor. ([57006](https://github.com/WordPress/gutenberg/pull/57006)) +- hooks: Subscribe only to relevant attributes. ([56783](https://github.com/WordPress/gutenberg/pull/56783)) + +#### Site Editor +- Fix typing performance by not rendering sidebar. ([56927](https://github.com/WordPress/gutenberg/pull/56927)) + +#### Components +- ToolsPanel: Fix deregister/register on type. ([56770](https://github.com/WordPress/gutenberg/pull/56770)) + +#### Modules API +- Load the import map polyfill only when there is an import map. ([56699](https://github.com/WordPress/gutenberg/pull/56699)) + +#### Post Editor +- Editor: Avoid double parsing content in 'getSuggestedPostFormat' selelector. ([56679](https://github.com/WordPress/gutenberg/pull/56679)) + + +### Experiments + +#### Data Views +- DataViews: Add story. ([56761](https://github.com/WordPress/gutenberg/pull/56761)) +- DataViews: Add support for `NOT IN` operator in filter. ([56479](https://github.com/WordPress/gutenberg/pull/56479)) +- DataViews: Centralize the view definition and rename `list` to `table`. ([56693](https://github.com/WordPress/gutenberg/pull/56693)) +- DataViews: Do not export strings constants. ([56754](https://github.com/WordPress/gutenberg/pull/56754)) +- DataViews: Export the view components as defaults. ([56677](https://github.com/WordPress/gutenberg/pull/56677)) +- DataViews: Fix dropdown menu actions with modal. ([56760](https://github.com/WordPress/gutenberg/pull/56760)) +- DataViews: Hide pagination if we have only one page. ([56948](https://github.com/WordPress/gutenberg/pull/56948)) +- DataViews: Implement `NOT IN` operator for author filter in templates. ([56777](https://github.com/WordPress/gutenberg/pull/56777)) +- DataViews: Iterate on list view. ([56746](https://github.com/WordPress/gutenberg/pull/56746)) +- DataViews: Make `Actions` styles the same as any other column header. ([56654](https://github.com/WordPress/gutenberg/pull/56654)) +- DataViews: Make `mediaField` not hidable. ([56643](https://github.com/WordPress/gutenberg/pull/56643)) +- DataViews: Rename view components. ([56709](https://github.com/WordPress/gutenberg/pull/56709)) +- DataViews: Render data async conditionally. ([56851](https://github.com/WordPress/gutenberg/pull/56851)) +- DataViews: Set proper role for AddFilter's items. ([56714](https://github.com/WordPress/gutenberg/pull/56714)) +- DataViews: Set proper semantics for dropdown items. ([56676](https://github.com/WordPress/gutenberg/pull/56676)) +- DataViews: Update sorting semantics. ([56717](https://github.com/WordPress/gutenberg/pull/56717)) +- Dataviews: Extract to dedicated bundled package. ([56721](https://github.com/WordPress/gutenberg/pull/56721)) + +#### Block Validation/Deprecation +- Input Field Block: Use `useblockProps` hook in save function. ([56507](https://github.com/WordPress/gutenberg/pull/56507)) + +#### Patterns +- Implement partially synced patterns behind an experimental flag. ([56235](https://github.com/WordPress/gutenberg/pull/56235)) + + +### Documentation + +- Add the nested blocks chapter to the platform documentation. ([56689](https://github.com/WordPress/gutenberg/pull/56689)) +- Components: Update CHANGELOG.md. ([56960](https://github.com/WordPress/gutenberg/pull/56960)) +- Doc: Search Control - add Storybook link. ([56815](https://github.com/WordPress/gutenberg/pull/56815)) +- Doc: Spinner - add Storybook link. ([56818](https://github.com/WordPress/gutenberg/pull/56818)) +- Docs: Add storybook link for spinner component. ([56953](https://github.com/WordPress/gutenberg/pull/56953)) +- Docs: Fix {% end %} tab position to show the text. ([56735](https://github.com/WordPress/gutenberg/pull/56735)) +- Docs: Fundamentals of Block Development - Minor fixes - registration-of-a-block. ([56731](https://github.com/WordPress/gutenberg/pull/56731)) +- Docs: Fundamentals of Block Development - add links. ([56700](https://github.com/WordPress/gutenberg/pull/56700)) +- Docs: Fundamentals of Block Development ---- Small fixes for "Block wrapper". ([56651](https://github.com/WordPress/gutenberg/pull/56651)) +- Link to Dashicons. ([56872](https://github.com/WordPress/gutenberg/pull/56872)) +- Platform Docs: Add trusted by section. ([56749](https://github.com/WordPress/gutenberg/pull/56749)) +- Revert "Doc: Spinner - add Storybook link". ([56913](https://github.com/WordPress/gutenberg/pull/56913)) +- Update Getting Started Guide for Gutenberg 17.2. ([56674](https://github.com/WordPress/gutenberg/pull/56674)) +- Update InnerBlocks defaultblock doc usage. ([56728](https://github.com/WordPress/gutenberg/pull/56728)) +- Update formatting and fix grammar in the Block Editor Handbook readme. ([56798](https://github.com/WordPress/gutenberg/pull/56798)) + + +### Code Quality + +- Block editor: hooks: Avoid getEditWrapperProps. ([56912](https://github.com/WordPress/gutenberg/pull/56912)) +- Block lib: Use RichText.isEmpty where forgotten. ([56726](https://github.com/WordPress/gutenberg/pull/56726)) +- Block library: Reusable caption component util. ([56606](https://github.com/WordPress/gutenberg/pull/56606)) +- Core data revisions: Remove hardcoded supports constant. ([56701](https://github.com/WordPress/gutenberg/pull/56701)) +- Editor: Cleanup default editor mode handling. ([56819](https://github.com/WordPress/gutenberg/pull/56819)) +- Editor: Move the BlockCanvas component within the EditorCanvas component. ([56850](https://github.com/WordPress/gutenberg/pull/56850)) +- Editor: Move the device type state to the editor package. ([56866](https://github.com/WordPress/gutenberg/pull/56866)) +- Editor: Unify device preview styles. ([56904](https://github.com/WordPress/gutenberg/pull/56904)) +- Fix PHP linter failing. ([56905](https://github.com/WordPress/gutenberg/pull/56905)) +- Framework: Bundle the BlockTools component within BlockCanvas. ([56996](https://github.com/WordPress/gutenberg/pull/56996)) +- Move `useDebouncedInput` hook to @wordpress/compose package. ([56744](https://github.com/WordPress/gutenberg/pull/56744)) +- Post Editor: Rely on the editor store for the template mode state. ([56716](https://github.com/WordPress/gutenberg/pull/56716)) +- Refactor <BlockToolbar />. ([56335](https://github.com/WordPress/gutenberg/pull/56335)) +- Remove Block Tools BackCompat. ([56874](https://github.com/WordPress/gutenberg/pull/56874)) +- Site and Post Editor: Unify the DocumentBar component. ([56778](https://github.com/WordPress/gutenberg/pull/56778)) +- getValueFromObjectPath: Remove memize. ([56711](https://github.com/WordPress/gutenberg/pull/56711)) + +#### Block Editor +- Don't render undefined classname in useBlockProps hook. ([56923](https://github.com/WordPress/gutenberg/pull/56923)) +- One hook to rule them all: Preparation for a block supports API. ([56862](https://github.com/WordPress/gutenberg/pull/56862)) +- RichText: Pass value to store. ([43204](https://github.com/WordPress/gutenberg/pull/43204)) +- hooks: Manage BlockListBlock filters in one place. ([56875](https://github.com/WordPress/gutenberg/pull/56875)) + +#### Global Styles +- Command Palette: Use getRevisions instead of deprecated selector. ([56738](https://github.com/WordPress/gutenberg/pull/56738)) +- Global styles revisions: Remove PHP unit tests that are running in Core. ([56492](https://github.com/WordPress/gutenberg/pull/56492)) + +#### Components +- Site editor: Do not use navigator's internal classname. ([56911](https://github.com/WordPress/gutenberg/pull/56911)) + +#### Data Views +- DataViews: Remove TanStack. ([56873](https://github.com/WordPress/gutenberg/pull/56873)) + + +### Tools + +- Env: Migrate to Compose V2. ([51339](https://github.com/WordPress/gutenberg/pull/51339)) +- Scripts: Fix CSS imports not minified. ([56516](https://github.com/WordPress/gutenberg/pull/56516)) +- wp-env: Make env-cwd option work on Windows. ([56265](https://github.com/WordPress/gutenberg/pull/56265)) + +#### Testing +- Migrate 'editor multi entity saving' end-to-end tests to Playwright. ([56670](https://github.com/WordPress/gutenberg/pull/56670)) +- Migrate 'inner-blocks-locking-all-embed' end-to-end tests to Playwright. ([56673](https://github.com/WordPress/gutenberg/pull/56673)) +- Migrate 'site editor export' end-to-end tests to Playwright. ([56675](https://github.com/WordPress/gutenberg/pull/56675)) +- RN: Add watch mode for native tests. ([56788](https://github.com/WordPress/gutenberg/pull/56788)) +- Scripts: Enable skipping Playwright browser installation. ([56594](https://github.com/WordPress/gutenberg/pull/56594)) +- Tabs: Implement `ariakit/test` in unit tests. ([56835](https://github.com/WordPress/gutenberg/pull/56835)) +- `CustomSelectControl`: Add additional unit tests. ([56575](https://github.com/WordPress/gutenberg/pull/56575)) + + +### Copy + +- Copy/fix capitalization of WordPress. ([56834](https://github.com/WordPress/gutenberg/pull/56834)) + +#### Site Editor +- Improve text and design of the block removal warnings. ([56869](https://github.com/WordPress/gutenberg/pull/56869)) + +#### Global Styles +- Global styles welcome guide: Add a space between translated strings. ([56839](https://github.com/WordPress/gutenberg/pull/56839)) + +#### Block Library +- Simplify page list edit warning. ([56829](https://github.com/WordPress/gutenberg/pull/56829)) + +#### Patterns +- End pattern page descriptions with a period. ([56828](https://github.com/WordPress/gutenberg/pull/56828)) + + +## First time contributors + +The following PRs were merged by first time contributors: + +- @benoitchantre: Scripts: Fix CSS imports not minified. ([56516](https://github.com/WordPress/gutenberg/pull/56516)) +- @kmanijak: Decode some characters if used in taxonomy name so it's displayed correctly in Query Loop filters. ([50376](https://github.com/WordPress/gutenberg/pull/50376)) +- @lithrel: Env: Migrate to Compose V2. ([51339](https://github.com/WordPress/gutenberg/pull/51339)) +- @nk-o: Fix: PHP 8.1 deprecated warning strpos(). ([56171](https://github.com/WordPress/gutenberg/pull/56171)) +- @taylorgorman: Link to Dashicons. ([56872](https://github.com/WordPress/gutenberg/pull/56872)) +- @valerogarte: #55702 - Control dimensions (margin and padding) of the list-item block. ([55874](https://github.com/WordPress/gutenberg/pull/55874)) + + +## Contributors + +The following contributors merged PRs in this release: + +@afercia @ajlende @alexstine @andrewhayward @andrewserong @apeatling @atachibana @Aurorum @benoitchantre @bph @brookewp @chad1008 @ciampo @colorful-tones @dcalhoun @derekblank @draganescu @ellatrix @fluiddot @geriux @getdave @jameskoster @jasmussen @jeherve @jeryj @jffng @jonathanbossenger @jorgefilipecosta @jsnajdr @juanmaguitar @kevin940726 @kmanijak @lithrel @luisherranz @Mamaduka @matiasbenedetto @mikachan @miminari @mtias @ndiego @nk-o @ntsekouras @oandregal @ramonjd @richtabor @scruffian @SiobhyB @t-hamano @talldan @taylorgorman @tellthemachines @tyxla @valerogarte @WunderBart @youknowriad + + + + = 17.2.3 = ## Changelog From 79cca7e6a4b223bfbdefcd5df5937c6cf5274d00 Mon Sep 17 00:00:00 2001 From: Luis Herranz <luisherranz@gmail.com> Date: Wed, 20 Dec 2023 15:58:07 +0100 Subject: [PATCH 304/325] Modules API: Refactor, tests, and final dependencies array structure (#57231) * WIP * Remove the scripts code and improve the tests * Add missing comments * Add tests for get_version_query_string * Refactor and finish tests * Remove unnecessary array check * Remove HTML from comments * Escape tag attributes * Rename module preloads method * Fix typo * Rename module preloads method in tests --- .../modules/class-gutenberg-modules.php | 185 +++++++---- .../modules/class-gutenberg-modules-test.php | 299 +++++++++++++++--- 2 files changed, 380 insertions(+), 104 deletions(-) diff --git a/lib/experimental/modules/class-gutenberg-modules.php b/lib/experimental/modules/class-gutenberg-modules.php index 5f847fa8c897ad..bbe51f1376a7db 100644 --- a/lib/experimental/modules/class-gutenberg-modules.php +++ b/lib/experimental/modules/class-gutenberg-modules.php @@ -20,64 +20,90 @@ class Gutenberg_Modules { private static $registered = array(); /** - * An array of queued modules. + * An array of module identifiers that were enqueued before registered. * - * @var string[] + * @var array */ - private static $enqueued = array(); + private static $enqueued_modules_before_register = array(); /** - * Registers the module if no module with that module identifier already - * exists. + * Registers the module if no module with that module identifier has already + * been registered. * - * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. - * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. - * @param array $dependencies Optional. An array of module identifiers of the static and dynamic dependencies of this module. It can be an indexed array, in which case all the dependencies are static, or it can be an associative array, in which case it has to contain the keys `static` and `dynamic`. - * @param string|bool|null $version Optional. String specifying module version number. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, a timestamp is used. If it is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. + * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static. + * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added. */ public static function register( $module_identifier, $src, $dependencies = array(), $version = false ) { - // Register the module if it's not already registered. if ( ! isset( self::$registered[ $module_identifier ] ) ) { - $deps = array( - 'static' => isset( $dependencies['static'] ) || isset( $dependencies['dynamic'] ) ? $dependencies['static'] ?? array() : $dependencies, - 'dynamic' => isset( $dependencies['dynamic'] ) ? $dependencies['dynamic'] : array(), - ); + $deps = array(); + foreach ( $dependencies as $dependency ) { + if ( isset( $dependency['id'] ) ) { + $deps[] = array( + 'id' => $dependency['id'], + 'type' => isset( $dependency['type'] ) && 'dynamic' === $dependency['type'] ? 'dynamic' : 'static', + ); + } elseif ( is_string( $dependency ) ) { + $deps[] = array( + 'id' => $dependency, + 'type' => 'static', + ); + } + } self::$registered[ $module_identifier ] = array( 'src' => $src, 'version' => $version, + 'enqueued' => in_array( $module_identifier, self::$enqueued_modules_before_register, true ), 'dependencies' => $deps, ); } } /** - * Enqueues a module in the page. + * Marks the module to be enqueued in the page. * * @param string $module_identifier The identifier of the module. */ public static function enqueue( $module_identifier ) { - // Add the module to the queue if it's not already there. - if ( ! in_array( $module_identifier, self::$enqueued, true ) ) { - self::$enqueued[] = $module_identifier; + if ( isset( self::$registered[ $module_identifier ] ) ) { + self::$registered[ $module_identifier ]['enqueued'] = true; + } elseif ( ! in_array( $module_identifier, self::$enqueued_modules_before_register, true ) ) { + self::$enqueued_modules_before_register[] = $module_identifier; + } + } + + /** + * Unmarks the module so it is no longer enqueued in the page. + * + * @param string $module_identifier The identifier of the module. + */ + public static function dequeue( $module_identifier ) { + if ( isset( self::$registered[ $module_identifier ] ) ) { + self::$registered[ $module_identifier ]['enqueued'] = false; + } + $key = array_search( $module_identifier, self::$enqueued_modules_before_register, true ); + if ( false !== $key ) { + array_splice( self::$enqueued_modules_before_register, $key, 1 ); } } /** * Returns the import map array. * - * @return array Associative array with 'imports' key mapping to an array of module identifiers and their respective source strings. + * @return array Array with an 'imports' key mapping to an array of module identifiers and their respective source URLs, including the version query. */ public static function get_import_map() { $imports = array(); - foreach ( self::get_dependencies( self::$enqueued, array( 'static', 'dynamic' ) ) as $module_identifier => $module ) { + foreach ( self::get_dependencies( array_keys( self::get_enqueued() ) ) as $module_identifier => $module ) { $imports[ $module_identifier ] = $module['src'] . self::get_version_query_string( $module['version'] ); } return array( 'imports' => $imports ); } /** - * Prints the import map. + * Prints the import map using a script tag with an type="importmap" attribute. */ public static function print_import_map() { $import_map = self::get_import_map(); @@ -90,27 +116,30 @@ public static function print_import_map() { * Prints all the enqueued modules using <script type="module">. */ public static function print_enqueued_modules() { - foreach ( self::$enqueued as $module_identifier ) { - if ( isset( self::$registered[ $module_identifier ] ) ) { - $module = self::$registered[ $module_identifier ]; - wp_print_script_tag( - array( - 'type' => 'module', - 'src' => $module['src'] . self::get_version_query_string( $module['version'] ), - 'id' => $module_identifier, - ) - ); - } + foreach ( self::get_enqueued() as $module_identifier => $module ) { + wp_print_script_tag( + array( + 'type' => 'module', + 'src' => $module['src'] . self::get_version_query_string( $module['version'] ), + 'id' => $module_identifier, + ) + ); } } /** - * Prints the link tag with rel="modulepreload" for all the static - * dependencies of the enqueued modules. + * Prints the the static dependencies of the enqueued modules using link tags + * with rel="modulepreload" attributes. */ public static function print_module_preloads() { - foreach ( self::get_dependencies( self::$enqueued, array( 'static' ) ) as $dependency_identifier => $module ) { - echo '<link rel="modulepreload" href="' . $module['src'] . self::get_version_query_string( $module['version'] ) . '" id="' . $dependency_identifier . '">'; + foreach ( self::get_dependencies( array_keys( self::get_enqueued() ), array( 'static' ) ) as $module_identifier => $module ) { + if ( true !== $module['enqueued'] ) { + echo sprintf( + '<link rel="modulepreload" href="%s" id="%s">', + esc_attr( $module['src'] . self::get_version_query_string( $module['version'] ) ), + esc_attr( $module_identifier ) + ); + } } } @@ -120,7 +149,7 @@ public static function print_module_preloads() { * * TODO: Replace the polyfill with a simpler version that only provides * support for import maps and load it only when the browser doesn't support - * import maps (https://github.com/guybedford/es-module-shims/issues/371). + * import maps (https://github.com/guybedford/es-module-shims/issues/406). */ public static function print_import_map_polyfill() { $import_map = self::get_import_map(); @@ -135,15 +164,17 @@ public static function print_import_map_polyfill() { } /** - * Gets the module's version. It either returns a timestamp (if SCRIPT_DEBUG - * is true), the explicit version of the module if it is set and not false, or - * an empty string if none of the above conditions are met. + * Gets the version of a module. + * + * If SCRIPT_DEBUG is true, the version is the current timestamp. If $version + * is set to false, the version number is the currently installed WordPress + * version. If $version is set to null, no version is added. * * @param array $version The version of the module. * @return string A string presenting the version. */ private static function get_version_query_string( $version ) { - if ( SCRIPT_DEBUG ) { + if ( defined( 'SCRIPT_DEBUG ' ) && SCRIPT_DEBUG ) { return '?ver=' . time(); } elseif ( false === $version ) { return '?ver=' . get_bloginfo( 'version' ); @@ -154,29 +185,46 @@ private static function get_version_query_string( $version ) { } /** - * Returns all unique static and/or dynamic dependencies for the received modules. It's - * recursive, so it will also get the static or dynamic dependencies of the dependencies. + * Retrieves an array of enqueued modules. + * + * @return array Array of modules keyed by module identifier. + */ + private static function get_enqueued() { + $enqueued = array(); + foreach ( self::$registered as $module_identifier => $module ) { + if ( true === $module['enqueued'] ) { + $enqueued[ $module_identifier ] = $module; + } + } + return $enqueued; + } + + /** + * Retrieves all the dependencies for given modules depending on type. * - * @param array $module_identifiers The identifiers of the modules to get dependencies for. - * @param array $types The type of dependencies to retrieve. It can be `static`, `dynamic` or both. - * @return array The array containing the unique dependencies of the modules. + * This method is recursive to also retrieve dependencies of the dependencies. + * It will consolidate an array containing unique dependencies based on the + * requested types ('static' or 'dynamic'). + * + * @param array $module_identifiers The identifiers of the modules for which to gather dependencies. + * @param array $types Optional. Types of dependencies to retrieve: 'static', 'dynamic', or both. Default is both. + * @return array Array of modules keyed by module identifier. */ private static function get_dependencies( $module_identifiers, $types = array( 'static', 'dynamic' ) ) { return array_reduce( $module_identifiers, function ( $dependency_modules, $module_identifier ) use ( $types ) { - if ( ! isset( self::$registered[ $module_identifier ] ) ) { - return $dependency_modules; - } - $dependencies = array(); - foreach ( $types as $type ) { - $dependencies = array_merge( $dependencies, self::$registered[ $module_identifier ]['dependencies'][ $type ] ); + foreach ( self::$registered[ $module_identifier ]['dependencies'] as $dependency ) { + if ( + in_array( $dependency['type'], $types, true ) && + isset( self::$registered[ $dependency['id'] ] ) && + ! isset( $dependency_modules[ $dependency['id'] ] ) + ) { + $dependencies[ $dependency['id'] ] = self::$registered[ $dependency['id'] ]; + } } - $dependencies = array_unique( $dependencies ); - $dependency_modules = array_intersect_key( self::$registered, array_flip( $dependencies ) ); - - return array_merge( $dependency_modules, $dependency_modules, self::get_dependencies( $dependencies, $types ) ); + return array_merge( $dependency_modules, $dependencies, self::get_dependencies( array_keys( $dependencies ), $types ) ); }, array() ); @@ -184,27 +232,36 @@ function ( $dependency_modules, $module_identifier ) use ( $types ) { } /** - * Registers a JavaScript module. It will be added to the import map. + * Registers the module if no module with that module identifier has already + * been registered. * - * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. - * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. - * @param array $dependencies Optional. An array of module identifiers of the static and dynamic dependencies of this module. It can be an indexed array, in which case all the dependencies are static, or it can be an associative array, in which case it has to contain the keys `static` and `dynamic`. - * @param string|bool|null $version Optional. String specifying module version number. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, a timestamp is used. If it is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added. + * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + * @param string $src Full URL of the module, or path of the script relative to the WordPress root directory. + * @param array $dependencies Optional. An array of module identifiers of the dependencies of this module. The dependencies can be strings or arrays. If they are arrays, they need an `id` key with the module identifier, and can contain a `type` key with either `static` or `dynamic`. By default, dependencies that don't contain a type are considered static. + * @param string|false|null $version Optional. String specifying module version number. Defaults to false. It is added to the URL as a query string for cache busting purposes. If SCRIPT_DEBUG is true, the version is the current timestamp. If $version is set to false, the version number is the currently installed WordPress version. If $version is set to null, no version is added. */ function gutenberg_register_module( $module_identifier, $src, $dependencies = array(), $version = false ) { Gutenberg_Modules::register( $module_identifier, $src, $dependencies, $version ); } /** - * Enqueues a JavaScript module. It will be added to both the import map and a - * script tag with the "module" type. + * Marks the module to be enqueued in the page. * - * @param string $module_identifier The identifier of the module. Should be unique. It will be used in the final import map. + * @param string $module_identifier The identifier of the module. */ function gutenberg_enqueue_module( $module_identifier ) { Gutenberg_Modules::enqueue( $module_identifier ); } +/** + * Unmarks the module so it is not longer enqueued in the page. + * + * @param string $module_identifier The identifier of the module. + */ +function gutenberg_dequeue_module( $module_identifier ) { + Gutenberg_Modules::dequeue( $module_identifier ); +} + // Prints the import map in the head tag. add_action( 'wp_head', array( 'Gutenberg_Modules', 'print_import_map' ) ); diff --git a/phpunit/experimental/modules/class-gutenberg-modules-test.php b/phpunit/experimental/modules/class-gutenberg-modules-test.php index 87ff6e6647d7bf..a7f6c3b491a53d 100644 --- a/phpunit/experimental/modules/class-gutenberg-modules-test.php +++ b/phpunit/experimental/modules/class-gutenberg-modules-test.php @@ -9,58 +9,277 @@ * Tests for the Gutenberg_Modules_Test class. */ class Gutenberg_Modules_Test extends WP_UnitTestCase { - - protected $old_wp_scripts; - protected $old_modules_markup; + protected $registered; + protected $old_registered; public function set_up() { parent::set_up(); - - $this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null; - remove_action( 'wp_default_scripts', 'wp_default_scripts' ); - remove_action( 'wp_default_scripts', 'wp_default_packages' ); - $GLOBALS['wp_scripts'] = new WP_Scripts(); - $this->old_modules_markup = get_echo( array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + $this->registered = new ReflectionProperty( 'Gutenberg_Modules', 'registered' ); + $this->registered->setAccessible( true ); + $this->old_registered = $this->registered->getValue(); + $this->registered->setValue( array() ); } public function tear_down() { - $GLOBALS['wp_scripts'] = $this->old_wp_scripts; - add_action( 'wp_default_scripts', 'wp_default_scripts' ); + $this->registered->setValue( $this->old_registered ); parent::tear_down(); } - public function test_wp_enqueue_module() { - global $wp_version; - gutenberg_register_module( 'no-deps-no-version', 'interactivity-api-1.js' ); - gutenberg_enqueue_module( 'no-deps-no-version' ); - gutenberg_register_module( 'deps-no-version', 'interactivity-api-2.js', array( 'no-deps-no-version' ) ); - gutenberg_enqueue_module( 'deps-no-version' ); + public function get_enqueued_modules() { + $modules_markup = get_echo( array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + $p = new WP_HTML_Tag_Processor( $modules_markup ); + $enqueued_modules = array(); + + while ( $p->next_tag( + array( + 'tag' => 'SCRIPT', + 'type' => 'module', + ) + ) ) { + $enqueued_modules[ $p->get_attribute( 'id' ) ] = $p->get_attribute( 'src' ); + } - $modules_markup = get_echo( array( 'Gutenberg_Modules', 'print_enqueued_modules' ) ); + return $enqueued_modules; + } + + public function get_import_map() { $import_map_markup = get_echo( array( 'Gutenberg_Modules', 'print_import_map' ) ); - $preload_markup = get_echo( array( 'Gutenberg_Modules', 'print_module_preloads' ) ); + preg_match( '/<script type="importmap">([^<]+)<\/script>/s', $import_map_markup, $import_map_string ); + return json_decode( $import_map_string[1], true )['imports']; + } - $previous_tags = new WP_HTML_Tag_Processor( $this->old_modules_markup ); - $previous_src_stack = array(); - while ( $previous_tags->next_tag( array( 'type' => 'module' ) ) ) { - $previous_src_stack[] = $previous_tags->get_attribute( 'src' ); - } - // Test that there are 2 new <script type="module"> added to the markup. - $tags = new WP_HTML_Tag_Processor( $modules_markup ); - $src_stack = array(); - while ( $tags->next_tag( array( 'type' => 'module' ) ) ) { - $src_stack[] = $tags->get_attribute( 'src' ); + public function get_preloaded_modules() { + $preloaded_markup = get_echo( array( 'Gutenberg_Modules', 'print_module_preloads' ) ); + $p = new WP_HTML_Tag_Processor( $preloaded_markup ); + $preloaded_modules = array(); + + while ( $p->next_tag( + array( + 'tag' => 'LINK', + 'rel' => 'modulepreload', + ) + ) ) { + $preloaded_modules[ $p->get_attribute( 'id' ) ] = $p->get_attribute( 'href' ); } - $new_src_stack = array_values( array_diff( $src_stack, $previous_src_stack ) ); - $this->assertEquals( 2, count( $new_src_stack ) ); - $this->assertEquals( 'interactivity-api-1.js?ver=' . $wp_version, $new_src_stack[0] ); - $this->assertEquals( 'interactivity-api-2.js?ver=' . $wp_version, $new_src_stack[1] ); - // Test that there is 1 <script type="importmap"> added to the markup. - $tags = new WP_HTML_Tag_Processor( $import_map_markup ); - $this->assertEquals( true, $tags->next_tag( array( 'rel' => 'importmap' ) ) ); - - // Test that there is 1 <link type="modulepreload"> added to the markup. - $tags = new WP_HTML_Tag_Processor( $preload_markup ); - $this->assertEquals( true, $tags->next_tag( array( 'rel' => 'modulepreload' ) ) ); + + return $preloaded_modules; + } + + public function test_gutenberg_enqueue_module() { + gutenberg_register_module( 'foo', '/foo.js' ); + gutenberg_register_module( 'bar', '/bar.js' ); + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'bar' ); + + $enqueued_modules = $this->get_enqueued_modules(); + + $this->assertEquals( 2, count( $enqueued_modules ) ); + $this->assertEquals( true, str_starts_with( $enqueued_modules['foo'], '/foo.js' ) ); + $this->assertEquals( true, str_starts_with( $enqueued_modules['bar'], '/bar.js' ) ); + } + + public function test_gutenberg_dequeue_module() { + gutenberg_register_module( 'foo', '/foo.js' ); + gutenberg_register_module( 'bar', '/bar.js' ); + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'bar' ); + gutenberg_dequeue_module( 'foo' ); // Dequeued. + + $enqueued_modules = $this->get_enqueued_modules(); + + $this->assertEquals( 1, count( $enqueued_modules ) ); + $this->assertEquals( false, isset( $enqueued_modules['foo'] ) ); + $this->assertEquals( true, isset( $enqueued_modules['bar'] ) ); + } + + public function test_gutenberg_enqueue_module_works_before_register() { + gutenberg_enqueue_module( 'foo' ); + gutenberg_register_module( 'foo', '/foo.js' ); + gutenberg_enqueue_module( 'bar' ); // Not registered. + + $enqueued_modules = $this->get_enqueued_modules(); + + $this->assertEquals( 1, count( $enqueued_modules ) ); + $this->assertEquals( true, str_starts_with( $enqueued_modules['foo'], '/foo.js' ) ); + $this->assertEquals( false, isset( $enqueued_modules['bar'] ) ); + } + + public function test_gutenberg_dequeue_module_works_before_register() { + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'bar' ); + gutenberg_dequeue_module( 'foo' ); + gutenberg_register_module( 'foo', '/foo.js' ); + gutenberg_register_module( 'bar', '/bar.js' ); + + $enqueued_modules = $this->get_enqueued_modules(); + + $this->assertEquals( 1, count( $enqueued_modules ) ); + $this->assertEquals( false, isset( $enqueued_modules['foo'] ) ); + $this->assertEquals( true, isset( $enqueued_modules['bar'] ) ); + } + + public function test_gutenberg_import_map_dependencies() { + gutenberg_register_module( 'foo', '/foo.js', array( 'dep' ) ); + gutenberg_register_module( 'dep', '/dep.js' ); + gutenberg_register_module( 'no-dep', '/no-dep.js' ); + gutenberg_enqueue_module( 'foo' ); + + $import_map = $this->get_import_map(); + + $this->assertEquals( 1, count( $import_map ) ); + $this->assertEquals( true, str_starts_with( $import_map['dep'], '/dep.js' ) ); + $this->assertEquals( false, isset( $import_map['no-dep'] ) ); + } + + public function test_gutenberg_import_map_no_duplicate_dependencies() { + gutenberg_register_module( 'foo', '/foo.js', array( 'dep' ) ); + gutenberg_register_module( 'bar', '/bar.js', array( 'dep' ) ); + gutenberg_register_module( 'dep', '/dep.js' ); + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'bar' ); + + $import_map = $this->get_import_map(); + + $this->assertEquals( 1, count( $import_map ) ); + $this->assertEquals( true, str_starts_with( $import_map['dep'], '/dep.js' ) ); + } + + public function test_gutenberg_import_map_recursive_dependencies() { + gutenberg_register_module( + 'foo', + '/foo.js', + array( + 'static-dep', + array( + 'id' => 'dynamic-dep', + 'type' => 'dynamic', + ), + ) + ); + gutenberg_register_module( + 'static-dep', + '/static-dep.js', + array( + array( + 'id' => 'nested-static-dep', + 'type' => 'static', + ), + array( + 'id' => 'nested-dynamic-dep', + 'type' => 'dynamic', + ), + ) + ); + gutenberg_register_module( 'dynamic-dep', '/dynamic-dep.js' ); + gutenberg_register_module( 'nested-static-dep', '/nested-static-dep.js' ); + gutenberg_register_module( 'nested-dynamic-dep', '/nested-dynamic-dep.js' ); + gutenberg_register_module( 'no-dep', '/no-dep.js' ); + gutenberg_enqueue_module( 'foo' ); + + $import_map = $this->get_import_map(); + + $this->assertEquals( true, str_starts_with( $import_map['static-dep'], '/static-dep.js' ) ); + $this->assertEquals( true, str_starts_with( $import_map['dynamic-dep'], '/dynamic-dep.js' ) ); + $this->assertEquals( true, str_starts_with( $import_map['nested-static-dep'], '/nested-static-dep.js' ) ); + $this->assertEquals( true, str_starts_with( $import_map['nested-dynamic-dep'], '/nested-dynamic-dep.js' ) ); + $this->assertEquals( false, isset( $import_map['no-dep'] ) ); + } + + public function test_gutenberg_enqueue_preloaded_static_dependencies() { + gutenberg_register_module( + 'foo', + '/foo.js', + array( + 'static-dep', + array( + 'id' => 'dynamic-dep', + 'type' => 'dynamic', + ), + ) + ); + gutenberg_register_module( + 'static-dep', + '/static-dep.js', + array( + array( + 'id' => 'nested-static-dep', + 'type' => 'static', + ), + array( + 'id' => 'nested-dynamic-dep', + 'type' => 'dynamic', + ), + ) + ); + gutenberg_register_module( 'dynamic-dep', '/dynamic-dep.js' ); + gutenberg_register_module( 'nested-static-dep', '/nested-static-dep.js' ); + gutenberg_register_module( 'nested-dynamic-dep', '/nested-dynamic-dep.js' ); + gutenberg_register_module( 'no-dep', '/no-dep.js' ); + gutenberg_enqueue_module( 'foo' ); + + $preloaded_modules = $this->get_preloaded_modules(); + + $this->assertEquals( 2, count( $preloaded_modules ) ); + $this->assertEquals( true, str_starts_with( $preloaded_modules['static-dep'], '/static-dep.js' ) ); + $this->assertEquals( true, str_starts_with( $preloaded_modules['nested-static-dep'], '/nested-static-dep.js' ) ); + $this->assertEquals( false, isset( $import_map['no-dep'] ) ); + $this->assertEquals( false, isset( $import_map['dynamic-dep'] ) ); + $this->assertEquals( false, isset( $import_map['nested-dynamic-dep'] ) ); + } + + public function test_gutenberg_preloaded_dependencies_filter_enqueued_modules() { + gutenberg_register_module( + 'foo', + '/foo.js', + array( + 'dep', + 'enqueued-dep', + ) + ); + gutenberg_register_module( 'dep', '/dep.js' ); + gutenberg_register_module( 'enqueued-dep', '/enqueued-dep.js' ); + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'enqueued-dep' ); // Not preloaded. + + $preloaded_modules = $this->get_preloaded_modules(); + + $this->assertEquals( 1, count( $preloaded_modules ) ); + $this->assertEquals( true, isset( $preloaded_modules['dep'] ) ); + $this->assertEquals( false, isset( $preloaded_modules['enqueued-dep'] ) ); + } + + public function test_gutenberg_enqueued_modules_with_dependants_add_import_map() { + gutenberg_register_module( + 'foo', + '/foo.js', + array( + 'dep', + 'enqueued-dep', + ) + ); + gutenberg_register_module( 'dep', '/dep.js' ); + gutenberg_register_module( 'enqueued-dep', '/enqueued-dep.js' ); + gutenberg_enqueue_module( 'foo' ); + gutenberg_enqueue_module( 'enqueued-dep' ); // Also in the import map. + + $import_map = $this->get_import_map(); + + $this->assertEquals( 2, count( $import_map ) ); + $this->assertEquals( true, isset( $import_map['dep'] ) ); + $this->assertEquals( true, isset( $import_map['enqueued-dep'] ) ); + } + + public function test_get_version_query_string() { + $get_version_query_string = new ReflectionMethod( 'Gutenberg_Modules', 'get_version_query_string' ); + $get_version_query_string->setAccessible( true ); + + $result = $get_version_query_string->invoke( null, '1.0' ); + $this->assertEquals( '?ver=1.0', $result ); + + $result = $get_version_query_string->invoke( null, false ); + $this->assertEquals( '?ver=' . get_bloginfo( 'version' ), $result ); + + $result = $get_version_query_string->invoke( null, null ); + $this->assertEquals( '', $result ); } } From 4173b3060a7aa01785915e2a1d2991cc4d6a27b5 Mon Sep 17 00:00:00 2001 From: Ella <4710635+ellatrix@users.noreply.github.com> Date: Wed, 20 Dec 2023 17:17:40 +0200 Subject: [PATCH 305/325] Writing flow: absorb partial multi selection dispatching (#47525) --- .../src/components/rich-text/index.js | 6 +- .../src/components/writing-flow/index.js | 1 - .../writing-flow/use-drag-selection.js | 22 ++- .../writing-flow/use-selection-observer.js | 136 +++++++++++++----- .../src/component/use-input-and-selection.js | 65 ++------- .../various/multi-block-selection.spec.js | 10 ++ 6 files changed, 147 insertions(+), 93 deletions(-) diff --git a/packages/block-editor/src/components/rich-text/index.js b/packages/block-editor/src/components/rich-text/index.js index ef21a8aa4ab239..51a70677a5edc7 100644 --- a/packages/block-editor/src/components/rich-text/index.js +++ b/packages/block-editor/src/components/rich-text/index.js @@ -325,10 +325,13 @@ export function RichTextWrapper( { ...props } { ...autocompleteProps } ref={ useMergeRefs( [ + // Rich text ref must be first because its focus listener + // must be set up before any other ref calls .focus() on + // mount. + richTextRef, forwardedRef, autocompleteProps.ref, props.ref, - richTextRef, useBeforeInputRules( { value, onChange } ), useInputRules( { getValue, @@ -387,6 +390,7 @@ export function RichTextWrapper( // tabIndex because Safari will focus the element. However, // Safari will correctly ignore nested contentEditable elements. tabIndex={ props.tabIndex === 0 ? null : props.tabIndex } + data-wp-block-attribute-key={ identifier } /> </> ); diff --git a/packages/block-editor/src/components/writing-flow/index.js b/packages/block-editor/src/components/writing-flow/index.js index f15c1dac0267fd..c76ab74b03b775 100644 --- a/packages/block-editor/src/components/writing-flow/index.js +++ b/packages/block-editor/src/components/writing-flow/index.js @@ -47,7 +47,6 @@ export function useWritingFlow() { useRefEffect( ( node ) => { node.tabIndex = 0; - node.contentEditable = hasMultiSelection; if ( ! hasMultiSelection ) { return; diff --git a/packages/block-editor/src/components/writing-flow/use-drag-selection.js b/packages/block-editor/src/components/writing-flow/use-drag-selection.js index e013776e49c7a8..8d8791afe5f09d 100644 --- a/packages/block-editor/src/components/writing-flow/use-drag-selection.js +++ b/packages/block-editor/src/components/writing-flow/use-drag-selection.js @@ -27,8 +27,12 @@ function setContentEditableWrapper( node, value ) { export default function useDragSelection() { const { startMultiSelect, stopMultiSelect } = useDispatch( blockEditorStore ); - const { isSelectionEnabled, hasMultiSelection, isDraggingBlocks } = - useSelect( blockEditorStore ); + const { + isSelectionEnabled, + hasSelectedBlock, + isDraggingBlocks, + isMultiSelecting, + } = useSelect( blockEditorStore ); return useRefEffect( ( node ) => { const { ownerDocument } = node; @@ -45,7 +49,7 @@ export default function useDragSelection() { // so wait until the next animation frame to get the browser // selection. rafId = defaultView.requestAnimationFrame( () => { - if ( hasMultiSelection() ) { + if ( ! hasSelectedBlock() ) { return; } @@ -84,6 +88,16 @@ export default function useDragSelection() { return; } + // Abort if we are already multi-selecting. + if ( isMultiSelecting() ) { + return; + } + + // Abort if selection is leaving writing flow. + if ( node === target ) { + return; + } + // Check the attribute, not the contentEditable attribute. All // child elements of the content editable wrapper are editable // and return true for this property. We only want to start @@ -127,7 +141,7 @@ export default function useDragSelection() { startMultiSelect, stopMultiSelect, isSelectionEnabled, - hasMultiSelection, + hasSelectedBlock, ] ); } diff --git a/packages/block-editor/src/components/writing-flow/use-selection-observer.js b/packages/block-editor/src/components/writing-flow/use-selection-observer.js index b780950dae0ea2..57082f90c055fd 100644 --- a/packages/block-editor/src/components/writing-flow/use-selection-observer.js +++ b/packages/block-editor/src/components/writing-flow/use-selection-observer.js @@ -3,6 +3,7 @@ */ import { useSelect, useDispatch } from '@wordpress/data'; import { useRefEffect } from '@wordpress/compose'; +import { create } from '@wordpress/rich-text'; /** * Internal dependencies @@ -75,10 +76,20 @@ function setContentEditableWrapper( node, value ) { // Since we are calling this on every selection change, check if the value // needs to be updated first because it trigger the browser to recalculate // style. - if ( node.contentEditable !== String( value ) ) + if ( node.contentEditable !== String( value ) ) { node.contentEditable = value; - // Firefox doesn't automatically move focus. - if ( value ) node.focus(); + + // Firefox doesn't automatically move focus. + if ( value ) { + node.focus(); + } + } +} + +function getRichTextElement( node ) { + const element = + node.nodeType === node.ELEMENT_NODE ? node : node.parentElement; + return element?.closest( '[data-wp-block-attribute-key]' ); } /** @@ -87,7 +98,7 @@ function setContentEditableWrapper( node, value ) { export default function useSelectionObserver() { const { multiSelect, selectBlock, selectionChange } = useDispatch( blockEditorStore ); - const { getBlockParents, getBlockSelectionStart } = + const { getBlockParents, getBlockSelectionStart, isMultiSelecting } = useSelect( blockEditorStore ); return useRefEffect( ( node ) => { @@ -101,6 +112,16 @@ export default function useSelectionObserver() { return; } + const startNode = extractSelectionStartNode( selection ); + const endNode = extractSelectionEndNode( selection ); + + if ( + ! node.contains( startNode ) || + ! node.contains( endNode ) + ) { + return; + } + // If selection is collapsed and we haven't used `shift+click`, // end multi selection and disable the contentEditable wrapper. // We have to check about `shift+click` case because elements @@ -109,16 +130,24 @@ export default function useSelectionObserver() { // For now we check if the event is a `mouse` event. const isClickShift = event.shiftKey && event.type === 'mouseup'; if ( selection.isCollapsed && ! isClickShift ) { - setContentEditableWrapper( node, false ); + if ( + node.contentEditable === 'true' && + ! isMultiSelecting() + ) { + setContentEditableWrapper( node, false ); + let element = + startNode.nodeType === startNode.ELEMENT_NODE + ? startNode + : startNode.parentElement; + element = element?.closest( '[contenteditable]' ); + element?.focus(); + } return; } - let startClientId = getBlockClientId( - extractSelectionStartNode( selection ) - ); - let endClientId = getBlockClientId( - extractSelectionEndNode( selection ) - ); + let startClientId = getBlockClientId( startNode ); + let endClientId = getBlockClientId( endNode ); + // If the selection has changed and we had pressed `shift+click`, // we need to check if in an element that doesn't support // text selection has been clicked. @@ -155,7 +184,11 @@ export default function useSelectionObserver() { const isSingularSelection = startClientId === endClientId; if ( isSingularSelection ) { - selectBlock( startClientId ); + if ( ! isMultiSelecting() ) { + selectBlock( startClientId ); + } else { + multiSelect( startClientId, startClientId ); + } } else { const startPath = [ ...getBlockParents( startClientId ), @@ -167,39 +200,68 @@ export default function useSelectionObserver() { ]; const depth = findDepth( startPath, endPath ); - multiSelect( startPath[ depth ], endPath[ depth ] ); - } - } + if ( + startPath[ depth ] !== startClientId || + endPath[ depth ] !== endClientId + ) { + multiSelect( startPath[ depth ], endPath[ depth ] ); + return; + } - function addListeners() { - ownerDocument.addEventListener( - 'selectionchange', - onSelectionChange - ); - defaultView.addEventListener( 'mouseup', onSelectionChange ); + const richTextElementStart = + getRichTextElement( startNode ); + const richTextElementEnd = getRichTextElement( endNode ); + + if ( richTextElementStart && richTextElementEnd ) { + const range = selection.getRangeAt( 0 ); + const richTextDataStart = create( { + element: richTextElementStart, + range, + __unstableIsEditableTree: true, + } ); + const richTextDataEnd = create( { + element: richTextElementEnd, + range, + __unstableIsEditableTree: true, + } ); + + const startOffset = + richTextDataStart.start ?? richTextDataStart.end; + const endOffset = + richTextDataEnd.start ?? richTextDataEnd.end; + selectionChange( { + start: { + clientId: startClientId, + attributeKey: + richTextElementStart.dataset + .wpBlockAttributeKey, + offset: startOffset, + }, + end: { + clientId: endClientId, + attributeKey: + richTextElementEnd.dataset + .wpBlockAttributeKey, + offset: endOffset, + }, + } ); + } else { + multiSelect( startClientId, endClientId ); + } + } } - function removeListeners() { + ownerDocument.addEventListener( + 'selectionchange', + onSelectionChange + ); + defaultView.addEventListener( 'mouseup', onSelectionChange ); + return () => { ownerDocument.removeEventListener( 'selectionchange', onSelectionChange ); defaultView.removeEventListener( 'mouseup', onSelectionChange ); - } - - function resetListeners() { - removeListeners(); - addListeners(); - } - - addListeners(); - // We must allow rich text to set selection first. This ensures that - // our `selectionchange` listener is always reset to be called after - // the rich text one. - node.addEventListener( 'focusin', resetListeners ); - return () => { - removeListeners(); - node.removeEventListener( 'focusin', resetListeners ); }; }, [ multiSelect, selectBlock, selectionChange, getBlockParents ] diff --git a/packages/rich-text/src/component/use-input-and-selection.js b/packages/rich-text/src/component/use-input-and-selection.js index 870bfe170635c0..ecfcc2151a989e 100644 --- a/packages/rich-text/src/component/use-input-and-selection.js +++ b/packages/rich-text/src/component/use-input-and-selection.js @@ -126,48 +126,14 @@ export function useInputAndSelection( props ) { return; } - // If the selection changes where the active element is a parent of - // the rich text instance (writing flow), call `onSelectionChange` - // for the rich text instance that contains the start or end of the - // selection. + // Ensure the active element is the rich text element. if ( ownerDocument.activeElement !== element ) { - // Only process if the active elment is contentEditable, either - // this rich text instance or the writing flow parent. Fixes a - // bug in Firefox where it strangely selects the closest - // contentEditable element, even though the click was outside - // any contentEditable element. - if ( ownerDocument.activeElement.contentEditable !== 'true' ) { - return; - } - - if ( ! ownerDocument.activeElement.contains( element ) ) { - return; - } - - const selection = defaultView.getSelection(); - const { anchorNode, focusNode } = selection; - - if ( - element.contains( anchorNode ) && - element !== anchorNode && - element.contains( focusNode ) && - element !== focusNode - ) { - const { start, end } = createRecord(); - record.current.activeFormats = EMPTY_ACTIVE_FORMATS; - onSelectionChange( start, end ); - } else if ( - element.contains( anchorNode ) && - element !== anchorNode - ) { - const { start, end: offset = start } = createRecord(); - record.current.activeFormats = EMPTY_ACTIVE_FORMATS; - onSelectionChange( offset ); - } else if ( element.contains( focusNode ) ) { - const { start, end: offset = start } = createRecord(); - record.current.activeFormats = EMPTY_ACTIVE_FORMATS; - onSelectionChange( undefined, offset ); - } + // If it is not, we can stop listening for selection changes. + // We resume listening when the element is focused. + ownerDocument.removeEventListener( + 'selectionchange', + handleSelectionChange + ); return; } @@ -276,18 +242,21 @@ export function useInputAndSelection( props ) { }; } else { applyRecord( record.current ); - onSelectionChange( record.current.start, record.current.end ); } + + onSelectionChange( record.current.start, record.current.end ); + + ownerDocument.addEventListener( + 'selectionchange', + handleSelectionChange + ); } element.addEventListener( 'input', onInput ); element.addEventListener( 'compositionstart', onCompositionStart ); element.addEventListener( 'compositionend', onCompositionEnd ); element.addEventListener( 'focus', onFocus ); - ownerDocument.addEventListener( - 'selectionchange', - handleSelectionChange - ); + return () => { element.removeEventListener( 'input', onInput ); element.removeEventListener( @@ -296,10 +265,6 @@ export function useInputAndSelection( props ) { ); element.removeEventListener( 'compositionend', onCompositionEnd ); element.removeEventListener( 'focus', onFocus ); - ownerDocument.removeEventListener( - 'selectionchange', - handleSelectionChange - ); }; }, [] ); } diff --git a/test/e2e/specs/editor/various/multi-block-selection.spec.js b/test/e2e/specs/editor/various/multi-block-selection.spec.js index 585e4f1851373a..a7d7444a223f79 100644 --- a/test/e2e/specs/editor/various/multi-block-selection.spec.js +++ b/test/e2e/specs/editor/various/multi-block-selection.spec.js @@ -693,6 +693,16 @@ test.describe( 'Multi-block selection', () => { force: true, } ); + await expect + .poll( multiBlockSelectionUtils.getSelectedFlatIndices ) + .toEqual( [ 1 ] ); + + await paragraph1.click( { + position: { x: -1, y: 0 }, + // Use force since it's outside the bounding box of the element. + force: true, + } ); + await expect .poll( () => page.evaluate( () => window.getSelection().rangeCount ) From 415ecc997f03c97f92a201fc83c3b2b8191d8b4a Mon Sep 17 00:00:00 2001 From: Jonathan Desrosiers <desrosj@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:01:32 -0500 Subject: [PATCH 306/325] Update to Node.js 20.x (#56331) * Update to Node.js 18.x, but also test 20.x. * Remove upper limit. * update node v18 usage and active LTS alignment in docs * Update to Node.js 20.x. * Also update the pull request automation workflow. * Updating lock file. * Check for the latest version of Node. * Simplifying some matrices. * Add `check-latest` to prevent failures on outdated images. * Try testing Node.js 21.x. * Correct `continue-on-error` syntax. * Job name improvements. * Remove allowed failures. --------- Co-authored-by: Markus Dobmann <m.do@posteo.net> --- .github/setup-node/action.yml | 1 + .github/workflows/build-plugin-zip.yml | 2 ++ .github/workflows/bundle-size.yml | 1 + .github/workflows/create-block.yml | 6 +++--- .github/workflows/end2end-test.yml | 2 -- .github/workflows/publish-npm-packages.yml | 2 ++ .github/workflows/pull-request-automation.yml | 6 ++---- .github/workflows/static-checks.yml | 1 + .github/workflows/unit-test.yml | 7 +++---- .nvmrc | 2 +- .../code/getting-started-with-code-contribution.md | 2 +- package-lock.json | 4 ++-- package.json | 4 ++-- platform-docs/docs/intro.md | 6 +++--- 14 files changed, 24 insertions(+), 22 deletions(-) diff --git a/.github/setup-node/action.yml b/.github/setup-node/action.yml index 22cb81618a1efb..fccce2e4e93bcf 100644 --- a/.github/setup-node/action.yml +++ b/.github/setup-node/action.yml @@ -14,6 +14,7 @@ runs: with: node-version-file: '.nvmrc' node-version: ${{ inputs.node-version }} + check-latest: true cache: npm - name: Get Node.js and npm version diff --git a/.github/workflows/build-plugin-zip.yml b/.github/workflows/build-plugin-zip.yml index 8f649f1e15889d..a2628bf7af6160 100644 --- a/.github/workflows/build-plugin-zip.yml +++ b/.github/workflows/build-plugin-zip.yml @@ -174,6 +174,7 @@ jobs: uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' + check-latest: true cache: npm - name: Build Gutenberg plugin ZIP file @@ -336,6 +337,7 @@ jobs: with: node-version-file: 'main/.nvmrc' registry-url: 'https://registry.npmjs.org' + check-latest: true - name: Publish packages to npm ("latest" dist-tag) run: | diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 3b4d51bddbda0b..23b245cb6f114b 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -46,6 +46,7 @@ jobs: uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' + check-latest: true cache: npm - uses: preactjs/compressed-size-action@8119d3d31b6e57b167e09c81dfa877eada3bcb35 # v2.5.0 diff --git a/.github/workflows/create-block.yml b/.github/workflows/create-block.yml index 0e4325b53f69da..d817ac1e0be976 100644 --- a/.github/workflows/create-block.yml +++ b/.github/workflows/create-block.yml @@ -14,14 +14,14 @@ concurrency: jobs: checks: - name: Checks + name: Checks w/Node.js ${{ matrix.node }} on ${{ matrix.os }} runs-on: ${{ matrix.os }} if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} strategy: fail-fast: false matrix: - node: ['16'] - os: [macos-latest, ubuntu-latest, windows-latest] + node: ['20', '21'] + os: ['macos-latest', 'ubuntu-latest', 'windows-latest'] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/.github/workflows/end2end-test.yml b/.github/workflows/end2end-test.yml index ddbf714cb50232..d065bf8afad44d 100644 --- a/.github/workflows/end2end-test.yml +++ b/.github/workflows/end2end-test.yml @@ -20,8 +20,6 @@ jobs: name: Puppeteer runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 diff --git a/.github/workflows/publish-npm-packages.yml b/.github/workflows/publish-npm-packages.yml index 18bdb63a6c3770..163012451d6002 100644 --- a/.github/workflows/publish-npm-packages.yml +++ b/.github/workflows/publish-npm-packages.yml @@ -71,6 +71,7 @@ jobs: with: node-version-file: 'cli/.nvmrc' registry-url: 'https://registry.npmjs.org' + check-latest: true - name: Setup Node.js (for WP major version) if: ${{ github.event.inputs.release_type == 'wp' && github.event.inputs.wp_version }} @@ -78,6 +79,7 @@ jobs: with: node-version-file: 'publish/.nvmrc' registry-url: 'https://registry.npmjs.org' + check-latest: true - name: Publish development packages to npm ("next" dist-tag) if: ${{ github.event.inputs.release_type == 'development' }} diff --git a/.github/workflows/pull-request-automation.yml b/.github/workflows/pull-request-automation.yml index b8154e335776a2..785b42a19054db 100644 --- a/.github/workflows/pull-request-automation.yml +++ b/.github/workflows/pull-request-automation.yml @@ -8,9 +8,6 @@ jobs: pull-request-automation: runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' }} - strategy: - matrix: - node: ['16'] steps: # Checkout defaults to using the branch which triggered the event, which @@ -23,7 +20,8 @@ jobs: - name: Use desired version of Node.js uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: - node-version: ${{ matrix.node }} + node-version-file: '.nvmrc' + check-latest: true - name: Cache NPM packages uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 diff --git a/.github/workflows/static-checks.yml b/.github/workflows/static-checks.yml index ff8c27b14e39e8..2465f357a97cf0 100644 --- a/.github/workflows/static-checks.yml +++ b/.github/workflows/static-checks.yml @@ -30,6 +30,7 @@ jobs: uses: actions/setup-node@8f152de45cc393bb48ce5d89d36b731f54556e65 # v4.0.0 with: node-version-file: '.nvmrc' + check-latest: true cache: npm - name: Npm install diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 78f70cc4ed9f74..b6d5465ab43a6c 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -21,14 +21,13 @@ concurrency: jobs: unit-js: - name: JavaScript + name: JavaScript (Node.js ${{ matrix.node }}) runs-on: ubuntu-latest if: ${{ github.repository == 'WordPress/gutenberg' || github.event_name == 'pull_request' }} - strategy: fail-fast: false matrix: - node: ['16'] + node: ['20', '21'] steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 @@ -40,7 +39,7 @@ jobs: with: node-version: ${{ matrix.node }} - - name: Npm build + - name: npm build # It's not necessary to run the full build, since Jest can interpret # source files with `babel-jest`. Some packages have their own custom # build tasks, however. These must be run. diff --git a/.nvmrc b/.nvmrc index b6a7d89c68e0ca..209e3ef4b6247c 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +20 diff --git a/docs/contributors/code/getting-started-with-code-contribution.md b/docs/contributors/code/getting-started-with-code-contribution.md index c3282c0f8003da..30a78037ab75e3 100644 --- a/docs/contributors/code/getting-started-with-code-contribution.md +++ b/docs/contributors/code/getting-started-with-code-contribution.md @@ -5,7 +5,7 @@ The following guide is for setting up your local environment to contribute to th ## Prerequisites - Node.js - Gutenberg is a JavaScript project and requires [Node.js](https://nodejs.org/). The project is built using Node.js v16, and npm v8. See the [LTS release schedule](https://github.com/nodejs/Release#release-schedule) for details. + Gutenberg is a JavaScript project that requires [Node.js](https://nodejs.org/). The project is currently built using Node.js v20 and npm v10. Though best efforts are made to always use the Active LTS version of Node.js, this will not always be the case. For more details, please refer to the [Node.js release schedule](https://github.com/nodejs/Release#release-schedule). We recommend using the [Node Version Manager](https://github.com/nvm-sh/nvm) (nvm) since it is the easiest way to install and manage node for macOS, Linux, and Windows 10 using WSL2. See [our Development Tools guide](/docs/getting-started/devenv/README.md#development-tools) or the Nodejs site for additional installation instructions. diff --git a/package-lock.json b/package-lock.json index 478570140e51aa..88404c01a18a91 100644 --- a/package-lock.json +++ b/package-lock.json @@ -249,8 +249,8 @@ "worker-farm": "1.7.0" }, "engines": { - "node": ">=16.0.0", - "npm": ">=8 <9" + "node": ">=20.10.0", + "npm": ">=10.2.3" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/package.json b/package.json index e1dd7fba270773..489a55cd904538 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,8 @@ "url": "https://github.com/WordPress/gutenberg/issues" }, "engines": { - "node": ">=16.0.0", - "npm": ">=8 <9" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "config": { "IS_GUTENBERG_PLUGIN": true diff --git a/platform-docs/docs/intro.md b/platform-docs/docs/intro.md index 79a142731891e5..b8961040715c7b 100644 --- a/platform-docs/docs/intro.md +++ b/platform-docs/docs/intro.md @@ -9,7 +9,7 @@ Let's discover how to use the **Gutenberg Block Editor** to build your own block ## What you'll need -- [Node.js](https://nodejs.org/en/download/) version 16.14 or above. +- [Node.js](https://nodejs.org/en/download/) version 20.10 or above. - We're going to be using "vite" to setup our single page application (SPA) that contains a block editor. You can use your own setup, and your own application for this. ## Preparing the SPA powered by Vite. @@ -59,7 +59,7 @@ registerCoreBlocks(); function Editor() { const [blocks, setBlocks] = useState([]); return ( - {/* + {/* The BlockEditorProvider is the wrapper of the block editor's state. All the UI elements of the block editor need to be rendered within this provider. */} @@ -82,4 +82,4 @@ const root = createRoot(document.getElementById("app")); root.render(<Editor />); ``` -That's it! You now have a very basic block editor with several block types included by default: paragraphs, headings, lists, quotes, images... \ No newline at end of file +That's it! You now have a very basic block editor with several block types included by default: paragraphs, headings, lists, quotes, images... From 588fc12533fbe465bf26db1d27f78f8c003e0cd4 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 21 Dec 2023 07:26:26 +1100 Subject: [PATCH 307/325] Global styles revisions: integrate style book (#56800) * first commit * Passing down an actions array to the editor site canvas - here we can add a style book toggle for revisions. * Add e2e test * If you open revisions then style book, the close button (in the canvas area) will exit style book, but the revisions panel remains open. If you open style book then open revisions, style book will remain toggled on. Render the style book window immediately to avoid a flash of the underlying editor * Updating e2e tests * remove alt color for style book * If you open both Style Book and Revisions, then close Revisions, the Style Book remains open Added e2e tests to reflect this --- .../editor-canvas-container/index.js | 7 ++- .../global-styles/screen-revisions/index.js | 29 +++++++-- .../src/components/global-styles/ui.js | 1 + .../src/components/revisions/index.js | 23 +++---- .../global-styles-sidebar.js | 62 ++++++++++++++----- .../src/components/style-book/index.js | 37 +++++++++-- .../user-global-styles-revisions.spec.js | 44 +++++++++++++ 7 files changed, 158 insertions(+), 45 deletions(-) diff --git a/packages/edit-site/src/components/editor-canvas-container/index.js b/packages/edit-site/src/components/editor-canvas-container/index.js index 89bee6012722f6..d08c30ac5646ab 100644 --- a/packages/edit-site/src/components/editor-canvas-container/index.js +++ b/packages/edit-site/src/components/editor-canvas-container/index.js @@ -34,6 +34,7 @@ function getEditorCanvasContainerTitle( view ) { case 'style-book': return __( 'Style Book' ); case 'global-styles-revisions': + case 'global-styles-revisions:style-book': return __( 'Global styles revisions' ); default: return ''; @@ -87,12 +88,12 @@ function EditorCanvasContainer( { ); function onCloseContainer() { - if ( typeof onClose === 'function' ) { - onClose(); - } setIsListViewOpened( showListViewByDefault ); setEditorCanvasContainerView( undefined ); setIsClosed( true ); + if ( typeof onClose === 'function' ) { + onClose(); + } } function closeOnEscape( event ) { diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index aa380c5a9fbd0b..048a1e664e1dfb 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -26,6 +26,7 @@ import SidebarFixedBottom from '../../sidebar-edit-mode/sidebar-fixed-bottom'; import { store as editSiteStore } from '../../../store'; import useGlobalStylesRevisions from './use-global-styles-revisions'; import RevisionsButtons from './revisions-buttons'; +import StyleBook from '../../style-book'; const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( blockEditorPrivateApis @@ -104,7 +105,10 @@ function ScreenRevisions() { }; useEffect( () => { - if ( editorCanvasContainerView !== 'global-styles-revisions' ) { + if ( + ! editorCanvasContainerView || + ! editorCanvasContainerView.startsWith( 'global-styles-revisions' ) + ) { goTo( '/' ); // Return to global styles main panel. setEditorCanvasContainerView( editorCanvasContainerView ); } @@ -156,13 +160,26 @@ function ScreenRevisions() { { isLoading && ( <Spinner className="edit-site-global-styles-screen-revisions__loading" /> ) } + { editorCanvasContainerView === + 'global-styles-revisions:style-book' ? ( + <StyleBook + userConfig={ currentlySelectedRevision } + isSelected={ () => {} } + onClose={ () => { + setEditorCanvasContainerView( + 'global-styles-revisions' + ); + } } + /> + ) : ( + <Revisions + blocks={ blocks } + userConfig={ currentlySelectedRevision } + closeButtonLabel={ __( 'Close revisions' ) } + /> + ) } { shouldShowRevisions && ( <> - <Revisions - blocks={ blocks } - userConfig={ currentlySelectedRevision } - onClose={ onCloseRevisions } - /> <div className="edit-site-global-styles-screen-revisions"> <RevisionsButtons onChange={ selectRevision } diff --git a/packages/edit-site/src/components/global-styles/ui.js b/packages/edit-site/src/components/global-styles/ui.js index 2a6af2e19229c6..3abd1c811f11eb 100644 --- a/packages/edit-site/src/components/global-styles/ui.js +++ b/packages/edit-site/src/components/global-styles/ui.js @@ -243,6 +243,7 @@ function GlobalStylesEditorCanvasContainerLink() { useEffect( () => { switch ( editorCanvasContainerView ) { case 'global-styles-revisions': + case 'global-styles-revisions:style-book': goTo( '/revisions' ); break; case 'global-styles-css': diff --git a/packages/edit-site/src/components/revisions/index.js b/packages/edit-site/src/components/revisions/index.js index 4757c9b27213ea..3d697880895797 100644 --- a/packages/edit-site/src/components/revisions/index.js +++ b/packages/edit-site/src/components/revisions/index.js @@ -11,8 +11,7 @@ import { __unstableIframe as Iframe, } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; -import { useMemo } from '@wordpress/element'; -import { store as coreStore } from '@wordpress/core-data'; +import { useContext, useMemo } from '@wordpress/element'; /** * Internal dependencies @@ -22,23 +21,18 @@ import { unlock } from '../../lock-unlock'; import { mergeBaseAndUserConfigs } from '../global-styles/global-styles-provider'; import EditorCanvasContainer from '../editor-canvas-container'; -const { ExperimentalBlockEditorProvider, useGlobalStylesOutputWithConfig } = - unlock( blockEditorPrivateApis ); +const { + ExperimentalBlockEditorProvider, + GlobalStylesContext, + useGlobalStylesOutputWithConfig, +} = unlock( blockEditorPrivateApis ); function isObjectEmpty( object ) { return ! object || Object.keys( object ).length === 0; } -function Revisions( { onClose, userConfig, blocks } ) { - const { baseConfig } = useSelect( - ( select ) => ( { - baseConfig: - select( - coreStore - ).__experimentalGetCurrentThemeBaseGlobalStyles(), - } ), - [] - ); +function Revisions( { userConfig, blocks } ) { + const { base: baseConfig } = useContext( GlobalStylesContext ); const mergedConfig = useMemo( () => { if ( ! isObjectEmpty( userConfig ) && ! isObjectEmpty( baseConfig ) ) { @@ -71,7 +65,6 @@ function Revisions( { onClose, userConfig, blocks } ) { return ( <EditorCanvasContainer title={ __( 'Revisions' ) } - onClose={ onClose } closeButtonLabel={ __( 'Close revisions' ) } enableResizing={ true } > diff --git a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js index ebb2e8a2a27718..20ae12923d2372 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/global-styles-sidebar.js @@ -33,6 +33,7 @@ export default function GlobalStylesSidebar() { showListViewByDefault, hasRevisions, isRevisionsOpened, + isRevisionsStyleBookOpened, } = useSelect( ( select ) => { const { getActiveComplementaryArea } = select( interfaceStore ); const { getEditorCanvasContainerView, getCanvasMode } = unlock( @@ -64,6 +65,8 @@ export default function GlobalStylesSidebar() { showListViewByDefault: _showListViewByDefault, hasRevisions: !! globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count, + isRevisionsStyleBookOpened: + 'global-styles-revisions:style-book' === canvasContainerView, isRevisionsOpened: 'global-styles-revisions' === canvasContainerView, }; @@ -80,16 +83,44 @@ export default function GlobalStylesSidebar() { const { setIsListViewOpened } = useDispatch( editorStore ); const { goTo } = useNavigator(); - const loadRevisions = () => { - setIsListViewOpened( false ); - if ( ! isRevisionsOpened ) { - goTo( '/revisions' ); - setEditorCanvasContainerView( 'global-styles-revisions' ); - } else { + const toggleRevisions = () => { + setIsListViewOpened( false ); + if ( isRevisionsStyleBookOpened ) { + goTo( '/' ); + setEditorCanvasContainerView( 'style-book' ); + return; + } + if ( isRevisionsOpened ) { goTo( '/' ); setEditorCanvasContainerView( undefined ); + return; } + goTo( '/revisions' ); + + if ( isStyleBookOpened ) { + setEditorCanvasContainerView( + 'global-styles-revisions:style-book' + ); + } else { + setEditorCanvasContainerView( 'global-styles-revisions' ); + } + }; + const toggleStyleBook = () => { + if ( isRevisionsOpened ) { + setEditorCanvasContainerView( + 'global-styles-revisions:style-book' + ); + return; + } + if ( isRevisionsStyleBookOpened ) { + setEditorCanvasContainerView( 'global-styles-revisions' ); + return; + } + setIsListViewOpened( isStyleBookOpened && showListViewByDefault ); + setEditorCanvasContainerView( + isStyleBookOpened ? undefined : 'style-book' + ); }; return ( @@ -113,25 +144,22 @@ export default function GlobalStylesSidebar() { <Button icon={ seen } label={ __( 'Style Book' ) } - isPressed={ isStyleBookOpened } + isPressed={ + isStyleBookOpened || isRevisionsStyleBookOpened + } disabled={ shouldClearCanvasContainerView } - onClick={ () => { - setIsListViewOpened( - isStyleBookOpened && showListViewByDefault - ); - setEditorCanvasContainerView( - isStyleBookOpened ? undefined : 'style-book' - ); - } } + onClick={ toggleStyleBook } /> </FlexItem> <FlexItem> <Button label={ __( 'Revisions' ) } icon={ backup } - onClick={ loadRevisions } + onClick={ toggleRevisions } disabled={ ! hasRevisions } - isPressed={ isRevisionsOpened } + isPressed={ + isRevisionsOpened || isRevisionsStyleBookOpened + } /> </FlexItem> <GlobalStylesMenuSlot /> diff --git a/packages/edit-site/src/components/style-book/index.js b/packages/edit-site/src/components/style-book/index.js index 19508f0a59f8ea..d3e0cb3531ae99 100644 --- a/packages/edit-site/src/components/style-book/index.js +++ b/packages/edit-site/src/components/style-book/index.js @@ -27,7 +27,7 @@ import { } from '@wordpress/block-editor'; import { useSelect } from '@wordpress/data'; import { useResizeObserver } from '@wordpress/compose'; -import { useMemo, useState, memo } from '@wordpress/element'; +import { useMemo, useState, memo, useContext } from '@wordpress/element'; import { ENTER, SPACE } from '@wordpress/keycodes'; /** @@ -35,10 +35,14 @@ import { ENTER, SPACE } from '@wordpress/keycodes'; */ import { unlock } from '../../lock-unlock'; import EditorCanvasContainer from '../editor-canvas-container'; +import { mergeBaseAndUserConfigs } from '../global-styles/global-styles-provider'; -const { ExperimentalBlockEditorProvider, useGlobalStyle } = unlock( - blockEditorPrivateApis -); +const { + ExperimentalBlockEditorProvider, + useGlobalStyle, + GlobalStylesContext, + useGlobalStylesOutputWithConfig, +} = unlock( blockEditorPrivateApis ); const { CompositeV2: Composite, @@ -118,6 +122,10 @@ const STYLE_BOOK_IFRAME_STYLES = ` } `; +function isObjectEmpty( object ) { + return ! object || Object.keys( object ).length === 0; +} + function getExamples() { // Use our own example for the Heading block so that we can show multiple // heading levels. @@ -174,7 +182,9 @@ function StyleBook( { onClick, onSelect, showCloseButton = true, + onClose, showTabs = true, + userConfig = {}, } ) { const [ resizeObserver, sizes ] = useResizeObserver(); const [ textColor ] = useGlobalStyle( 'color.text' ); @@ -195,18 +205,37 @@ function StyleBook( { } ) ), [ examples ] ); + const { base: baseConfig } = useContext( GlobalStylesContext ); + const mergedConfig = useMemo( () => { + if ( ! isObjectEmpty( userConfig ) && ! isObjectEmpty( baseConfig ) ) { + return mergeBaseAndUserConfigs( baseConfig, userConfig ); + } + return {}; + }, [ baseConfig, userConfig ] ); + + // Copied from packages/edit-site/src/components/revisions/index.js + // could we create a shared hook? const originalSettings = useSelect( ( select ) => select( blockEditorStore ).getSettings(), [] ); + const settings = useMemo( () => ( { ...originalSettings, __unstableIsPreviewMode: true } ), [ originalSettings ] ); + const [ globalStyles ] = useGlobalStylesOutputWithConfig( mergedConfig ); + + settings.styles = + ! isObjectEmpty( globalStyles ) && ! isObjectEmpty( userConfig ) + ? globalStyles + : settings.styles; + return ( <EditorCanvasContainer + onClose={ onClose } enableResizing={ enableResizing } closeButtonLabel={ showCloseButton ? __( 'Close Style Book' ) : null diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index fe917425f6e2cf..0602e3441103a0 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -145,6 +145,50 @@ test.describe( 'Global styles revisions', () => { page.getByLabel( 'Global styles revisions list' ) ).toBeVisible(); } ); + + test( 'should allow switching to style book view', async ( { + page, + editor, + userGlobalStylesRevisions, + } ) => { + await editor.canvas.locator( 'body' ).click(); + await userGlobalStylesRevisions.openStylesPanel(); + const revisionsButton = page.getByRole( 'button', { + name: 'Revisions', + } ); + const styleBookButton = page.getByRole( 'button', { + name: 'Style Book', + } ); + await revisionsButton.click(); + // We can see the Revisions list. + await expect( + page.getByLabel( 'Global styles revisions list' ) + ).toBeVisible(); + await expect( + page.locator( 'iframe[name="revisions"]' ) + ).toBeVisible(); + await expect( + page.locator( 'iframe[name="style-book-canvas"]' ) + ).toBeHidden(); + await styleBookButton.click(); + await expect( + page.locator( 'iframe[name="style-book-canvas"]' ) + ).toBeVisible(); + await expect( page.locator( 'iframe[name="revisions"]' ) ).toBeHidden(); + + // Deactivating revisions view while the style book is open should close revisions, + // but not the style book. + await revisionsButton.click(); + + // Style book is still visible but... + await expect( + page.locator( 'iframe[name="style-book-canvas"]' ) + ).toBeVisible(); + // The Revisions list is hidden. + await expect( + page.getByLabel( 'Global styles revisions list' ) + ).toBeHidden(); + } ); } ); class UserGlobalStylesRevisions { From a48c4a3a264b4df9d5eeafc145d14b93a0dba7b2 Mon Sep 17 00:00:00 2001 From: Nick Diego <nick.diego@automattic.com> Date: Wed, 20 Dec 2023 14:55:49 -0600 Subject: [PATCH 308/325] Fix incorrect links in ToggleGroupControl docs. (#57236) --- .../toggle-group-control-option-base/README.md | 2 +- .../toggle-group-control-option-icon/README.md | 2 +- .../toggle-group-control/toggle-group-control-option/README.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/README.md b/packages/components/src/toggle-group-control/toggle-group-control-option-base/README.md index a3a9a94807e93d..e673a1ed45bbf6 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/README.md +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/README.md @@ -4,7 +4,7 @@ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. </div> -`ToggleGroupControlOptionBase` is a form component and is meant to be used as an internal, generic component for any children of [`ToggleGroupControl`](<(/packages/components/src/toggle-group-control/toggle-group-control/README.md)>). +`ToggleGroupControlOptionBase` is a form component and is meant to be used as an internal, generic component for any children of [`ToggleGroupControl`](/packages/components/src/toggle-group-control/toggle-group-control/README.md). ## Props diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-icon/README.md b/packages/components/src/toggle-group-control/toggle-group-control-option-icon/README.md index d82700ae21b1f5..8b96470e21301e 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-icon/README.md +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-icon/README.md @@ -4,7 +4,7 @@ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. </div> -`ToggleGroupControlOptionIcon` is a form component which is meant to be used as a child of [`ToggleGroupControl`] and displays an icon(<(/packages/components/src/toggle-group-control/toggle-group-control/README.md)>). +`ToggleGroupControlOptionIcon` is a form component which is meant to be used as a child of [`ToggleGroupControl`](/packages/components/src/toggle-group-control/toggle-group-control/README.md) and displays an icon. ## Usage diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option/README.md b/packages/components/src/toggle-group-control/toggle-group-control-option/README.md index 2de2e665c3f5b2..7f9f4d32f29fc3 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option/README.md +++ b/packages/components/src/toggle-group-control/toggle-group-control-option/README.md @@ -4,7 +4,7 @@ This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. </div> -`ToggleGroupControlOption` is a form component and is meant to be used as a child of [`ToggleGroupControl`]((/packages/components/src/toggle-group-control/toggle-group-control/README.md)). +`ToggleGroupControlOption` is a form component and is meant to be used as a child of [`ToggleGroupControl`](/packages/components/src/toggle-group-control/toggle-group-control/README.md). ## Usage From 0167c532094b9f5f14c7f1278c6b5218874ce4b9 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 21 Dec 2023 11:44:07 +1100 Subject: [PATCH 309/325] Global styles revisions: add pagination (#56799) * first commit * Testing out lockin pagination to the bottom and giving every item an Apply button * Remove count from heading - it's in the pagination Fudge the button * Making the pagination sticky Creating an internal state for revisions to avoid refreshing * Moving pagination and Apply button to a sticky bottom footer * Styling pagination buttons Shifting "Apply" button to the single item so that we don't have to deal with it being next to the pagination row. * revision item is flex * Restyle pagination Restyle apply button Reinstate fixed bottom slot * refactored e2e tests to have an open revision method added pagination tests * Added unit tests * Display text if a revision can't be applied. * Fix Safari rendering. * Prevent x scrolling on Safari and Firefox. --- .../global-styles/screen-revisions/index.js | 203 +++++++++--------- .../screen-revisions/revisions-buttons.js | 35 ++- .../global-styles/screen-revisions/style.scss | 58 ++++- .../test/use-global-styles-revisions.js | 67 ++++++ .../use-global-styles-revisions.js | 119 ++++++---- .../components/page-patterns/patterns-list.js | 3 +- .../src/components/page-patterns/style.scss | 49 ++--- .../pagination.js => pagination/index.js} | 31 ++- .../src/components/pagination/style.scss | 5 + .../sidebar-edit-mode/sidebar-fixed-bottom.js | 14 +- .../components/sidebar-edit-mode/style.scss | 6 +- packages/edit-site/src/style.scss | 1 + .../user-global-styles-revisions.spec.js | 68 ++++-- 13 files changed, 437 insertions(+), 222 deletions(-) rename packages/edit-site/src/components/{page-patterns/pagination.js => pagination/index.js} (68%) create mode 100644 packages/edit-site/src/components/pagination/style.scss diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 048a1e664e1dfb..0f52cbda221ee9 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -3,13 +3,11 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { - Button, __experimentalUseNavigator as useNavigator, __experimentalConfirmDialog as ConfirmDialog, Spinner, } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; -import { store as coreStore } from '@wordpress/core-data'; import { useContext, useState, useEffect } from '@wordpress/element'; import { privateApis as blockEditorPrivateApis, @@ -27,47 +25,39 @@ import { store as editSiteStore } from '../../../store'; import useGlobalStylesRevisions from './use-global-styles-revisions'; import RevisionsButtons from './revisions-buttons'; import StyleBook from '../../style-book'; +import Pagination from '../../pagination'; const { GlobalStylesContext, areGlobalStyleConfigsEqual } = unlock( blockEditorPrivateApis ); +const PAGE_SIZE = 10; + function ScreenRevisions() { const { goTo } = useNavigator(); const { user: currentEditorGlobalStyles, setUserConfig } = useContext( GlobalStylesContext ); - const { blocks, editorCanvasContainerView, revisionsCount } = useSelect( - ( select ) => { - const { - getEntityRecord, - __experimentalGetCurrentGlobalStylesId, - __experimentalGetDirtyEntityRecords, - } = select( coreStore ); - const isDirty = __experimentalGetDirtyEntityRecords().length > 0; - const globalStylesId = __experimentalGetCurrentGlobalStylesId(); - const globalStyles = globalStylesId - ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) - : undefined; - let _revisionsCount = - globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count || 0; - // one for the reset item. - _revisionsCount++; - // one for any dirty changes (unsaved). - if ( isDirty ) { - _revisionsCount++; - } - return { - editorCanvasContainerView: unlock( - select( editSiteStore ) - ).getEditorCanvasContainerView(), - blocks: select( blockEditorStore ).getBlocks(), - revisionsCount: _revisionsCount, - }; - }, + const { blocks, editorCanvasContainerView } = useSelect( + ( select ) => ( { + editorCanvasContainerView: unlock( + select( editSiteStore ) + ).getEditorCanvasContainerView(), + blocks: select( blockEditorStore ).getBlocks(), + } ), [] ); - const { revisions, isLoading, hasUnsavedChanges } = - useGlobalStylesRevisions(); + const [ currentPage, setCurrentPage ] = useState( 1 ); + const [ currentRevisions, setCurrentRevisions ] = useState( [] ); + const { revisions, isLoading, hasUnsavedChanges, revisionsCount } = + useGlobalStylesRevisions( { + query: { + per_page: PAGE_SIZE, + page: currentPage, + }, + } ); + + const numPages = Math.ceil( revisionsCount / PAGE_SIZE ); + const [ currentlySelectedRevision, setCurrentlySelectedRevision ] = useState( currentEditorGlobalStyles ); const [ @@ -114,6 +104,12 @@ function ScreenRevisions() { } }, [ editorCanvasContainerView ] ); + useEffect( () => { + if ( ! isLoading && revisions.length ) { + setCurrentRevisions( revisions ); + } + }, [ revisions, isLoading ] ); + const firstRevision = revisions[ 0 ]; const currentlySelectedRevisionId = currentlySelectedRevision?.id; const shouldSelectFirstItem = @@ -141,8 +137,10 @@ function ScreenRevisions() { // Only display load button if there is a revision to load, // and it is different from the current editor styles. const isLoadButtonEnabled = - !! currentlySelectedRevisionId && ! selectedRevisionMatchesEditorStyles; - const shouldShowRevisions = ! isLoading && revisions.length; + !! currentlySelectedRevisionId && + currentlySelectedRevisionId !== 'unsaved' && + ! selectedRevisionMatchesEditorStyles; + const hasRevisions = !! currentRevisions.length; return ( <> @@ -157,83 +155,74 @@ function ScreenRevisions() { ) } onBack={ onCloseRevisions } /> - { isLoading && ( + { ! hasRevisions && ( <Spinner className="edit-site-global-styles-screen-revisions__loading" /> ) } - { editorCanvasContainerView === - 'global-styles-revisions:style-book' ? ( - <StyleBook - userConfig={ currentlySelectedRevision } - isSelected={ () => {} } - onClose={ () => { - setEditorCanvasContainerView( - 'global-styles-revisions' - ); - } } - /> - ) : ( - <Revisions - blocks={ blocks } - userConfig={ currentlySelectedRevision } - closeButtonLabel={ __( 'Close revisions' ) } - /> - ) } - { shouldShowRevisions && ( - <> - <div className="edit-site-global-styles-screen-revisions"> - <RevisionsButtons - onChange={ selectRevision } - selectedRevisionId={ currentlySelectedRevisionId } - userRevisions={ revisions } - canApplyRevision={ isLoadButtonEnabled } + <> + { hasRevisions && + ( editorCanvasContainerView === + 'global-styles-revisions:style-book' ? ( + <StyleBook + userConfig={ currentlySelectedRevision } + isSelected={ () => {} } + onClose={ () => { + setEditorCanvasContainerView( + 'global-styles-revisions' + ); + } } /> - { isLoadButtonEnabled && ( - <SidebarFixedBottom> - <Button - variant="primary" - className="edit-site-global-styles-screen-revisions__button" - disabled={ - ! currentlySelectedRevisionId || - currentlySelectedRevisionId === - 'unsaved' - } - onClick={ () => { - if ( hasUnsavedChanges ) { - setIsLoadingRevisionWithUnsavedChanges( - true - ); - } else { - restoreRevision( - currentlySelectedRevision - ); - } - } } - > - { currentlySelectedRevisionId === 'parent' - ? __( 'Reset to defaults' ) - : __( 'Apply' ) } - </Button> - </SidebarFixedBottom> - ) } - </div> - { isLoadingRevisionWithUnsavedChanges && ( - <ConfirmDialog - isOpen={ isLoadingRevisionWithUnsavedChanges } - confirmButtonText={ __( 'Apply' ) } - onConfirm={ () => - restoreRevision( currentlySelectedRevision ) - } - onCancel={ () => - setIsLoadingRevisionWithUnsavedChanges( false ) - } - > - { __( - 'Any unsaved changes will be lost when you apply this revision.' + ) : ( + <Revisions + blocks={ blocks } + userConfig={ currentlySelectedRevision } + closeButtonLabel={ __( 'Close revisions' ) } + /> + ) ) } + <div className="edit-site-global-styles-screen-revisions"> + <RevisionsButtons + onChange={ selectRevision } + selectedRevisionId={ currentlySelectedRevisionId } + userRevisions={ currentRevisions } + canApplyRevision={ isLoadButtonEnabled } + onApplyRevision={ () => + hasUnsavedChanges + ? setIsLoadingRevisionWithUnsavedChanges( true ) + : restoreRevision( currentlySelectedRevision ) + } + /> + </div> + { numPages > 1 && ( + <SidebarFixedBottom className="edit-site-global-styles-screen-revisions__footer"> + <Pagination + className="edit-site-global-styles-screen-revisions__pagination" + currentPage={ currentPage } + numPages={ numPages } + changePage={ setCurrentPage } + totalItems={ revisionsCount } + disabled={ isLoading } + label={ __( + 'Global Styles pagination navigation' ) } - </ConfirmDialog> - ) } - </> - ) } + /> + </SidebarFixedBottom> + ) } + { isLoadingRevisionWithUnsavedChanges && ( + <ConfirmDialog + isOpen={ isLoadingRevisionWithUnsavedChanges } + confirmButtonText={ __( 'Apply' ) } + onConfirm={ () => + restoreRevision( currentlySelectedRevision ) + } + onCancel={ () => + setIsLoadingRevisionWithUnsavedChanges( false ) + } + > + { __( + 'Any unsaved changes will be lost when you apply this revision.' + ) } + </ConfirmDialog> + ) } + </> </> ); } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js index 9eff635bbc4a6a..db7a6877fef10e 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/revisions-buttons.js @@ -115,6 +115,7 @@ function RevisionsButtons( { selectedRevisionId, onChange, canApplyRevision, + onApplyRevision, } ) { const { currentThemeName, currentUser } = useSelect( ( select ) => { const { getCurrentTheme, getCurrentUser } = select( coreStore ); @@ -178,6 +179,7 @@ function RevisionsButtons( { } ) } key={ id } + aria-current={ isSelected } > <Button className="edit-site-global-styles-screen-revisions__revision-button" @@ -208,6 +210,13 @@ function RevisionsButtons( { { displayDate } </time> ) } + <span className="edit-site-global-styles-screen-revisions__meta"> + <img + alt={ authorDisplayName } + src={ authorAvatar } + /> + { authorDisplayName } + </span> { isSelected && ( <ChangesSummary blockNames={ blockNames } @@ -219,16 +228,28 @@ function RevisionsButtons( { } /> ) } - <span className="edit-site-global-styles-screen-revisions__meta"> - <img - alt={ authorDisplayName } - src={ authorAvatar } - /> - { authorDisplayName } - </span> </span> ) } </Button> + { isSelected && + ( areStylesEqual ? ( + <p className="edit-site-global-styles-screen-revisions__applied-text"> + { __( + 'These styles are already applied to your site.' + ) } + </p> + ) : ( + <Button + disabled={ areStylesEqual } + variant="primary" + className="edit-site-global-styles-screen-revisions__apply-button" + onClick={ onApplyRevision } + > + { isReset + ? __( 'Reset to defaults' ) + : __( 'Apply' ) } + </Button> + ) ) } </li> ); } ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index a127142ac60edb..97d27282f61afa 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -1,6 +1,7 @@ .edit-site-global-styles-screen-revisions { - margin: $grid-unit-20; + margin: 0 $grid-unit-20 $grid-unit-20 $grid-unit-20; + padding-bottom: $grid-unit-60; } .edit-site-global-styles-screen-revisions__revisions-list { @@ -14,6 +15,8 @@ .edit-site-global-styles-screen-revisions__revision-item { position: relative; cursor: pointer; + display: flex; + flex-direction: column; &:hover { background: rgba(var(--wp-admin-theme-color--rgb), 0.04); @@ -93,9 +96,17 @@ } } -.edit-site-global-styles-screen-revisions__button { - justify-content: center; - width: 100%; +.edit-site-global-styles-screen-revisions__apply-button.is-primary, +.edit-site-global-styles-screen-revisions__applied-text { + align-self: flex-start; + // Left margin = left padding of .edit-site-global-styles-screen-revisions__revision-button. + margin: 0 $grid-unit-15 $grid-unit-15 $grid-unit-50; +} + +.edit-site-global-styles-screen-revisions__applied-text { + color: $gray-600; + font-size: 12px; + font-style: italic; } .edit-site-global-styles-screen-revisions__description { @@ -116,8 +127,9 @@ display: flex; justify-content: start; width: 100%; - align-items: center; + align-items: flex-start; font-size: 12px; + text-align: left; img { width: $grid-unit-20; height: $grid-unit-20; @@ -137,3 +149,39 @@ line-height: $default-line-height; } +.edit-site-global-styles-screen-revisions__pagination.edit-site-global-styles-screen-revisions__pagination { + justify-content: space-between; + gap: 2px; + .edit-site-pagination__total { + position: absolute; + left: -1000px; + height: 1px; + margin: -1px; + overflow: hidden; + } + + .components-text { + font-size: 12px; + // `will-change` is required because something about flex props prevents + // Safari from rendering the page / total text. + will-change: opacity; + } + .components-button.is-tertiary { + width: $button-size-small; + height: $button-size-small; + font-size: 28px; + font-weight: 200; + color: $gray-900; + margin-bottom: $grid-unit-05; + } + .components-button.is-tertiary:disabled { + color: $gray-600; + } + .components-button.is-tertiary:hover { + background: transparent; + } +} + +.edit-site-global-styles-screen-revisions__footer { + height: $grid-unit-60; +} diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js index 4b03e38fe34d27..0262086e58b2ad 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/test/use-global-styles-revisions.js @@ -50,6 +50,7 @@ describe( 'useGlobalStylesRevisions', () => { }, ], isLoadingGlobalStylesRevisions: false, + revisionsCount: 1, }; it( 'returns loaded revisions with no unsaved changes', () => { @@ -158,4 +159,70 @@ describe( 'useGlobalStylesRevisions', () => { expect( hasUnsavedChanges ).toBe( false ); expect( revisions ).toEqual( [] ); } ); + + it( 'should prepend unsaved changes item and append reset item to paginated results', () => { + useSelect.mockImplementation( () => ( { + ...selectValue, + revisionsCount: 2, + isDirty: true, + } ) ); + + // Prepend unsaved changes item to paginated results. + const { result: resultPrepend } = renderHook( () => + useGlobalStylesRevisions( { + query: { + per_page: 1, + page: 1, + }, + } ) + ); + expect( resultPrepend.current.revisions ).toEqual( [ + { + author: { + avatar_urls: {}, + name: 'fred', + }, + id: 'unsaved', + modified: resultPrepend.current.revisions[ 0 ].modified, + settings: 'cake', + styles: 'ice-cream', + }, + { + author: { + id: 4, + name: 'sam', + }, + id: 1, + isLatest: true, + settings: {}, + styles: {}, + }, + ] ); + + // Append reset item to paginated results. + const { result: resultAppend } = renderHook( () => + useGlobalStylesRevisions( { + query: { + per_page: 1, + page: 2, + }, + } ) + ); + expect( resultAppend.current.revisions ).toEqual( [ + { + author: { + id: 4, + name: 'sam', + }, + id: 1, + settings: {}, + styles: {}, + }, + { + id: 'parent', + settings: {}, + styles: {}, + }, + ] ); + } ); } ); diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js index bacc79a97cb6de..2cd0b3b7bdea60 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/use-global-styles-revisions.js @@ -17,62 +17,77 @@ const SITE_EDITOR_AUTHORS_QUERY = { context: 'view', capabilities: [ 'edit_theme_options' ], }; +const DEFAULT_QUERY = { per_page: 100, page: 1 }; const EMPTY_ARRAY = []; const { GlobalStylesContext } = unlock( blockEditorPrivateApis ); -export default function useGlobalStylesRevisions() { +export default function useGlobalStylesRevisions( { query } = {} ) { const { user: userConfig } = useContext( GlobalStylesContext ); + const _query = { ...DEFAULT_QUERY, ...query }; const { authors, currentUser, isDirty, revisions, isLoadingGlobalStylesRevisions, - } = useSelect( ( select ) => { - const { - __experimentalGetDirtyEntityRecords, - getCurrentUser, - getUsers, - getRevisions, - __experimentalGetCurrentGlobalStylesId, - isResolving, - } = select( coreStore ); - const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); - const _currentUser = getCurrentUser(); - const _isDirty = dirtyEntityRecords.length > 0; - const query = { - per_page: 100, - }; - const globalStylesId = __experimentalGetCurrentGlobalStylesId(); - const globalStylesRevisions = - getRevisions( 'root', 'globalStyles', globalStylesId, query ) || - EMPTY_ARRAY; - const _authors = getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY; - const _isResolving = isResolving( 'getRevisions', [ - 'root', - 'globalStyles', - globalStylesId, - query, - ] ); - return { - authors: _authors, - currentUser: _currentUser, - isDirty: _isDirty, - revisions: globalStylesRevisions, - isLoadingGlobalStylesRevisions: _isResolving, - }; - }, [] ); + revisionsCount, + } = useSelect( + ( select ) => { + const { + __experimentalGetDirtyEntityRecords, + getCurrentUser, + getUsers, + getRevisions, + __experimentalGetCurrentGlobalStylesId, + getEntityRecord, + isResolving, + } = select( coreStore ); + const dirtyEntityRecords = __experimentalGetDirtyEntityRecords(); + const _currentUser = getCurrentUser(); + const _isDirty = dirtyEntityRecords.length > 0; + const globalStylesId = __experimentalGetCurrentGlobalStylesId(); + const globalStyles = globalStylesId + ? getEntityRecord( 'root', 'globalStyles', globalStylesId ) + : undefined; + const _revisionsCount = + globalStyles?._links?.[ 'version-history' ]?.[ 0 ]?.count ?? 0; + const globalStylesRevisions = + getRevisions( + 'root', + 'globalStyles', + globalStylesId, + _query + ) || EMPTY_ARRAY; + const _authors = + getUsers( SITE_EDITOR_AUTHORS_QUERY ) || EMPTY_ARRAY; + const _isResolving = isResolving( 'getRevisions', [ + 'root', + 'globalStyles', + globalStylesId, + _query, + ] ); + return { + authors: _authors, + currentUser: _currentUser, + isDirty: _isDirty, + revisions: globalStylesRevisions, + isLoadingGlobalStylesRevisions: _isResolving, + revisionsCount: _revisionsCount, + }; + }, + [ query ] + ); return useMemo( () => { - let _modifiedRevisions = []; if ( ! authors.length || isLoadingGlobalStylesRevisions ) { return { - revisions: _modifiedRevisions, + revisions: EMPTY_ARRAY, hasUnsavedChanges: isDirty, isLoading: true, + revisionsCount, }; } // Adds author details to each revision. - _modifiedRevisions = revisions.map( ( revision ) => { + const _modifiedRevisions = revisions.map( ( revision ) => { return { ...revision, author: authors.find( @@ -81,9 +96,14 @@ export default function useGlobalStylesRevisions() { }; } ); - if ( _modifiedRevisions.length ) { + const fetchedRevisionsCount = revisions.length; + + if ( fetchedRevisionsCount ) { // Flags the most current saved revision. - if ( _modifiedRevisions[ 0 ].id !== 'unsaved' ) { + if ( + _modifiedRevisions[ 0 ].id !== 'unsaved' && + _query.page === 1 + ) { _modifiedRevisions[ 0 ].isLatest = true; } @@ -92,7 +112,8 @@ export default function useGlobalStylesRevisions() { isDirty && userConfig && Object.keys( userConfig ).length > 0 && - currentUser + currentUser && + _query.page === 1 ) { const unsavedRevision = { id: 'unsaved', @@ -108,17 +129,23 @@ export default function useGlobalStylesRevisions() { _modifiedRevisions.unshift( unsavedRevision ); } - _modifiedRevisions.push( { - id: 'parent', - styles: {}, - settings: {}, - } ); + if ( + _query.page === Math.ceil( revisionsCount / _query.per_page ) + ) { + // Adds an item for the default theme styles. + _modifiedRevisions.push( { + id: 'parent', + styles: {}, + settings: {}, + } ); + } } return { revisions: _modifiedRevisions, hasUnsavedChanges: isDirty, isLoading: false, + revisionsCount, }; }, [ isDirty, diff --git a/packages/edit-site/src/components/page-patterns/patterns-list.js b/packages/edit-site/src/components/page-patterns/patterns-list.js index 6015fbbf4caf34..91ca083607674d 100644 --- a/packages/edit-site/src/components/page-patterns/patterns-list.js +++ b/packages/edit-site/src/components/page-patterns/patterns-list.js @@ -31,7 +31,7 @@ import usePatterns from './use-patterns'; import SidebarButton from '../sidebar-button'; import { unlock } from '../../lock-unlock'; import { PATTERN_SYNC_TYPES, PATTERN_TYPES } from '../../utils/constants'; -import Pagination from './pagination'; +import Pagination from '../pagination'; const { useLocation, useHistory } = unlock( routerPrivateApis ); @@ -217,6 +217,7 @@ export default function PatternsList( { categoryId, type } ) { </VStack> { numPages > 1 && ( <Pagination + className="edit-site-patterns__pagination" currentPage={ currentPage } numPages={ numPages } changePage={ changePage } diff --git a/packages/edit-site/src/components/page-patterns/style.scss b/packages/edit-site/src/components/page-patterns/style.scss index d02b82ff2560d0..8995a0d25c96cf 100644 --- a/packages/edit-site/src/components/page-patterns/style.scss +++ b/packages/edit-site/src/components/page-patterns/style.scss @@ -67,32 +67,6 @@ background: $gray-700; color: $gray-100; } - - .edit-site-patterns__grid-pagination { - border-top: 1px solid $gray-800; - background: $gray-900; - padding: $grid-unit-30 $grid-unit-40; - position: sticky; - bottom: 0; - z-index: z-index(".edit-site-patterns__grid-pagination"); - - .components-button.is-tertiary { - width: $button-size-compact; - height: $button-size-compact; - color: $gray-100; - background-color: $gray-800; - justify-content: center; - - &:disabled { - color: $gray-600; - background: none; - } - - &:hover:not(:disabled) { - background-color: $gray-700; - } - } - } } .edit-site-patterns__header { @@ -226,3 +200,26 @@ .edit-site-patterns__delete-modal { width: $modal-width-small; } + +.edit-site-patterns__pagination { + padding: $grid-unit-30 $grid-unit-40; + border-top: 1px solid $gray-800; + background: $gray-900; + position: sticky; + bottom: 0; + color: $gray-100; + z-index: z-index(".edit-site-patterns__grid-pagination"); + .components-button.is-tertiary { + background-color: $gray-800; + color: $gray-100; + + &:disabled { + color: $gray-600; + background: none; + } + + &:hover:not(:disabled) { + background-color: $gray-700; + } + } +} diff --git a/packages/edit-site/src/components/page-patterns/pagination.js b/packages/edit-site/src/components/pagination/index.js similarity index 68% rename from packages/edit-site/src/components/page-patterns/pagination.js rename to packages/edit-site/src/components/pagination/index.js index 702e24cd31b7f0..fa347d2caaad34 100644 --- a/packages/edit-site/src/components/page-patterns/pagination.js +++ b/packages/edit-site/src/components/pagination/index.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -13,15 +18,21 @@ export default function Pagination( { numPages, changePage, totalItems, + className, + disabled = false, + buttonVariant = 'tertiary', + label = __( 'Pagination Navigation' ), } ) { return ( <HStack expanded={ false } + as="nav" + aria-label={ label } spacing={ 3 } justify="flex-start" - className="edit-site-patterns__grid-pagination" + className={ classnames( 'edit-site-pagination', className ) } > - <Text variant="muted"> + <Text variant="muted" className="edit-site-pagination__total"> { // translators: %s: Total number of patterns. sprintf( @@ -33,17 +44,17 @@ export default function Pagination( { </Text> <HStack expanded={ false } spacing={ 1 }> <Button - variant="tertiary" + variant={ buttonVariant } onClick={ () => changePage( 1 ) } - disabled={ currentPage === 1 } + disabled={ disabled || currentPage === 1 } aria-label={ __( 'First page' ) } > « </Button> <Button - variant="tertiary" + variant={ buttonVariant } onClick={ () => changePage( currentPage - 1 ) } - disabled={ currentPage === 1 } + disabled={ disabled || currentPage === 1 } aria-label={ __( 'Previous page' ) } > ‹ @@ -59,17 +70,17 @@ export default function Pagination( { </Text> <HStack expanded={ false } spacing={ 1 }> <Button - variant="tertiary" + variant={ buttonVariant } onClick={ () => changePage( currentPage + 1 ) } - disabled={ currentPage === numPages } + disabled={ disabled || currentPage === numPages } aria-label={ __( 'Next page' ) } > › </Button> <Button - variant="tertiary" + variant={ buttonVariant } onClick={ () => changePage( numPages ) } - disabled={ currentPage === numPages } + disabled={ disabled || currentPage === numPages } aria-label={ __( 'Last page' ) } > » diff --git a/packages/edit-site/src/components/pagination/style.scss b/packages/edit-site/src/components/pagination/style.scss new file mode 100644 index 00000000000000..c74d54a70930f3 --- /dev/null +++ b/packages/edit-site/src/components/pagination/style.scss @@ -0,0 +1,5 @@ +.edit-site-pagination .components-button.is-tertiary { + width: $button-size-compact; + height: $button-size-compact; + justify-content: center; +} diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js index 56e205bd330286..46f25bf7a19146 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + /** * WordPress dependencies */ @@ -13,10 +18,15 @@ const SIDEBAR_FIXED_BOTTOM_SLOT_FILL_NAME = 'SidebarFixedBottom'; const { Slot: SidebarFixedBottomSlot, Fill: SidebarFixedBottomFill } = createPrivateSlotFill( SIDEBAR_FIXED_BOTTOM_SLOT_FILL_NAME ); -export default function SidebarFixedBottom( { children } ) { +export default function SidebarFixedBottom( { className, children } ) { return ( <SidebarFixedBottomFill> - <div className="edit-site-sidebar-fixed-bottom-slot"> + <div + className={ classnames( + 'edit-site-sidebar-fixed-bottom-slot', + className + ) } + > { children } </div> </SidebarFixedBottomFill> diff --git a/packages/edit-site/src/components/sidebar-edit-mode/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/style.scss index 544c38e0ef07b1..1bfb87aafa15c5 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/style.scss @@ -101,11 +101,11 @@ } .edit-site-sidebar-fixed-bottom-slot { - position: sticky; + position: absolute; + min-width: 100%; bottom: 0; background: $white; - display: flex; - padding: $grid-unit-20; + padding: $grid-unit-15; border-top: $border-width solid $gray-300; box-sizing: content-box; } diff --git a/packages/edit-site/src/style.scss b/packages/edit-site/src/style.scss index 9e9cdbc6684b81..53c4f672ed8c23 100644 --- a/packages/edit-site/src/style.scss +++ b/packages/edit-site/src/style.scss @@ -48,6 +48,7 @@ @import "./components/resizable-frame/style.scss"; @import "./hooks/push-changes-to-global-styles/style.scss"; @import "./components/global-styles/font-library-modal/style.scss"; +@import "./components/pagination/style.scss"; body.js #wpadminbar { display: none; diff --git a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js index 0602e3441103a0..ff62da26bcfacf 100644 --- a/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js +++ b/test/e2e/specs/site-editor/user-global-styles-revisions.spec.js @@ -4,18 +4,25 @@ const { test, expect } = require( '@wordpress/e2e-test-utils-playwright' ); test.use( { - userGlobalStylesRevisions: async ( { page, requestUtils }, use ) => { - await use( new UserGlobalStylesRevisions( { page, requestUtils } ) ); + userGlobalStylesRevisions: async ( + { editor, page, requestUtils }, + use + ) => { + await use( + new UserGlobalStylesRevisions( { editor, page, requestUtils } ) + ); }, } ); test.describe( 'Global styles revisions', () => { + let stylesPostId; test.beforeAll( async ( { requestUtils } ) => { await Promise.all( [ requestUtils.activateTheme( 'emptytheme' ), requestUtils.deleteAllTemplates( 'wp_template' ), requestUtils.deleteAllTemplates( 'wp_template_part' ), ] ); + stylesPostId = await requestUtils.getCurrentThemeGlobalStylesPostId(); } ); test.afterAll( async ( { requestUtils } ) => { @@ -34,20 +41,12 @@ test.describe( 'Global styles revisions', () => { await editor.canvas.locator( 'body' ).click(); const currentRevisions = await userGlobalStylesRevisions.getGlobalStylesRevisions(); + // Create a revision: change a style and save it. + await userGlobalStylesRevisions.saveRevision( stylesPostId, { + color: { background: 'blue' }, + } ); await userGlobalStylesRevisions.openStylesPanel(); - // Change a style and save it. - await page.getByRole( 'button', { name: 'Colors styles' } ).click(); - - await page - .getByRole( 'button', { name: 'Color Background styles' } ) - .click(); - await page - .getByRole( 'option', { name: 'Color: Cyan bluish gray' } ) - .click( { force: true } ); - - await editor.saveSiteEditorEntities(); - // Now there should be enough revisions to show the revisions UI. await page.getByRole( 'button', { name: 'Revisions' } ).click(); @@ -189,11 +188,34 @@ test.describe( 'Global styles revisions', () => { page.getByLabel( 'Global styles revisions list' ) ).toBeHidden(); } ); + + test( 'should paginate', async ( { + page, + editor, + userGlobalStylesRevisions, + } ) => { + await editor.canvas.locator( 'body' ).click(); + // Create > 10 revisions to display pagination navigation component. + for ( let i = 9; i < 21; i++ ) { + await userGlobalStylesRevisions.saveRevision( stylesPostId, { + typography: { fontSize: `${ i }px` }, + } ); + } + await userGlobalStylesRevisions.openStylesPanel(); + await page.getByRole( 'button', { name: 'Revisions' } ).click(); + const pagination = page.getByLabel( + 'Global Styles pagination navigation' + ); + await expect( pagination ).toContainText( '1 of 2' ); + await pagination.getByRole( 'button', { name: 'Next page' } ).click(); + await expect( pagination ).toContainText( '2 of 2' ); + } ); } ); class UserGlobalStylesRevisions { - constructor( { page, requestUtils } ) { + constructor( { editor, page, requestUtils } ) { this.page = page; + this.editor = editor; this.requestUtils = requestUtils; } @@ -214,4 +236,20 @@ class UserGlobalStylesRevisions { .getByRole( 'button', { name: 'Styles' } ) .click(); } + + async saveRevision( stylesPostId, styles = {}, settings = {} ) { + await this.page.evaluate( + async ( [ _stylesPostId, _styles, _settings ] ) => { + window.wp.data + .dispatch( 'core' ) + .editEntityRecord( 'root', 'globalStyles', _stylesPostId, { + id: _stylesPostId, + settings: _settings, + styles: _styles, + } ); + }, + [ stylesPostId, styles, settings ] + ); + await this.editor.saveSiteEditorEntities(); + } } From 891c4cf2c90bfcfe5197a4565feba7528bf3cd6a Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Wed, 20 Dec 2023 22:00:41 -0500 Subject: [PATCH 310/325] `FormTokenField`: handle `disabled` prop on internal `Button` (#57187) * pass disabled prop to `Button` * changelog --- packages/components/CHANGELOG.md | 1 + packages/components/src/form-token-field/token.tsx | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 19feebd911b456..402d78fc354d5d 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -7,6 +7,7 @@ - `FontSizePicker`: Use Button API for keeping focus on reset ([#57221](https://github.com/WordPress/gutenberg/pull/57221)). - `FontSizePicker`: Fix Reset button focus loss ([#57196](https://github.com/WordPress/gutenberg/pull/57196)). - `PaletteEdit`: Consider digits when generating kebab-cased slug ([#56713](https://github.com/WordPress/gutenberg/pull/56713)). +- `FormTokenField`: Prevent focus being passed to internal buttons when component is disabled ([#57187](https://github.com/WordPress/gutenberg/pull/57187)). - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). - `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). - `Snackbar`: Remove erroneous `__unstableHTML` prop from TypeScript definitions ([#57218](https://github.com/WordPress/gutenberg/pull/57218)). diff --git a/packages/components/src/form-token-field/token.tsx b/packages/components/src/form-token-field/token.tsx index e5633aa3b75cf0..745a6d485df9b8 100644 --- a/packages/components/src/form-token-field/token.tsx +++ b/packages/components/src/form-token-field/token.tsx @@ -74,6 +74,7 @@ export default function Token( { className="components-form-token-field__remove-token" icon={ closeSmall } onClick={ ! disabled ? onClick : undefined } + disabled={ disabled } label={ messages.remove } aria-describedby={ `components-form-token-field__token-text-${ instanceId }` } /> From e103a18cf94a26e6b2864387d6e2ca324e31d240 Mon Sep 17 00:00:00 2001 From: Ramon <ramonjd@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:30:53 +1100 Subject: [PATCH 311/325] Let's fix the pagination to the bottom of the sidebar panel using CSS. (#57294) We can now delete the Slot Container, which is not used elsewhere. --- .../edit-site/src/components/editor/index.js | 2 - .../global-styles/screen-revisions/index.js | 121 +++++++++--------- .../global-styles/screen-revisions/style.scss | 22 ++-- .../sidebar-edit-mode/sidebar-fixed-bottom.js | 36 ------ .../components/sidebar-edit-mode/style.scss | 10 -- 5 files changed, 69 insertions(+), 122 deletions(-) delete mode 100644 packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js diff --git a/packages/edit-site/src/components/editor/index.js b/packages/edit-site/src/components/editor/index.js index 6718fc705b50b9..afae1b362ee998 100644 --- a/packages/edit-site/src/components/editor/index.js +++ b/packages/edit-site/src/components/editor/index.js @@ -52,7 +52,6 @@ import useTitle from '../routes/use-title'; import CanvasLoader from '../canvas-loader'; import { unlock } from '../../lock-unlock'; import useEditedEntityRecord from '../use-edited-entity-record'; -import { SidebarFixedBottomSlot } from '../sidebar-edit-mode/sidebar-fixed-bottom'; import PatternModal from '../pattern-modal'; import { POST_TYPE_LABELS, TEMPLATE_POST_TYPE } from '../../utils/constants'; import SiteEditorCanvas from '../block-editor/site-editor-canvas'; @@ -266,7 +265,6 @@ export default function Editor( { isLoading } ) { isRightSidebarOpen && ( <> <ComplementaryArea.Slot scope="core/edit-site" /> - <SidebarFixedBottomSlot /> </> ) } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/index.js b/packages/edit-site/src/components/global-styles/screen-revisions/index.js index 0f52cbda221ee9..b1a5cdb3ce93ed 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/index.js +++ b/packages/edit-site/src/components/global-styles/screen-revisions/index.js @@ -20,7 +20,6 @@ import { import ScreenHeader from '../header'; import { unlock } from '../../../lock-unlock'; import Revisions from '../../revisions'; -import SidebarFixedBottom from '../../sidebar-edit-mode/sidebar-fixed-bottom'; import { store as editSiteStore } from '../../../store'; import useGlobalStylesRevisions from './use-global-styles-revisions'; import RevisionsButtons from './revisions-buttons'; @@ -158,71 +157,65 @@ function ScreenRevisions() { { ! hasRevisions && ( <Spinner className="edit-site-global-styles-screen-revisions__loading" /> ) } - <> - { hasRevisions && - ( editorCanvasContainerView === - 'global-styles-revisions:style-book' ? ( - <StyleBook - userConfig={ currentlySelectedRevision } - isSelected={ () => {} } - onClose={ () => { - setEditorCanvasContainerView( - 'global-styles-revisions' - ); - } } - /> - ) : ( - <Revisions - blocks={ blocks } - userConfig={ currentlySelectedRevision } - closeButtonLabel={ __( 'Close revisions' ) } - /> - ) ) } - <div className="edit-site-global-styles-screen-revisions"> - <RevisionsButtons - onChange={ selectRevision } - selectedRevisionId={ currentlySelectedRevisionId } - userRevisions={ currentRevisions } - canApplyRevision={ isLoadButtonEnabled } - onApplyRevision={ () => - hasUnsavedChanges - ? setIsLoadingRevisionWithUnsavedChanges( true ) - : restoreRevision( currentlySelectedRevision ) - } + { hasRevisions && + ( editorCanvasContainerView === + 'global-styles-revisions:style-book' ? ( + <StyleBook + userConfig={ currentlySelectedRevision } + isSelected={ () => {} } + onClose={ () => { + setEditorCanvasContainerView( + 'global-styles-revisions' + ); + } } + /> + ) : ( + <Revisions + blocks={ blocks } + userConfig={ currentlySelectedRevision } + closeButtonLabel={ __( 'Close revisions' ) } + /> + ) ) } + <RevisionsButtons + onChange={ selectRevision } + selectedRevisionId={ currentlySelectedRevisionId } + userRevisions={ currentRevisions } + canApplyRevision={ isLoadButtonEnabled } + onApplyRevision={ () => + hasUnsavedChanges + ? setIsLoadingRevisionWithUnsavedChanges( true ) + : restoreRevision( currentlySelectedRevision ) + } + /> + { numPages > 1 && ( + <div className="edit-site-global-styles-screen-revisions__footer"> + <Pagination + className="edit-site-global-styles-screen-revisions__pagination" + currentPage={ currentPage } + numPages={ numPages } + changePage={ setCurrentPage } + totalItems={ revisionsCount } + disabled={ isLoading } + label={ __( 'Global Styles pagination navigation' ) } /> </div> - { numPages > 1 && ( - <SidebarFixedBottom className="edit-site-global-styles-screen-revisions__footer"> - <Pagination - className="edit-site-global-styles-screen-revisions__pagination" - currentPage={ currentPage } - numPages={ numPages } - changePage={ setCurrentPage } - totalItems={ revisionsCount } - disabled={ isLoading } - label={ __( - 'Global Styles pagination navigation' - ) } - /> - </SidebarFixedBottom> - ) } - { isLoadingRevisionWithUnsavedChanges && ( - <ConfirmDialog - isOpen={ isLoadingRevisionWithUnsavedChanges } - confirmButtonText={ __( 'Apply' ) } - onConfirm={ () => - restoreRevision( currentlySelectedRevision ) - } - onCancel={ () => - setIsLoadingRevisionWithUnsavedChanges( false ) - } - > - { __( - 'Any unsaved changes will be lost when you apply this revision.' - ) } - </ConfirmDialog> - ) } - </> + ) } + { isLoadingRevisionWithUnsavedChanges && ( + <ConfirmDialog + isOpen={ isLoadingRevisionWithUnsavedChanges } + confirmButtonText={ __( 'Apply' ) } + onConfirm={ () => + restoreRevision( currentlySelectedRevision ) + } + onCancel={ () => + setIsLoadingRevisionWithUnsavedChanges( false ) + } + > + { __( + 'Any unsaved changes will be lost when you apply this revision.' + ) } + </ConfirmDialog> + ) } </> ); } diff --git a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss index 97d27282f61afa..4228f199689cbd 100644 --- a/packages/edit-site/src/components/global-styles/screen-revisions/style.scss +++ b/packages/edit-site/src/components/global-styles/screen-revisions/style.scss @@ -1,12 +1,8 @@ - -.edit-site-global-styles-screen-revisions { - margin: 0 $grid-unit-20 $grid-unit-20 $grid-unit-20; - padding-bottom: $grid-unit-60; -} - .edit-site-global-styles-screen-revisions__revisions-list { list-style: none; - margin: 0; + margin: 0 $grid-unit-20 $grid-unit-20 $grid-unit-20; + // Forces subsequent flex items to the bottom. + flex-grow: 1; li { margin-bottom: 0; } @@ -167,12 +163,11 @@ will-change: opacity; } .components-button.is-tertiary { - width: $button-size-small; - height: $button-size-small; font-size: 28px; font-weight: 200; color: $gray-900; margin-bottom: $grid-unit-05; + line-height: 1.2; } .components-button.is-tertiary:disabled { color: $gray-600; @@ -183,5 +178,12 @@ } .edit-site-global-styles-screen-revisions__footer { - height: $grid-unit-60; + height: $grid-unit-70; + z-index: 1; + position: sticky; + min-width: 100%; + bottom: 0; + background: $white; + padding: $grid-unit-15; + border-top: $border-width solid $gray-300; } diff --git a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js b/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js deleted file mode 100644 index 46f25bf7a19146..00000000000000 --- a/packages/edit-site/src/components/sidebar-edit-mode/sidebar-fixed-bottom.js +++ /dev/null @@ -1,36 +0,0 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - -/** - * WordPress dependencies - */ -import { privateApis as componentsPrivateApis } from '@wordpress/components'; - -/** - * Internal dependencies - */ -import { unlock } from '../../lock-unlock'; - -const { createPrivateSlotFill } = unlock( componentsPrivateApis ); -const SIDEBAR_FIXED_BOTTOM_SLOT_FILL_NAME = 'SidebarFixedBottom'; -const { Slot: SidebarFixedBottomSlot, Fill: SidebarFixedBottomFill } = - createPrivateSlotFill( SIDEBAR_FIXED_BOTTOM_SLOT_FILL_NAME ); - -export default function SidebarFixedBottom( { className, children } ) { - return ( - <SidebarFixedBottomFill> - <div - className={ classnames( - 'edit-site-sidebar-fixed-bottom-slot', - className - ) } - > - { children } - </div> - </SidebarFixedBottomFill> - ); -} - -export { SidebarFixedBottomSlot }; diff --git a/packages/edit-site/src/components/sidebar-edit-mode/style.scss b/packages/edit-site/src/components/sidebar-edit-mode/style.scss index 1bfb87aafa15c5..eeb5dc2d170cd2 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/style.scss +++ b/packages/edit-site/src/components/sidebar-edit-mode/style.scss @@ -99,13 +99,3 @@ } } } - -.edit-site-sidebar-fixed-bottom-slot { - position: absolute; - min-width: 100%; - bottom: 0; - background: $white; - padding: $grid-unit-15; - border-top: $border-width solid $gray-300; - box-sizing: content-box; -} From bf87c21982743ae5cce4d66ef0f7ecf4bb0389a2 Mon Sep 17 00:00:00 2001 From: tellthemachines <tellthemachines@users.noreply.github.com> Date: Thu, 21 Dec 2023 18:20:15 +1100 Subject: [PATCH 312/325] Hide drop indicator where block isn't allowed to drop. (#56843) * Hide drop indicator where block isn't allowed to drop. * Don't show indicator when drop outside of allowed parent * logic to change the draggable icon * Make chip transparent when dragging over undroppable area. * Remove unused prop * Check insertion point visibility * Move some code around * Throttle the event listener * Add dependencies to effect * Update CSS * Fade chip only when dragging in editor canvas * Improve disabled chip * Update to styled span. * Only check validity of container blocks. --- .../block-draggable/draggable-chip.js | 12 +- .../src/components/block-draggable/index.js | 120 +++++++++++++++++- .../src/components/block-draggable/style.scss | 35 +++++ .../src/components/block-mover/index.js | 2 +- .../components/use-block-drop-zone/index.js | 89 ++++++++++++- 5 files changed, 245 insertions(+), 13 deletions(-) diff --git a/packages/block-editor/src/components/block-draggable/draggable-chip.js b/packages/block-editor/src/components/block-draggable/draggable-chip.js index d7d053be179fa6..cca21e7e0b7858 100644 --- a/packages/block-editor/src/components/block-draggable/draggable-chip.js +++ b/packages/block-editor/src/components/block-draggable/draggable-chip.js @@ -10,7 +10,12 @@ import { dragHandle } from '@wordpress/icons'; */ import BlockIcon from '../block-icon'; -export default function BlockDraggableChip( { count, icon, isPattern } ) { +export default function BlockDraggableChip( { + count, + icon, + isPattern, + fadeWhenDisabled, +} ) { const patternLabel = isPattern && __( 'Pattern' ); return ( <div className="block-editor-block-draggable-chip-wrapper"> @@ -37,6 +42,11 @@ export default function BlockDraggableChip( { count, icon, isPattern } ) { <FlexItem> <BlockIcon icon={ dragHandle } /> </FlexItem> + { fadeWhenDisabled && ( + <FlexItem className="block-editor-block-draggable-chip__disabled"> + <span className="block-editor-block-draggable-chip__disabled-icon"></span> + </FlexItem> + ) } </Flex> </div> </div> diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js index 0b8f3c2d87f520..7191a0f1428a92 100644 --- a/packages/block-editor/src/components/block-draggable/index.js +++ b/packages/block-editor/src/components/block-draggable/index.js @@ -5,6 +5,7 @@ import { store as blocksStore } from '@wordpress/blocks'; import { Draggable } from '@wordpress/components'; import { useSelect, useDispatch } from '@wordpress/data'; import { useEffect, useRef } from '@wordpress/element'; +import { throttle } from '@wordpress/compose'; /** * Internal dependencies @@ -12,6 +13,8 @@ import { useEffect, useRef } from '@wordpress/element'; import BlockDraggableChip from './draggable-chip'; import useScrollWhenDragging from './use-scroll-when-dragging'; import { store as blockEditorStore } from '../../store'; +import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs'; +import { isDropTargetValid } from '../use-block-drop-zone'; const BlockDraggable = ( { children, @@ -19,16 +22,24 @@ const BlockDraggable = ( { cloneClassname, onDragStart, onDragEnd, + fadeWhenDisabled = false, } ) => { - const { srcRootClientId, isDraggable, icon } = useSelect( + const { + srcRootClientId, + isDraggable, + icon, + visibleInserter, + getBlockType, + } = useSelect( ( select ) => { const { canMoveBlocks, getBlockRootClientId, getBlockName, getBlockAttributes, + isBlockInsertionPointVisible, } = select( blockEditorStore ); - const { getBlockType, getActiveBlockVariation } = + const { getBlockType: _getBlockType, getActiveBlockVariation } = select( blocksStore ); const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); const blockName = getBlockName( clientIds[ 0 ] ); @@ -40,15 +51,21 @@ const BlockDraggable = ( { return { srcRootClientId: rootClientId, isDraggable: canMoveBlocks( clientIds, rootClientId ), - icon: variation?.icon || getBlockType( blockName )?.icon, + icon: variation?.icon || _getBlockType( blockName )?.icon, + visibleInserter: isBlockInsertionPointVisible(), + getBlockType: _getBlockType, }; }, [ clientIds ] ); + const isDragging = useRef( false ); const [ startScrolling, scrollOnDragOver, stopScrolling ] = useScrollWhenDragging(); + const { getAllowedBlocks, getBlockNamesByClientId, getBlockRootClientId } = + useSelect( blockEditorStore ); + const { startDraggingBlocks, stopDraggingBlocks } = useDispatch( blockEditorStore ); @@ -61,6 +78,97 @@ const BlockDraggable = ( { }; }, [] ); + // Find the root of the editor iframe. + const blockRef = useBlockRef( clientIds[ 0 ] ); + const editorRoot = blockRef.current?.closest( 'body' ); + + /* + * Add a dragover event listener to the editor root to track the blocks being dragged over. + * The listener has to be inside the editor iframe otherwise the target isn't accessible. + */ + useEffect( () => { + if ( ! editorRoot || ! fadeWhenDisabled ) { + return; + } + + const onDragOver = ( event ) => { + if ( ! event.target.closest( '[data-block]' ) ) { + return; + } + const draggedBlockNames = getBlockNamesByClientId( clientIds ); + const targetClientId = event.target + .closest( '[data-block]' ) + .getAttribute( 'data-block' ); + + const allowedBlocks = getAllowedBlocks( targetClientId ); + const targetBlockName = getBlockNamesByClientId( [ + targetClientId, + ] )[ 0 ]; + + /* + * Check if the target is valid to drop in. + * If the target's allowedBlocks is an empty array, + * it isn't a container block, in which case we check + * its parent's validity instead. + */ + let dropTargetValid; + if ( allowedBlocks?.length === 0 ) { + const targetRootClientId = + getBlockRootClientId( targetClientId ); + const targetRootBlockName = getBlockNamesByClientId( [ + targetRootClientId, + ] )[ 0 ]; + const rootAllowedBlocks = + getAllowedBlocks( targetRootClientId ); + dropTargetValid = isDropTargetValid( + getBlockType, + rootAllowedBlocks, + draggedBlockNames, + targetRootBlockName + ); + } else { + dropTargetValid = isDropTargetValid( + getBlockType, + allowedBlocks, + draggedBlockNames, + targetBlockName + ); + } + + /* + * Update the body class to reflect if drop target is valid. + * This has to be done on the document body because the draggable + * chip is rendered outside of the editor iframe. + */ + if ( ! dropTargetValid && ! visibleInserter ) { + window?.document?.body?.classList?.add( + 'block-draggable-invalid-drag-token' + ); + } else { + window?.document?.body?.classList?.remove( + 'block-draggable-invalid-drag-token' + ); + } + }; + + const throttledOnDragOver = throttle( onDragOver, 200 ); + + editorRoot.addEventListener( 'dragover', throttledOnDragOver ); + + return () => { + editorRoot.removeEventListener( 'dragover', throttledOnDragOver ); + }; + }, [ + clientIds, + editorRoot, + fadeWhenDisabled, + getAllowedBlocks, + getBlockNamesByClientId, + getBlockRootClientId, + getBlockType, + visibleInserter, + ] ); + if ( ! isDraggable ) { return children( { draggable: false } ); } @@ -102,7 +210,11 @@ const BlockDraggable = ( { } } } __experimentalDragComponent={ - <BlockDraggableChip count={ clientIds.length } icon={ icon } /> + <BlockDraggableChip + count={ clientIds.length } + icon={ icon } + fadeWhenDisabled + /> } > { ( { onDraggableStart, onDraggableEnd } ) => { diff --git a/packages/block-editor/src/components/block-draggable/style.scss b/packages/block-editor/src/components/block-draggable/style.scss index a27d4c4caf2f29..afbf77319f7200 100644 --- a/packages/block-editor/src/components/block-draggable/style.scss +++ b/packages/block-editor/src/components/block-draggable/style.scss @@ -14,6 +14,7 @@ display: inline-flex; height: $block-toolbar-height; padding: 0 ( $grid-unit-15 + $border-width ); + position: relative; user-select: none; width: max-content; @@ -45,3 +46,37 @@ font-size: $default-font-size; } } + +// Specificity bump to override native component style. +.block-editor-block-draggable-chip__disabled.block-editor-block-draggable-chip__disabled { + opacity: 0; + position: absolute; + top: 0; + right: 0; + left: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + background-color: transparent; + transition: all 0.1s linear 0.1s; + + .block-editor-block-draggable-chip__disabled-icon { + width: $grid-unit-50 * 0.5; + height: $grid-unit-50 * 0.5; + box-shadow: inset 0 0 0 1.5px $white; + border-radius: 50%; + display: inline-block; + padding: 0; + background: transparent linear-gradient(-45deg, transparent 47.5%, $white 47.5%, $white 52.5%, transparent 52.5%); + + } +} + +.block-draggable-invalid-drag-token { + .block-editor-block-draggable-chip__disabled.block-editor-block-draggable-chip__disabled { + background-color: $gray-700; + opacity: 1; + box-shadow: 0 4px 8px rgba($black, 0.2); + } +} diff --git a/packages/block-editor/src/components/block-mover/index.js b/packages/block-editor/src/components/block-mover/index.js index 9b9f32457561e6..328160e944e954 100644 --- a/packages/block-editor/src/components/block-mover/index.js +++ b/packages/block-editor/src/components/block-mover/index.js @@ -64,7 +64,7 @@ function BlockMover( { clientIds, hideDragHandle } ) { } ) } > { ! hideDragHandle && ( - <BlockDraggable clientIds={ clientIds }> + <BlockDraggable clientIds={ clientIds } fadeWhenDisabled> { ( draggableProps ) => ( <Button icon={ dragHandle } diff --git a/packages/block-editor/src/components/use-block-drop-zone/index.js b/packages/block-editor/src/components/use-block-drop-zone/index.js index 4f8a08db0c610f..20bec8af76dff8 100644 --- a/packages/block-editor/src/components/use-block-drop-zone/index.js +++ b/packages/block-editor/src/components/use-block-drop-zone/index.js @@ -8,7 +8,10 @@ import { __experimentalUseDropZone as useDropZone, } from '@wordpress/compose'; import { isRTL } from '@wordpress/i18n'; -import { isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock } from '@wordpress/blocks'; +import { + isUnmodifiedDefaultBlock as getIsUnmodifiedDefaultBlock, + store as blocksStore, +} from '@wordpress/blocks'; /** * Internal dependencies @@ -191,6 +194,50 @@ export function getDropTargetPosition( ]; } +/** + * Check if the dragged blocks can be dropped on the target. + * @param {Function} getBlockType + * @param {Object[]} allowedBlocks + * @param {string[]} draggedBlockNames + * @param {string} targetBlockName + * @return {boolean} Whether the dragged blocks can be dropped on the target. + */ +export function isDropTargetValid( + getBlockType, + allowedBlocks, + draggedBlockNames, + targetBlockName +) { + // At root level allowedBlocks is undefined and all blocks are allowed. + // Otherwise, check if all dragged blocks are allowed. + let areBlocksAllowed = true; + if ( allowedBlocks ) { + const allowedBlockNames = allowedBlocks?.map( ( { name } ) => name ); + + areBlocksAllowed = draggedBlockNames.every( ( name ) => + allowedBlockNames?.includes( name ) + ); + } + + // Work out if dragged blocks have an allowed parent and if so + // check target block matches the allowed parent. + const draggedBlockTypes = draggedBlockNames.map( ( name ) => + getBlockType( name ) + ); + const targetMatchesDraggedBlockParents = draggedBlockTypes.every( + ( block ) => { + const [ allowedParentName ] = block?.parent || []; + if ( ! allowedParentName ) { + return true; + } + + return allowedParentName === targetBlockName; + } + ); + + return areBlocksAllowed && targetMatchesDraggedBlockParents; +} + /** * @typedef {Object} WPBlockDropZoneConfig * @property {?HTMLElement} dropZoneElement Optional element to be used as the drop zone. @@ -218,8 +265,15 @@ export default function useBlockDropZone( { operation: 'insert', } ); - const { getBlockListSettings, getBlocks, getBlockIndex } = - useSelect( blockEditorStore ); + const { getBlockType } = useSelect( blocksStore ); + const { + getBlockListSettings, + getBlocks, + getBlockIndex, + getDraggedBlockClientIds, + getBlockNamesByClientId, + getAllowedBlocks, + } = useSelect( blockEditorStore ); const { showInsertionPoint, hideInsertionPoint } = useDispatch( blockEditorStore ); @@ -235,6 +289,23 @@ export default function useBlockDropZone( { const throttled = useThrottle( useCallback( ( event, ownerDocument ) => { + const allowedBlocks = getAllowedBlocks( targetRootClientId ); + const targetBlockName = getBlockNamesByClientId( [ + targetRootClientId, + ] )[ 0 ]; + const draggedBlockNames = getBlockNamesByClientId( + getDraggedBlockClientIds() + ); + const isBlockDroppingAllowed = isDropTargetValid( + getBlockType, + allowedBlocks, + draggedBlockNames, + targetBlockName + ); + if ( ! isBlockDroppingAllowed ) { + return; + } + const blocks = getBlocks( targetRootClientId ); // The block list is empty, don't show the insertion point but still allow dropping. @@ -299,14 +370,18 @@ export default function useBlockDropZone( { } ); }, [ - dropZoneElement, - getBlocks, + getAllowedBlocks, targetRootClientId, + getBlockNamesByClientId, + getDraggedBlockClientIds, + getBlockType, + getBlocks, getBlockListSettings, + dropZoneElement, + parentBlockClientId, + getBlockIndex, registry, showInsertionPoint, - getBlockIndex, - parentBlockClientId, ] ), 200 From 1275d2f0d02961a8cc109bcc3f3ab76a8609bdd6 Mon Sep 17 00:00:00 2001 From: Kai Hao <kevin830726@gmail.com> Date: Thu, 21 Dec 2023 18:04:44 +0800 Subject: [PATCH 313/325] [Pattern Overrides] Use a single checkbox to turn on pattern overrides for all allowed attributes (#57009) * Use a single checkbox to turn on pattern overrides * Add check before setting/unsetting connection source * Code review --- .../components/partial-syncing-controls.js | 86 ++++++++++--------- 1 file changed, 46 insertions(+), 40 deletions(-) diff --git a/packages/patterns/src/components/partial-syncing-controls.js b/packages/patterns/src/components/partial-syncing-controls.js index 42c39ce69e87bf..d20bd1d347012e 100644 --- a/packages/patterns/src/components/partial-syncing-controls.js +++ b/packages/patterns/src/components/partial-syncing-controls.js @@ -17,23 +17,38 @@ import { PARTIAL_SYNCING_SUPPORTED_BLOCKS } from '../constants'; function PartialSyncingControls( { name, attributes, setAttributes } ) { const syncedAttributes = PARTIAL_SYNCING_SUPPORTED_BLOCKS[ name ]; + const attributeSources = Object.keys( syncedAttributes ).map( + ( attributeName ) => + attributes.connections?.attributes?.[ attributeName ]?.source + ); + const isConnectedToOtherSources = attributeSources.every( + ( source ) => source && source !== 'pattern_attributes' + ); + + // Render nothing if all supported attributes are connected to other sources. + if ( isConnectedToOtherSources ) { + return null; + } + + function updateConnections( isChecked ) { + let updatedConnections = { + ...attributes.connections, + attributes: { ...attributes.connections?.attributes }, + }; - function updateConnections( attributeName, isChecked ) { if ( ! isChecked ) { - let updatedConnections = { - ...attributes.connections, - attributes: { - ...attributes.connections?.attributes, - [ attributeName ]: undefined, - }, - }; - if ( Object.keys( updatedConnections.attributes ).length === 1 ) { - updatedConnections.attributes = undefined; + for ( const attributeName of Object.keys( syncedAttributes ) ) { + if ( + updatedConnections.attributes[ attributeName ]?.source === + 'pattern_attributes' + ) { + delete updatedConnections.attributes[ attributeName ]; + } } - if ( - Object.keys( updatedConnections ).length === 1 && - updateConnections.attributes === undefined - ) { + if ( ! Object.keys( updatedConnections.attributes ).length ) { + delete updatedConnections.attributes; + } + if ( ! Object.keys( updatedConnections ).length ) { updatedConnections = undefined; } setAttributes( { @@ -42,15 +57,13 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { return; } - const updatedConnections = { - ...attributes.connections, - attributes: { - ...attributes.connections?.attributes, - [ attributeName ]: { + for ( const attributeName of Object.keys( syncedAttributes ) ) { + if ( ! updatedConnections.attributes[ attributeName ] ) { + updatedConnections.attributes[ attributeName ] = { source: 'pattern_attributes', - }, - }, - }; + }; + } + } if ( typeof attributes.metadata?.id === 'string' ) { setAttributes( { connections: updatedConnections } ); @@ -71,25 +84,18 @@ function PartialSyncingControls( { name, attributes, setAttributes } ) { <InspectorControls group="advanced"> <BaseControl __nextHasNoMarginBottom> <BaseControl.VisualLabel> - { __( 'Synced attributes' ) } + { __( 'Pattern overrides' ) } </BaseControl.VisualLabel> - { Object.entries( syncedAttributes ).map( - ( [ attributeName, label ] ) => ( - <CheckboxControl - key={ attributeName } - __nextHasNoMarginBottom - label={ label } - checked={ - attributes.connections?.attributes?.[ - attributeName - ]?.source === 'pattern_attributes' - } - onChange={ ( isChecked ) => { - updateConnections( attributeName, isChecked ); - } } - /> - ) - ) } + <CheckboxControl + __nextHasNoMarginBottom + label={ __( 'Allow instance overrides' ) } + checked={ attributeSources.some( + ( source ) => source === 'pattern_attributes' + ) } + onChange={ ( isChecked ) => { + updateConnections( isChecked ); + } } + /> </BaseControl> </InspectorControls> ); From 2b5cfdf52aad5f8d54ca44bac3cc87c475f01b72 Mon Sep 17 00:00:00 2001 From: Marco Ciampini <marco.ciampo@gmail.com> Date: Thu, 21 Dec 2023 11:53:49 +0100 Subject: [PATCH 314/325] Truncate: improve handling of non-string children (#57261) * Truncate: allow for numbers, otherwise just pass through children * Improve unit tests * CHANGELOG * Add extra explanation around truncation behavior based on the type of the children prop * Output warning when children are not string or number --- packages/components/CHANGELOG.md | 1 + packages/components/src/truncate/README.md | 8 ++ packages/components/src/truncate/hook.ts | 27 ++++--- .../components/src/truncate/test/index.tsx | 81 ++++++++++++------- packages/components/src/truncate/types.ts | 4 + 5 files changed, 84 insertions(+), 37 deletions(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 402d78fc354d5d..f6a1282744a496 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -11,6 +11,7 @@ - `Button`: Fix logic of `has-text` class addition ([#56949](https://github.com/WordPress/gutenberg/pull/56949)). - `FormTokenField`: Fix a regression where the suggestion list would re-open after clicking away from the input ([#57002](https://github.com/WordPress/gutenberg/pull/57002)). - `Snackbar`: Remove erroneous `__unstableHTML` prop from TypeScript definitions ([#57218](https://github.com/WordPress/gutenberg/pull/57218)). +- `Truncate`: improve handling of non-string `children` ([#57261](https://github.com/WordPress/gutenberg/pull/57261)). ### Enhancements diff --git a/packages/components/src/truncate/README.md b/packages/components/src/truncate/README.md index d173edb14977cd..7665d82de0ba87 100644 --- a/packages/components/src/truncate/README.md +++ b/packages/components/src/truncate/README.md @@ -24,6 +24,14 @@ function Example() { ## Props +##### `children`: `ReactNode` + +The children elements. + +Note: text truncation will be attempted only if the `children` are either of type `string` or `number`. In any other scenarios, the component will not attempt to truncate the text, and will pass through the `children`. + +- Required: Yes + ##### `ellipsis`: `string` The ellipsis string when truncating the text by the `limit` prop's value. diff --git a/packages/components/src/truncate/hook.ts b/packages/components/src/truncate/hook.ts index 3877e216ba5801..16406a4e51bb9a 100644 --- a/packages/components/src/truncate/hook.ts +++ b/packages/components/src/truncate/hook.ts @@ -33,17 +33,24 @@ export default function useTruncate( const cx = useCx(); - const truncatedContent = truncateContent( - typeof children === 'string' ? children : '', - { - ellipsis, - ellipsizeMode, - limit, - numberOfLines, - } - ); + let childrenAsText; + if ( typeof children === 'string' ) { + childrenAsText = children; + } else if ( typeof children === 'number' ) { + childrenAsText = children.toString(); + } - const shouldTruncate = ellipsizeMode === TRUNCATE_TYPE.auto; + const truncatedContent = childrenAsText + ? truncateContent( childrenAsText, { + ellipsis, + ellipsizeMode, + limit, + numberOfLines, + } ) + : children; + + const shouldTruncate = + !! childrenAsText && ellipsizeMode === TRUNCATE_TYPE.auto; const classes = useMemo( () => { const truncateLines = css` diff --git a/packages/components/src/truncate/test/index.tsx b/packages/components/src/truncate/test/index.tsx index 082b6aa232d477..399393a25e4201 100644 --- a/packages/components/src/truncate/test/index.tsx +++ b/packages/components/src/truncate/test/index.tsx @@ -8,36 +8,63 @@ import { render, screen } from '@testing-library/react'; */ import { Truncate } from '..'; -describe( 'props', () => { - test( 'should render correctly', () => { - render( <Truncate>Lorem ipsum.</Truncate> ); - expect( screen.getByText( 'Lorem ipsum.' ) ).toBeVisible(); - } ); +describe( 'Truncate', () => { + describe( 'with string or number children', () => { + test( 'should pass through when no truncation props are set', () => { + render( <Truncate>Lorem ipsum</Truncate> ); + expect( screen.getByText( 'Lorem ipsum' ) ).toBeVisible(); + } ); - test( 'should render limit', () => { - render( - <Truncate limit={ 1 } ellipsizeMode="tail"> - Lorem ipsum. - </Truncate> - ); - expect( screen.getByText( 'L…' ) ).toBeVisible(); - } ); + test( 'should render numbers correctly', () => { + render( <Truncate>{ 14 }</Truncate> ); + expect( screen.getByText( '14' ) ).toBeVisible(); + } ); + + test( 'should truncate text from the start when the limit prop is set and the ellipsizeMode is tail', () => { + render( + <Truncate limit={ 1 } ellipsizeMode="tail"> + Lorem ipsum + </Truncate> + ); + expect( screen.getByText( 'L…' ) ).toBeVisible(); + } ); + + test( 'should truncate text from the end when the limit prop is set and the ellipsizeMode is head', () => { + render( + <Truncate limit={ 1 } ellipsizeMode="head"> + Lorem ipsum + </Truncate> + ); + expect( screen.getByText( '…m' ) ).toBeVisible(); + } ); + + test( 'should render custom ellipsis', () => { + render( + <Truncate ellipsis="!!!" limit={ 5 } ellipsizeMode="tail"> + Lorem ipsum. + </Truncate> + ); + expect( screen.getByText( 'Lorem!!!' ) ).toBeVisible(); + } ); - test( 'should render custom ellipsis', () => { - render( - <Truncate ellipsis="!!!" limit={ 5 } ellipsizeMode="tail"> - Lorem ipsum. - </Truncate> - ); - expect( screen.getByText( 'Lorem!!!' ) ).toBeVisible(); + test( 'should render custom ellipsizeMode', () => { + render( + <Truncate ellipsis="!!!" ellipsizeMode="middle" limit={ 5 }> + Lorem ipsum. + </Truncate> + ); + expect( screen.getByText( 'Lo!!!m.' ) ).toBeVisible(); + } ); } ); - test( 'should render custom ellipsizeMode', () => { - render( - <Truncate ellipsis="!!!" ellipsizeMode="middle" limit={ 5 }> - Lorem ipsum. - </Truncate> - ); - expect( screen.getByText( 'Lo!!!m.' ) ).toBeVisible(); + describe( 'with other children types', () => { + test( 'should no-op and output a warning to console', () => { + render( + <Truncate> + <>Lorem ipsum</> + </Truncate> + ); + expect( screen.getByText( 'Lorem ipsum' ) ).toBeVisible(); + } ); } ); } ); diff --git a/packages/components/src/truncate/types.ts b/packages/components/src/truncate/types.ts index f9b720657d9b59..6725f9e225f8a4 100644 --- a/packages/components/src/truncate/types.ts +++ b/packages/components/src/truncate/types.ts @@ -48,6 +48,10 @@ export type TruncateProps = { numberOfLines?: number; /** * The children elements. + * + * Note: text truncation will be attempted only if the `children` are either + * of type `string` or `number`. In any other scenarios, the component will + * not attempt to truncate the text, and will pass through the `children`. */ children: ReactNode; }; From 73af69b59b44f28ed4abf0f8de2fdd285fd80d96 Mon Sep 17 00:00:00 2001 From: Chad Chadbourne <13856531+chad1008@users.noreply.github.com> Date: Thu, 21 Dec 2023 06:16:27 -0500 Subject: [PATCH 315/325] Tabs: update styling to more closely match previous implementation (#57275) * match previous `TabPanel`/`Button` styling * changelog --- packages/components/CHANGELOG.md | 1 + packages/components/src/tabs/styles.ts | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index f6a1282744a496..ecce61cb91509c 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -22,6 +22,7 @@ ### Experimental - `Tabs`: do not render hidden content ([#57046](https://github.com/WordPress/gutenberg/pull/57046)). +- `Tabs`: improve hover and text alignment styles ([#57275](https://github.com/WordPress/gutenberg/pull/57275)). - `Tabs`: make sure `Tab`s are associated to the right `TabPanel`s, regardless of the order they're rendered in ([#57033](https://github.com/WordPress/gutenberg/pull/57033)). ## 25.14.0 (2023-12-13) diff --git a/packages/components/src/tabs/styles.ts b/packages/components/src/tabs/styles.ts index cb735f3177662a..6abca79aa51aed 100644 --- a/packages/components/src/tabs/styles.ts +++ b/packages/components/src/tabs/styles.ts @@ -22,7 +22,9 @@ export const TabListWrapper = styled.div` `; export const Tab = styled( Ariakit.Tab )` - && { + & { + display: inline-flex; + align-items: center; position: relative; border-radius: 0; height: ${ space( 12 ) }; @@ -39,6 +41,10 @@ export const Tab = styled( Ariakit.Tab )` opacity: 0.3; } + &:hover { + color: ${ COLORS.theme.accent }; + } + &:focus:not( :disabled ) { position: relative; box-shadow: none; From 454f1166714c0a29d02e406d0eb6a099fccd02d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:04:11 +0100 Subject: [PATCH 316/325] DataViews: update docs (#57305) --- packages/dataviews/README.md | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index c0d0a01cbc3e28..f92be05d821c40 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -14,14 +14,18 @@ npm install @wordpress/dataviews --save ```js <DataViews - data={ pages } - getItemId={ ( item ) => item.id } - isLoading={ isLoadingPages } + data={ data } + paginationInfo={ { totalItems, totalPages } } view={ view } onChangeView={ onChangeView } fields={ fields } actions={ [ trashPostAction ] } - paginationInfo={ { totalItems, totalPages } } + search={ false } + searchLabel="Filter list" + getItemId={ ( item ) => item.id } + isLoading={ isLoadingData } + supportedLayouts={ [ 'table' ] } + deferredRendering={ true } onSelectionChange={ ( items ) => { /* ... */ } } /> ``` @@ -39,6 +43,11 @@ Example: ] ``` +## Pagination Info + +- `totalItems`: the total number of items in the datasets. +- `totalPages`: the total number of pages, taking into account the total items in the dataset and the number of items per page provided by the user. + ## View The view object configures how the dataset is visible to the user. @@ -79,9 +88,11 @@ Example: - `mediaField`: used by the `grid` and `list` layouts. The `id` of the field to be used for rendering each card's media. - `primaryField`: used by the `grid` and `list` layouts. The `id` of the field to be highlighted in each card/list item. -### View <=> data +### onChangeView: syncing view and data + +The view is a representation of the visible state of the dataset: what type of layout is used to display it (table, grid, etc.), how the dataset is filtered, how it is sorted or paginated. -The view is a representation of the visible state of the dataset. Note, however, that it's the consumer's responsibility to work with the data provider to make sure the user options defined through the view's config (sort, pagination, filters, etc.) are respected. +It's the consumer's responsibility to work with the data provider to make sure the user options defined through the view's config (sort, pagination, filters, etc.) are respected. The `onChangeView` prop allows the consumer to provide a callback to be called when the view config changes, to process the data accordingly. The following example shows how a view object is used to query the WordPress REST API via the entities abstraction. The same can be done with any other data provider. @@ -215,6 +226,16 @@ Array of operations that can be performed upon each record. Each action is an ob - `in`: operator to be used in filters for fields of type `enumeration`. - `notIn`: operator to be used in filters for fields of type `enumeration`. +## Other properties + +- `search`: whether the search input is enabled. `true` by default. +- `searchLabel`: what text to show in the search input. "Filter list" by default. +- `getItemId`: function that receives an item and return an unique identifier for it. Required. +- `isLoading`: whether the data is loading. `false` by default. +- `supportedLayouts`: array of layouts supported. By default, all are: `table`, `grid`, `list`. +- `deferredRendering`: whether the items should be rendered asynchronously. Required. +- `onSelectionChange`: callback that returns the selected items. So far, only the `list` view implements this. + ## Contributing to this package This is an individual package that's part of the Gutenberg project. The project is organized as a monorepo. It's made up of multiple self-contained software packages, each with a specific purpose. The packages in this monorepo are published to [npm](https://www.npmjs.com/) and used by [WordPress](https://make.wordpress.org/core/) as well as other software projects. From 907b53a09e3ce8cd5446506ac0fcc1d6f091ad3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 21 Dec 2023 13:16:14 +0100 Subject: [PATCH 317/325] DatViews: remove `paginationInfo` prop from ViewComponent (#57306) --- packages/dataviews/src/dataviews.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 9e7b45d04ef87f..8f63c21fc2fecf 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -79,7 +79,6 @@ export default function DataViews( { fields={ _fields } view={ view } onChangeView={ onChangeView } - paginationInfo={ paginationInfo } actions={ actions } data={ data } getItemId={ getItemId } From c3e60ff71ecadc4bed55fe219c91a8e51e6ef8b0 Mon Sep 17 00:00:00 2001 From: JuanMa <juanma.garrido@automattic.com> Date: Thu, 21 Dec 2023 13:16:26 +0100 Subject: [PATCH 318/325] minor heading to improve readability and reference (#57102) --- docs/getting-started/fundamentals/block-json.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/getting-started/fundamentals/block-json.md b/docs/getting-started/fundamentals/block-json.md index d06d2579969f4a..f659564f0cfc3c 100644 --- a/docs/getting-started/fundamentals/block-json.md +++ b/docs/getting-started/fundamentals/block-json.md @@ -73,7 +73,9 @@ _Example: Atributes stored in the Markup representation of the block_ <!-- /wp:block-development-examples/copyright-date-block-09aac3 -->x ``` -These [attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) are passed to the React component `Edit`(to display in the Block Editor), and the `save` function (to return the markup saved to the database) of the block, and to any server-side render definition for the block (see the `render` property above). +### Reading and updating attributes + +These [attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#attributes) are passed to the React component `Edit`(to display in the Block Editor) and the `save` function (to return the markup saved to the database) of the block, and to any server-side render definition for the block (see the `render` property above). The `Edit` component receives exclusively the capability of updating the attributes via the [`setAttributes`](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#setattributes) function. From 05b7175a7b0d51ffe9c79f02b9839c4bddd7431c Mon Sep 17 00:00:00 2001 From: Jerry Jones <jones.jeremydavid@gmail.com> Date: Thu, 21 Dec 2023 06:22:00 -0600 Subject: [PATCH 319/325] Exclude disabled buttons when setting initialIndex of NavigableToolbar (#57280) * Exclude disabled buttons when setting initial focus on toolbar When the first item was disabled, it was causing the last item of the toolbar to be focused because the the toolbar was trying to focus a disabled button. This updates the check to only include focusable buttons so we do not try to send focus to an unfocusable button. * Change getAllToolbarItemsIn to getAllFocusableToolbarItemsIn for clarity --- .../src/components/navigable-toolbar/index.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/navigable-toolbar/index.js b/packages/block-editor/src/components/navigable-toolbar/index.js index 1a85fa0f3d7fb4..e97efb2a4b3910 100644 --- a/packages/block-editor/src/components/navigable-toolbar/index.js +++ b/packages/block-editor/src/components/navigable-toolbar/index.js @@ -25,8 +25,12 @@ function hasOnlyToolbarItem( elements ) { return ! elements.some( ( element ) => ! ( dataProp in element.dataset ) ); } -function getAllToolbarItemsIn( container ) { - return Array.from( container.querySelectorAll( '[data-toolbar-item]' ) ); +function getAllFocusableToolbarItemsIn( container ) { + return Array.from( + container.querySelectorAll( + '[data-toolbar-item]:not([disabled]):not([aria-disabled="true"])' + ) + ); } function hasFocusWithin( container ) { @@ -141,7 +145,8 @@ function useToolbarFocus( { let raf = 0; if ( ! initialFocusOnMount ) { raf = window.requestAnimationFrame( () => { - const items = getAllToolbarItemsIn( navigableToolbarRef ); + const items = + getAllFocusableToolbarItemsIn( navigableToolbarRef ); const index = initialIndex || 0; if ( items[ index ] && hasFocusWithin( navigableToolbarRef ) ) { items[ index ].focus( { @@ -158,7 +163,7 @@ function useToolbarFocus( { if ( ! onIndexChange || ! navigableToolbarRef ) return; // When the toolbar element is unmounted and onIndexChange is passed, we // pass the focused toolbar item index so it can be hydrated later. - const items = getAllToolbarItemsIn( navigableToolbarRef ); + const items = getAllFocusableToolbarItemsIn( navigableToolbarRef ); const index = items.findIndex( ( item ) => item.tabIndex === 0 ); onIndexChange( index ); }; From eca4dfe9837e6eb2c182eb06202d6130285495b4 Mon Sep 17 00:00:00 2001 From: Jon Surrell <sirreal@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:29:08 +0100 Subject: [PATCH 320/325] Scripts, DependencyExtractionWebpackPlugin: Drop webpack4 and node<18 (#57303) * Drop support for webpack 4 Webpack 5 was released ~3 years ago. New features are being developed around ECMAScript modules, which are unsupported in webpack 4. * Drop support for Node.js versions < 18 v18 is the current LTS release of Node. Older versions are not supported. --- package-lock.json | 12 ++--- .../CHANGELOG.md | 5 ++ .../lib/index.js | 53 +++++++------------ .../package.json | 7 ++- packages/scripts/CHANGELOG.md | 4 ++ packages/scripts/package.json | 2 +- 6 files changed, 37 insertions(+), 46 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88404c01a18a91..0d048fae712ed6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -55067,14 +55067,13 @@ "dev": true, "license": "GPL-2.0-or-later", "dependencies": { - "json2php": "^0.0.7", - "webpack-sources": "^3.2.2" + "json2php": "^0.0.7" }, "engines": { - "node": ">=14" + "node": ">=18" }, "peerDependencies": { - "webpack": "^4.8.3 || ^5.0.0" + "webpack": "^5.0.0" } }, "packages/deprecated": { @@ -56461,7 +56460,7 @@ "wp-scripts": "bin/wp-scripts.js" }, "engines": { - "node": ">=14", + "node": ">=18", "npm": ">=6.14.4" }, "peerDependencies": { @@ -70399,8 +70398,7 @@ "@wordpress/dependency-extraction-webpack-plugin": { "version": "file:packages/dependency-extraction-webpack-plugin", "requires": { - "json2php": "^0.0.7", - "webpack-sources": "^3.2.2" + "json2php": "^0.0.7" } }, "@wordpress/deprecated": { diff --git a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md index 58d248129ff201..78de3e4142c87e 100644 --- a/packages/dependency-extraction-webpack-plugin/CHANGELOG.md +++ b/packages/dependency-extraction-webpack-plugin/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Breaking Changes + +- Drop support for webpack 4. +- Drop support for Node.js versions < 18. + ## 4.31.0 (2023-12-13) ## 4.30.0 (2023-11-29) diff --git a/packages/dependency-extraction-webpack-plugin/lib/index.js b/packages/dependency-extraction-webpack-plugin/lib/index.js index 581274c3684f93..98b484672ef3f9 100644 --- a/packages/dependency-extraction-webpack-plugin/lib/index.js +++ b/packages/dependency-extraction-webpack-plugin/lib/index.js @@ -3,10 +3,7 @@ */ const path = require( 'path' ); const webpack = require( 'webpack' ); -// In webpack 5 there is a `webpack.sources` field but for webpack 4 we have to fallback to the `webpack-sources` package. -const { RawSource } = webpack.sources || require( 'webpack-sources' ); const json2php = require( 'json2php' ); -const isWebpack4 = webpack.version.startsWith( '4.' ); const { createHash } = webpack.util; /** @@ -17,6 +14,7 @@ const { defaultRequestToHandle, } = require( './util' ); +const { RawSource } = webpack.sources; const defaultExternalizedReportFileName = 'externalized-dependencies.json'; class DependencyExtractionWebpackPlugin { @@ -46,13 +44,11 @@ class DependencyExtractionWebpackPlugin { // Offload externalization work to the ExternalsPlugin. this.externalsPlugin = new webpack.ExternalsPlugin( 'window', - isWebpack4 - ? this.externalizeWpDeps.bind( this ) - : this.externalizeWpDepsV5.bind( this ) + this.externalizeWpDeps.bind( this ) ); } - externalizeWpDeps( _context, request, callback ) { + externalizeWpDeps( { request }, callback ) { let externalRequest; // Handle via options.requestToExternal first. @@ -77,10 +73,6 @@ class DependencyExtractionWebpackPlugin { return callback(); } - externalizeWpDepsV5( { context, request }, callback ) { - return this.externalizeWpDeps( context, request, callback ); - } - mapRequestToDependency( request ) { // Handle via options.requestToHandle first. if ( typeof this.options.requestToHandle === 'function' ) { @@ -115,25 +107,19 @@ class DependencyExtractionWebpackPlugin { apply( compiler ) { this.externalsPlugin.apply( compiler ); - if ( isWebpack4 ) { - compiler.hooks.emit.tap( this.constructor.name, ( compilation ) => - this.addAssets( compilation ) - ); - } else { - compiler.hooks.thisCompilation.tap( - this.constructor.name, - ( compilation ) => { - compilation.hooks.processAssets.tap( - { - name: this.constructor.name, - stage: compiler.webpack.Compilation - .PROCESS_ASSETS_STAGE_ANALYSE, - }, - () => this.addAssets( compilation ) - ); - } - ); - } + compiler.hooks.thisCompilation.tap( + this.constructor.name, + ( compilation ) => { + compilation.hooks.processAssets.tap( + { + name: this.constructor.name, + stage: compiler.webpack.Compilation + .PROCESS_ASSETS_STAGE_ANALYSE, + }, + () => this.addAssets( compilation ) + ); + } + ); } addAssets( compilation ) { @@ -192,9 +178,8 @@ class DependencyExtractionWebpackPlugin { }; // Search for externalized modules in all chunks. - const modulesIterable = isWebpack4 - ? chunk.modulesIterable - : compilation.chunkGraph.getChunkModules( chunk ); + const modulesIterable = + compilation.chunkGraph.getChunkModules( chunk ); for ( const chunkModule of modulesIterable ) { processModule( chunkModule ); // Loop through submodules of ConcatenatedModule. @@ -253,7 +238,7 @@ class DependencyExtractionWebpackPlugin { compilation.assets[ assetFilename ] = new RawSource( this.stringify( assetData ) ); - chunk.files[ isWebpack4 ? 'push' : 'add' ]( assetFilename ); + chunk.files.add( assetFilename ); } if ( combineAssets ) { diff --git a/packages/dependency-extraction-webpack-plugin/package.json b/packages/dependency-extraction-webpack-plugin/package.json index bdb5cfc1210715..b3a296e3f00146 100644 --- a/packages/dependency-extraction-webpack-plugin/package.json +++ b/packages/dependency-extraction-webpack-plugin/package.json @@ -20,7 +20,7 @@ "url": "https://github.com/WordPress/gutenberg/issues" }, "engines": { - "node": ">=14" + "node": ">=18" }, "files": [ "lib", @@ -29,11 +29,10 @@ "main": "lib/index.js", "types": "lib/types.d.ts", "dependencies": { - "json2php": "^0.0.7", - "webpack-sources": "^3.2.2" + "json2php": "^0.0.7" }, "peerDependencies": { - "webpack": "^4.8.3 || ^5.0.0" + "webpack": "^5.0.0" }, "publishConfig": { "access": "public" diff --git a/packages/scripts/CHANGELOG.md b/packages/scripts/CHANGELOG.md index 9e2f0ff87a3abf..f6fb412377f7ef 100644 --- a/packages/scripts/CHANGELOG.md +++ b/packages/scripts/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Breaking Changes + +- Drop support for Node.js versions < 18. + ## 26.19.0 (2023-12-13) ### Bug Fix diff --git a/packages/scripts/package.json b/packages/scripts/package.json index eabc1335aace53..f97ab389a527b1 100644 --- a/packages/scripts/package.json +++ b/packages/scripts/package.json @@ -19,7 +19,7 @@ "url": "https://github.com/WordPress/gutenberg/issues" }, "engines": { - "node": ">=14", + "node": ">=18", "npm": ">=6.14.4" }, "files": [ From a5c602a4adef73fd2127b753c2a86d056e7c1860 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9?= <583546+oandregal@users.noreply.github.com> Date: Thu, 21 Dec 2023 14:41:26 +0100 Subject: [PATCH 321/325] DataViews: make `getItemId` optional (#57308) --- packages/dataviews/README.md | 4 +++- packages/dataviews/src/dataviews.js | 4 +++- packages/dataviews/src/view-grid.js | 4 ++-- packages/dataviews/src/view-list.js | 4 ++-- packages/dataviews/src/view-table.js | 4 ++-- packages/edit-site/src/components/page-pages/index.js | 1 - packages/edit-site/src/components/page-templates/index.js | 1 - 7 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/dataviews/README.md b/packages/dataviews/README.md index f92be05d821c40..1f4bdb91129f52 100644 --- a/packages/dataviews/README.md +++ b/packages/dataviews/README.md @@ -43,6 +43,8 @@ Example: ] ``` +By default, dataviews would use each record's `id` as an unique identifier. If it's not, the consumer should provide a `getItemId` function that returns one. See "Other props" section. + ## Pagination Info - `totalItems`: the total number of items in the datasets. @@ -230,7 +232,7 @@ Array of operations that can be performed upon each record. Each action is an ob - `search`: whether the search input is enabled. `true` by default. - `searchLabel`: what text to show in the search input. "Filter list" by default. -- `getItemId`: function that receives an item and return an unique identifier for it. Required. +- `getItemId`: function that receives an item and returns an unique identifier for it. By default, it uses the `id` of the item as unique identifier. If it's not, the consumer should provide their own. - `isLoading`: whether the data is loading. `false` by default. - `supportedLayouts`: array of layouts supported. By default, all are: `table`, `grid`, `list`. - `deferredRendering`: whether the items should be rendered asynchronously. Required. diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 8f63c21fc2fecf..c506d872c64a60 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -16,6 +16,8 @@ import Filters from './filters'; import Search from './search'; import { VIEW_LAYOUTS } from './constants'; +const defaultGetItemId = ( item ) => item.id; + export default function DataViews( { view, onChangeView, @@ -24,7 +26,7 @@ export default function DataViews( { searchLabel = undefined, actions, data, - getItemId, + getItemId = defaultGetItemId, isLoading = false, paginationInfo, supportedLayouts, diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index e2c34ba6749faa..e43c9744c560f8 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -44,10 +44,10 @@ export default function ViewGrid( { alignment="top" className="dataviews-grid-view" > - { usedData.map( ( item, index ) => ( + { usedData.map( ( item ) => ( <VStack spacing={ 3 } - key={ getItemId?.( item ) || index } + key={ getItemId( item ) } className="dataviews-view-grid__card" > <div className="dataviews-view-grid__media"> diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index c50a0f1ca2682c..ab544e0eec9c2c 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -47,9 +47,9 @@ export default function ViewList( { return ( <ul className="dataviews-list-view"> - { usedData.map( ( item, index ) => { + { usedData.map( ( item ) => { return ( - <li key={ getItemId?.( item ) || index }> + <li key={ getItemId( item ) }> <div role="button" tabIndex={ 0 } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index e08449b76491df..d676ceaf147482 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -386,8 +386,8 @@ function ViewTable( { </tr> </thead> <tbody> - { usedData.map( ( item, index ) => ( - <tr key={ getItemId?.( item ) || index }> + { usedData.map( ( item ) => ( + <tr key={ getItemId( item ) }> { visibleFields.map( ( field ) => ( <td key={ field.id } diff --git a/packages/edit-site/src/components/page-pages/index.js b/packages/edit-site/src/components/page-pages/index.js index 9c3f55fc78f3bc..c9e36c1bd585d8 100644 --- a/packages/edit-site/src/components/page-pages/index.js +++ b/packages/edit-site/src/components/page-pages/index.js @@ -338,7 +338,6 @@ export default function PagePages() { fields={ fields } actions={ actions } data={ pages || EMPTY_ARRAY } - getItemId={ ( item ) => item.id } isLoading={ isLoadingPages || isLoadingAuthors } view={ view } onChangeView={ onChangeView } diff --git a/packages/edit-site/src/components/page-templates/index.js b/packages/edit-site/src/components/page-templates/index.js index 85027c0d47f3a8..d97a5e42fb6126 100644 --- a/packages/edit-site/src/components/page-templates/index.js +++ b/packages/edit-site/src/components/page-templates/index.js @@ -382,7 +382,6 @@ export default function DataviewsTemplates() { fields={ fields } actions={ actions } data={ shownTemplates } - getItemId={ ( item ) => item.id } isLoading={ isLoadingData } view={ view } onChangeView={ onChangeView } From cf476e2f948f31be0011fc166a60d9ea07adf1ee Mon Sep 17 00:00:00 2001 From: Matias Benedetto <matias.benedetto@gmail.com> Date: Thu, 21 Dec 2023 08:28:47 -0600 Subject: [PATCH 322/325] Font Library: consolidate existing API rest endpoints. (#57282) * Rename and standardize the font family endpoints * update tests * format php * visibility of register_routes method * remove not needed config * remove not needed config Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --------- Co-authored-by: Jason Crist <146530+pbking@users.noreply.github.com> Co-authored-by: Jeff Ong <5375500+jffng@users.noreply.github.com> --- ...rest-autosave-font-families-controller.php | 25 ++++ ...ss-wp-rest-font-collections-controller.php | 124 ++++++++++++++++++ ...lass-wp-rest-font-families-controller.php} | 91 +++---------- .../fonts/font-library/font-library.php | 15 ++- lib/load.php | 4 +- .../font-library-modal/resolvers.js | 8 +- .../base.php | 11 +- .../getFontCollection.php | 16 +-- .../getFontCollections.php | 10 +- .../registerRoutes.php | 24 ++++ .../wpRestFontFamiliesController/base.php | 43 ++++++ .../installFonts.php | 10 +- .../uninstallFonts.php | 12 +- .../registerRoutes.php | 28 ---- 14 files changed, 278 insertions(+), 143 deletions(-) create mode 100644 lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php create mode 100644 lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php rename lib/experimental/fonts/font-library/{class-wp-rest-font-library-controller.php => class-wp-rest-font-families-controller.php} (84%) rename phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/base.php (69%) rename phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/getFontCollection.php (84%) rename phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontCollectionsController}/getFontCollections.php (77%) create mode 100644 phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php create mode 100644 phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php rename phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontFamiliesController}/installFonts.php (98%) rename phpunit/tests/fonts/font-library/{wpRestFontLibraryController => wpRestFontFamiliesController}/uninstallFonts.php (90%) delete mode 100644 phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php diff --git a/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php new file mode 100644 index 00000000000000..0e31bd4004b40f --- /dev/null +++ b/lib/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php @@ -0,0 +1,25 @@ +<?php +/** + * Autosave Rest Font Families Controller. + * + * This file contains the class for the Autosave REST API Font Families Controller. + * + * @package WordPress + * @subpackage Font Library + * @since 6.5.0 + */ + +if ( class_exists( 'WP_REST_Autosave_Font_Families_Controller' ) ) { + return; +} + +/** + * Autosave Font Families Controller class. + * + * @since 6.5.0 + */ +class WP_REST_Autosave_Font_Families_Controller { + public function register_routes() { + // disable autosave endpoints for font families + } +} diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php new file mode 100644 index 00000000000000..2367cba0b870a7 --- /dev/null +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php @@ -0,0 +1,124 @@ +<?php +/** + * Rest Font Collections Controller. + * + * This file contains the class for the REST API Font Collections Controller. + * + * @package WordPress + * @subpackage Font Library + * @since 6.5.0 + */ + +if ( class_exists( 'WP_REST_Font_Collections_Controller' ) ) { + return; +} + +/** + * Font Library Controller class. + * + * @since 6.5.0 + */ +class WP_REST_Font_Collections_Controller extends WP_REST_Controller { + + /** + * Constructor. + * + * @since 6.5.0 + */ + public function __construct() { + $this->rest_base = 'font-collections'; + $this->namespace = 'wp/v2'; + } + + /** + * Registers the routes for the objects of the controller. + * + * @since 6.5.0 + */ + public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_font_collections' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + ) + ); + + register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P<id>[\/\w-]+)', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_font_collection' ), + 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), + ), + ) + ); + } + + /** + * Gets a font collection. + * + * @since 6.5.0 + * + * @param WP_REST_Request $request Full details about the request. + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_font_collection( $request ) { + $id = $request->get_param( 'id' ); + $collection = WP_Font_Library::get_font_collection( $id ); + // If the collection doesn't exist returns a 404. + if ( is_wp_error( $collection ) ) { + $collection->add_data( array( 'status' => 404 ) ); + return $collection; + } + $collection_with_data = $collection->get_data(); + // If there was an error getting the collection data, return the error. + if ( is_wp_error( $collection_with_data ) ) { + $collection_with_data->add_data( array( 'status' => 500 ) ); + return $collection_with_data; + } + return new WP_REST_Response( $collection_with_data ); + } + + /** + * Gets the font collections available. + * + * @since 6.5.0 + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_font_collections() { + $collections = array(); + foreach ( WP_Font_Library::get_font_collections() as $collection ) { + $collections[] = $collection->get_config(); + } + + return new WP_REST_Response( $collections, 200 ); + } + + /** + * Checks whether the user has permissions to update the Font Library. + * + * @since 6.5.0 + * + * @return true|WP_Error True if the request has write access for the item, WP_Error object otherwise. + */ + public function update_font_library_permissions_check() { + if ( ! current_user_can( 'edit_theme_options' ) ) { + return new WP_Error( + 'rest_cannot_update_font_library', + __( 'Sorry, you are not allowed to update the Font Library on this site.', 'gutenberg' ), + array( + 'status' => rest_authorization_required_code(), + ) + ); + } + return true; + } +} diff --git a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php similarity index 84% rename from lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php rename to lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php index 9655178d706679..c92a0d2697f315 100644 --- a/lib/experimental/fonts/font-library/class-wp-rest-font-library-controller.php +++ b/lib/experimental/fonts/font-library/class-wp-rest-font-families-controller.php @@ -1,24 +1,24 @@ <?php /** - * Rest Font Library Controller. + * Rest Font Families Controller. * - * This file contains the class for the REST API Font Library Controller. + * This file contains the class for the REST API Font Families Controller. * * @package WordPress * @subpackage Font Library * @since 6.5.0 */ -if ( class_exists( 'WP_REST_Font_Library_Controller' ) ) { +if ( class_exists( 'WP_REST_Font_Families_Controller' ) ) { return; } /** - * Font Library Controller class. + * Font Families Controller class. * * @since 6.5.0 */ -class WP_REST_Font_Library_Controller extends WP_REST_Controller { +class WP_REST_Font_Families_Controller extends WP_REST_Posts_Controller { /** * Constructor. @@ -26,8 +26,9 @@ class WP_REST_Font_Library_Controller extends WP_REST_Controller { * @since 6.5.0 */ public function __construct() { - $this->rest_base = 'fonts'; + $this->rest_base = 'font-families'; $this->namespace = 'wp/v2'; + $this->post_type = 'wp_font_family'; } /** @@ -36,6 +37,19 @@ public function __construct() { * @since 6.5.0 */ public function register_routes() { + register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => function () { + return true;}, + ), + ) + ); + register_rest_route( $this->namespace, '/' . $this->rest_base, @@ -67,71 +81,6 @@ public function register_routes() { ), ) ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/collections', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collections' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - ), - ) - ); - - register_rest_route( - $this->namespace, - '/' . $this->rest_base . '/collections' . '/(?P<id>[\/\w-]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( $this, 'get_font_collection' ), - 'permission_callback' => array( $this, 'update_font_library_permissions_check' ), - ), - ) - ); - } - - /** - * Gets a font collection. - * - * @since 6.5.0 - * - * @param WP_REST_Request $request Full details about the request. - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_font_collection( $request ) { - $id = $request->get_param( 'id' ); - $collection = WP_Font_Library::get_font_collection( $id ); - // If the collection doesn't exist returns a 404. - if ( is_wp_error( $collection ) ) { - $collection->add_data( array( 'status' => 404 ) ); - return $collection; - } - $collection_with_data = $collection->get_data(); - // If there was an error getting the collection data, return the error. - if ( is_wp_error( $collection_with_data ) ) { - $collection_with_data->add_data( array( 'status' => 500 ) ); - return $collection_with_data; - } - return new WP_REST_Response( $collection_with_data ); - } - - /** - * Gets the font collections available. - * - * @since 6.5.0 - * - * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. - */ - public function get_font_collections() { - $collections = array(); - foreach ( WP_Font_Library::get_font_collections() as $collection ) { - $collections[] = $collection->get_config(); - } - - return new WP_REST_Response( $collections, 200 ); } /** diff --git a/lib/experimental/fonts/font-library/font-library.php b/lib/experimental/fonts/font-library/font-library.php index 6c31c02d409f7a..709f63e9126cbc 100644 --- a/lib/experimental/fonts/font-library/font-library.php +++ b/lib/experimental/fonts/font-library/font-library.php @@ -22,16 +22,19 @@ function gutenberg_init_font_library_routes() { // @core-merge: This code will go into Core's `create_initial_post_types()`. $args = array( - 'public' => false, - '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ - 'label' => 'Font Library', - 'show_in_rest' => true, + 'public' => false, + '_builtin' => true, /* internal use only. don't use this when registering your own post type. */ + 'label' => 'Font Family', + 'show_in_rest' => true, + 'rest_base' => 'font-families', + 'rest_controller_class' => 'WP_REST_Font_Families_Controller', + 'autosave_rest_controller_class' => 'WP_REST_Autosave_Font_Families_Controller', ); register_post_type( 'wp_font_family', $args ); // @core-merge: This code will go into Core's `create_initial_rest_routes()`. - $font_library_controller = new WP_REST_Font_Library_Controller(); - $font_library_controller->register_routes(); + $font_collections_controller = new WP_REST_Font_Collections_Controller(); + $font_collections_controller->register_routes(); } add_action( 'rest_api_init', 'gutenberg_init_font_library_routes' ); diff --git a/lib/load.php b/lib/load.php index ed108f764ada19..94cc87fb419d52 100644 --- a/lib/load.php +++ b/lib/load.php @@ -148,7 +148,9 @@ function gutenberg_is_experiment_enabled( $name ) { require __DIR__ . '/experimental/fonts/font-library/class-wp-font-library.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family-utils.php'; require __DIR__ . '/experimental/fonts/font-library/class-wp-font-family.php'; - require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-library-controller.php'; + require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-families-controller.php'; + require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-font-collections-controller.php'; + require __DIR__ . '/experimental/fonts/font-library/class-wp-rest-autosave-font-families-controller.php'; require __DIR__ . '/experimental/fonts/font-library/font-library.php'; } diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js index 425b3afb0e7c3c..0ab4a7ba742247 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/resolvers.js @@ -9,7 +9,7 @@ import apiFetch from '@wordpress/api-fetch'; export async function fetchInstallFonts( data ) { const config = { - path: '/wp/v2/fonts', + path: '/wp/v2/font-families', method: 'POST', body: data, }; @@ -21,7 +21,7 @@ export async function fetchUninstallFonts( fonts ) { font_families: fonts, }; const config = { - path: '/wp/v2/fonts', + path: '/wp/v2/font-families', method: 'DELETE', data, }; @@ -30,7 +30,7 @@ export async function fetchUninstallFonts( fonts ) { export async function fetchFontCollections() { const config = { - path: '/wp/v2/fonts/collections', + path: '/wp/v2/font-collections', method: 'GET', }; return apiFetch( config ); @@ -38,7 +38,7 @@ export async function fetchFontCollections() { export async function fetchFontCollection( id ) { const config = { - path: `/wp/v2/fonts/collections/${ id }`, + path: `/wp/v2/font-collections/${ id }`, method: 'GET', }; return apiFetch( config ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php similarity index 69% rename from phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php rename to phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php index a6b02f38a5e817..2469d71dc79ce8 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/base.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/base.php @@ -1,11 +1,11 @@ <?php /** - * Test Case for WP_REST_Font_Library_Controller tests. + * Test Case for WP_REST_Font_Collections_Controller tests. * * @package WordPress * @subpackage Font Library */ -abstract class WP_REST_Font_Library_Controller_UnitTestCase extends WP_UnitTestCase { +abstract class WP_REST_Font_Collections_Controller_UnitTestCase extends WP_UnitTestCase { /** * Fonts directory. @@ -18,8 +18,6 @@ abstract class WP_REST_Font_Library_Controller_UnitTestCase extends WP_UnitTestC public function set_up() { parent::set_up(); - static::$fonts_dir = WP_Font_Library::get_fonts_dir(); - // Create a user with administrator role. $admin_id = $this->factory->user->create( array( @@ -40,10 +38,5 @@ public function tear_down() { $property = $reflection->getProperty( 'collections' ); $property->setAccessible( true ); $property->setValue( null, array() ); - - // Clean up the /fonts directory. - foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { - @unlink( $file ); - } } } diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php similarity index 84% rename from phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php rename to phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php index f1a0a6a0cd510c..94e7daaa166345 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollection.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollection.php @@ -1,6 +1,6 @@ <?php /** - * Test WP_REST_Font_Library_Controller::get_font_collection(). + * Test WP_REST_Font_Collections_Controller::get_font_collection(). * * @package WordPress * @subpackage Font Library @@ -8,10 +8,10 @@ * @group fonts * @group font-library * - * @covers WP_REST_Font_Library_Controller::get_font_collection + * @covers WP_REST_Font_Collections_Controller::get_font_collection */ -class Tests_Fonts_WPRESTFontLibraryController_GetFontCollection extends WP_REST_Font_Library_Controller_UnitTestCase { +class Tests_Fonts_WPRESTFontCollectionsController_GetFontCollection extends WP_REST_Font_Collections_Controller_UnitTestCase { /** * Register mock collections. @@ -90,7 +90,7 @@ public function tear_down() { } public function test_get_font_collection_from_file() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/one-collection' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/one-collection' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); @@ -99,7 +99,7 @@ public function test_get_font_collection_from_file() { } public function test_get_font_collection_from_url() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-url' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-url' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); @@ -107,19 +107,19 @@ public function test_get_font_collection_from_url() { } public function test_get_non_existing_collection_should_return_404() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/non-existing-collection-id' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/non-existing-collection-id' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 404, $response->get_status() ); } public function test_get_non_existing_file_should_return_500() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-non-existing-file' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-file' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 500, $response->get_status() ); } public function test_get_non_existing_url_should_return_500() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections/collection-with-non-existing-url' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/collection-with-non-existing-url' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 500, $response->get_status() ); } diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php similarity index 77% rename from phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php rename to phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php index ad120ee36fce4d..224dab07cf0b7a 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/getFontCollections.php +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/getFontCollections.php @@ -1,6 +1,6 @@ <?php /** - * Test WP_REST_Font_Library_Controller::get_font_collections(). + * Test WP_REST_Font_Collections_Controller::get_font_collections(). * * @package WordPress * @subpackage Font Library @@ -8,13 +8,13 @@ * @group fonts * @group font-library * - * @covers WP_REST_Font_Library_Controller::get_font_collections + * @covers WP_REST_Font_Collections_Controller::get_font_collections */ -class Tests_Fonts_WPRESTFontLibraryController_GetFontCollections extends WP_REST_Font_Library_Controller_UnitTestCase { +class Tests_Fonts_WPRESTFontCollectionsController_GetFontCollections extends WP_REST_Font_Collections_Controller_UnitTestCase { public function test_get_font_collections_with_no_collection_registered() { - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $this->assertSame( array(), $response->get_data() ); @@ -34,7 +34,7 @@ public function test_get_font_collections() { ); wp_register_font_collection( $config ); - $request = new WP_REST_Request( 'GET', '/wp/v2/fonts/collections' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php new file mode 100644 index 00000000000000..c2c019fa70a022 --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpRestFontCollectionsController/registerRoutes.php @@ -0,0 +1,24 @@ +<?php +/** + * Test WP_REST_Font_Collections_Controller::register_routes(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_REST_Font_Collections_Controller::register_routes + */ + +class Tests_Fonts_WPRESTFontCollectionsController_RegisterRoutes extends WP_UnitTestCase { + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P<id>[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); + + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P<id>[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php new file mode 100644 index 00000000000000..5ab71a4379851f --- /dev/null +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/base.php @@ -0,0 +1,43 @@ +<?php +/** + * Test Case for WP_REST_Font_Families_Controller tests. + * + * @package WordPress + * @subpackage Font Library + */ +abstract class WP_REST_Font_Families_Controller_UnitTestCase extends WP_UnitTestCase { + + /** + * Fonts directory. + * + * @var string + */ + protected static $fonts_dir; + + + public function set_up() { + parent::set_up(); + + static::$fonts_dir = WP_Font_Library::get_fonts_dir(); + + // Create a user with administrator role. + $admin_id = $this->factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( $admin_id ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + parent::tear_down(); + + // Clean up the /fonts directory. + foreach ( $this->files_in_dir( static::$fonts_dir ) as $file ) { + @unlink( $file ); + } + } +} diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php similarity index 98% rename from phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php rename to phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php index 01ac1ff8436ed7..d35022306f4e6f 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/installFonts.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/installFonts.php @@ -1,6 +1,6 @@ <?php /** - * Test WP_REST_Font_Library_Controller::install_fonts(). + * Test WP_REST_Font_Families_Controller::install_fonts(). * * @package WordPress * @subpackage Font Library @@ -8,10 +8,10 @@ * @group fonts * @group font-library * - * @covers WP_REST_Font_Library_Controller::install_fonts + * @covers WP_REST_Font_Families_Controller::install_fonts */ -class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_Library_Controller_UnitTestCase { +class Tests_Fonts_WPRESTFontFamiliesController_InstallFonts extends WP_REST_Font_Families_Controller_UnitTestCase { /** * @@ -22,7 +22,7 @@ class Tests_Fonts_WPRESTFontLibraryController_InstallFonts extends WP_REST_Font_ * @param array $expected_response Expected response data. */ public function test_install_fonts( $font_families, $files, $expected_response ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); + $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); $font_families_json = json_encode( $font_families ); $install_request->set_param( 'font_families', $font_families_json ); $install_request->set_file_params( $files ); @@ -307,7 +307,7 @@ public function data_install_fonts() { * @param array $files Font files to install. */ public function test_install_with_improper_inputs( $font_families, $files = array() ) { - $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); + $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); $font_families_json = json_encode( $font_families ); $install_request->set_param( 'font_families', $font_families_json ); $install_request->set_file_params( $files ); diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php similarity index 90% rename from phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php rename to phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php index a3b613e6f983e0..241f26284fe5d2 100644 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/uninstallFonts.php +++ b/phpunit/tests/fonts/font-library/wpRestFontFamiliesController/uninstallFonts.php @@ -1,6 +1,6 @@ <?php /** - * Test WP_REST_Font_Library_Controller::install_fonts(). + * Test WP_REST_Font_Families_Controller::install_fonts(). * * @package WordPress * @subpackage Font Library @@ -8,10 +8,10 @@ * @group fonts * @group font-library * - * @covers WP_REST_Font_Library_Controller::install_fonts + * @covers WP_REST_Font_Families_Controller::install_fonts */ -class Tests_Fonts_WPRESTFontLibraryController_UninstallFonts extends WP_REST_Font_Library_Controller_UnitTestCase { +class Tests_Fonts_WPRESTFontFamiliesController_UninstallFonts extends WP_REST_Font_Families_Controller_UnitTestCase { /** * Install fonts to test uninstall. @@ -51,7 +51,7 @@ public function set_up() { ), ); - $install_request = new WP_REST_Request( 'POST', '/wp/v2/fonts' ); + $install_request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); $font_families_json = json_encode( $mock_families ); $install_request->set_param( 'font_families', $font_families_json ); rest_get_server()->dispatch( $install_request ); @@ -67,7 +67,7 @@ public function test_uninstall() { ), ); - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families' ); $uninstall_request->set_param( 'font_families', $font_families_to_uninstall ); $response = rest_get_server()->dispatch( $uninstall_request ); $this->assertSame( 200, $response->get_status(), 'The response status is not 200.' ); @@ -75,7 +75,7 @@ public function test_uninstall() { public function test_uninstall_non_existing_fonts() { - $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/fonts' ); + $uninstall_request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families' ); $non_existing_font_data = array( array( diff --git a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php b/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php deleted file mode 100644 index 2ac7b93c3a4141..00000000000000 --- a/phpunit/tests/fonts/font-library/wpRestFontLibraryController/registerRoutes.php +++ /dev/null @@ -1,28 +0,0 @@ -<?php -/** - * Test WP_REST_Font_Library_Controller::register_routes(). - * - * @package WordPress - * @subpackage Font Library - * - * @group fonts - * @group font-library - * - * @covers WP_REST_Font_Library_Controller::register_routes - */ - -class Tests_Fonts_WPRESTFontLibraryController_RegisterRoutes extends WP_UnitTestCase { - - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( '/wp/v2/fonts', $routes, 'Rest server has not the fonts path intialized.' ); - $this->assertCount( 2, $routes['/wp/v2/fonts'], 'Rest server has not the 2 fonts paths initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/fonts/collections'], 'Rest server has not the collections path initialized.' ); - $this->assertCount( 1, $routes['/wp/v2/fonts/collections/(?P<id>[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); - - $this->assertArrayHasKey( 'POST', $routes['/wp/v2/fonts'][0]['methods'], 'Rest server has not the POST method for fonts intialized.' ); - $this->assertArrayHasKey( 'DELETE', $routes['/wp/v2/fonts'][1]['methods'], 'Rest server has not the DELETE method for fonts intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections'][0]['methods'], 'Rest server has not the GET method for collections intialized.' ); - $this->assertArrayHasKey( 'GET', $routes['/wp/v2/fonts/collections/(?P<id>[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection intialized.' ); - } -} From 6e9c8565c225670fdcab44230bf00bcc7c320877 Mon Sep 17 00:00:00 2001 From: Joen A <1204802+jasmussen@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:48:19 +0100 Subject: [PATCH 323/325] Text selection: show CSS hack to Safari only. (#57300) --- .../src/components/block-list/content.scss | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/block-editor/src/components/block-list/content.scss b/packages/block-editor/src/components/block-list/content.scss index 3e87ba317d1350..d907ffd81d9dd0 100644 --- a/packages/block-editor/src/components/block-list/content.scss +++ b/packages/block-editor/src/components/block-list/content.scss @@ -17,6 +17,20 @@ } } + +// Hide selections on this element, otherwise Safari will include it stacked +// under your actual selection. +// This uses a CSS hack to show the rules to Safari only. Failing here is okay, +// it just makes the selection indication slightly less precise. That makes this +// hack a progressive enhancement. Stylelint is disabled to allow the hack to work. +/* stylelint-disable */ +_::-webkit-full-page-media, _:future, :root .block-editor-block-list__layout::selection, +_::-webkit-full-page-media, _:future, :root .has-multi-selection .block-editor-block-list__layout::selection { + background-color: transparent; +} +/* stylelint-enable */ + + // Note to developers refactoring this, please test navigation mode, and // multi selection and hovering the block switcher to highlight the block. // Also be sure to test partial selections in Safari, as it draws the @@ -24,16 +38,6 @@ .block-editor-block-list__layout { position: relative; - // Hide selections on this element, otherwise Safari will include it stacked - // under your actual selection. - &::selection { - background: transparent; - } - - .has-multi-selection &::selection { - background: transparent; - } - // Block multi selection // Apply a rounded radius to the entire block when multi selected, but with low specificity // so explicit radii set by tools are preserved. From 90c9ab3daff137ae3319c6db8beb1ecd3e344b49 Mon Sep 17 00:00:00 2001 From: Lena Morita <lena@jaguchi.com> Date: Thu, 21 Dec 2023 23:52:39 +0900 Subject: [PATCH 324/325] Update TypeScript-related tips in Contributing guide (#57267) --- packages/components/CONTRIBUTING.md | 162 ++++++++++------------------ 1 file changed, 57 insertions(+), 105 deletions(-) diff --git a/packages/components/CONTRIBUTING.md b/packages/components/CONTRIBUTING.md index 3d682c584f383d..825ce9eb506dfa 100644 --- a/packages/components/CONTRIBUTING.md +++ b/packages/components/CONTRIBUTING.md @@ -19,7 +19,6 @@ For an example of a component that follows these requirements, take a look at [` - [Documentation](#documentation) - [README example](#README-example) - [Folder structure](#folder-structure) -- [TypeScript migration guide](#refactoring-a-component-to-typescript) - [Using Radix UI primitives](#using-radix-ui-primitives) ## Introducing new components @@ -239,7 +238,63 @@ TDB --> ## TypeScript -We strongly encourage using TypeScript for all new components. Components should be typed using the `WordPressComponent` type. +We strongly encourage using TypeScript for all new components. + +Extend existing components’ props if possible, especially when a component internally forwards its props to another component in the package: + +```ts +type NumberControlProps = Omit< + InputControlProps, + 'isDragEnabled' | 'min' | 'max' +> & { + /* Additional props specific to NumberControl */ +}; +``` + +Use JSDocs syntax for each TypeScript property that is part of the public API of a component. The docs used here should be aligned with the component’s README. Add `@default` values where appropriate: + +```ts +/** + * Renders with elevation styles (box shadow). + * + * @default false + * @deprecated + */ +isElevated?: boolean; +``` + +Prefer `unknown` to `any`, and in general avoid it when possible. + +If the component forwards its `...restProps` to an underlying element/component, you should use the `WordPressComponentProps` type for the component's props: + +```ts +import type { WordPressComponentProps } from '../context'; +import type { ComponentOwnProps } from './types'; + +function UnconnectedMyComponent( + // The resulting type will include: + // - all props defined in `ComponentOwnProps` + // - all HTML props/attributes from the component specified as the second + // parameter (`div` in this example) + // - the special `as` prop (which marks the component as polymorphic), + // unless the third parameter is `false` + props: WordPressComponentProps< ComponentOwnProps, 'div', true > +) { /* ... */ } +``` + +### Considerations for the docgen + +Make sure you have a **named** export for the component, not just the default export ([example](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/divider/component.tsx)). This ensures that the docgen can properly extract the types data. The naming should be so that the connected/forwarded component has the plain component name (`MyComponent`), and the raw component is prefixed (`UnconnectedMyComponent` or `UnforwardedMyComponent`). This makes the component's `displayName` look nicer in React devtools and in the autogenerated Storybook code snippets. + +```js +function UnconnectedMyComponent() { /* ... */ } + +// 👇 Without this named export, the docgen will not work! +export const MyComponent = contextConnect( UnconnectedMyComponent, 'MyComponent' ); +export default MyComponent; +``` + +On the component's main named export, add a JSDoc comment that includes the main description and the example code snippet from the README ([example](https://github.com/WordPress/gutenberg/blob/43d9c82922619c1d1ff6b454f86f75c3157d3de6/packages/components/src/date-time/date-time/index.tsx#L193-L217)). _At the time of writing, the `@example` JSDoc keyword is not recognized by StoryBook's docgen, so please avoid using it_. <!-- TODO: add to the previous paragraph once the composision section gets added to this document. (more details about polymorphism can be found above in the "Components composition" section). --> @@ -538,109 +593,6 @@ component-family-name/ └── utils.ts ``` -## Refactoring a component to TypeScript - -*Note: This section assumes that the local developer environment is set up correctly, including TypeScript linting. We also strongly recommend using an IDE that supports TypeScript.* - -Given a component folder (e.g. `packages/components/src/unit-control`): - -1. Remove the folder from the exclude list in `tsconfig.json`, if it isn’t already. -2. Remove any `// @ts-nocheck` comments in the folder, if any. -3. Rename `*.js{x}` files to `*.ts{x}` (except stories and unit tests). -4. Run `npm run dev` and take note of all the errors (your IDE should also flag them). -5. Since we want to focus on one component’s folder at the time, if any errors are coming from files outside of the folder that is being refactored, there are two potential approaches: - 1. Following those same guidelines, refactor those dependencies first. - 1. Ideally, start from the “leaf” of the dependency tree and slowly work your way up the chain. - 2. Resume work on this component once all dependencies have been refactored. - 2. Alternatively: - 1. For each of those files, add `// @ts-nocheck` at the start of the file. - 2. If the components in the ignored files are destructuring props directly from the function's arguments, move the props destructuring to the function's body (this is to avoid TypeScript errors from trying to infer the props type): - - ```jsx - // Before: - function MyComponent( { myProp1, myProp2, ...restProps } ) { /* ... */ } - - // After: - function MyComponent( props ) { - const { myProp1, myProp2, ...restProps } = props; - - /* ... */ - } - ``` - - 3. Remove the folders from the exclude list in the `tsconfig.json` file. - 4. If you’re still getting errors about a component’s props, the easiest way is to slightly refactor this component and perform the props destructuring inside the component’s body (as opposed as in the function signature) — this is to prevent TypeScript from inferring the types of these props. - 5. Continue with the refactor of the current component (and take care of the refactor of the dependent components at a later stage). -6. Create a new `types.ts` file. -7. Slowly work your way through fixing the TypeScript errors in the folder: - 1. Try to avoid introducing any runtime changes, if possible. The aim of this refactor is to simply rewrite the component to TypeScript. - 2. Extract props to `types.ts`, and use them to type components. The README can be of help when determining a prop’s type. - 3. Use existing HTML types when possible? (e.g. `required` for an input field?) - 4. Use the `CSSProperties` type where it makes sense. - 5. Extend existing components’ props if possible, especially when a component internally forwards its props to another component in the package. - 6. If the component forwards its `...restProps` to an underlying element/component, you should use the `WordPressComponentProps` type for the component's props: - - ```tsx - import type { WordPressComponentProps } from '../context'; - import type { ComponentOwnProps } from './types'; - - function UnconnectedMyComponent( - // The resulting type will include: - // - all props defined in `ComponentOwnProps` - // - all HTML props/attributes from the component specified as the second - // parameter (`div` in this example) - // - the special `as` prop (which marks the component as polymorphic), - // unless the third parameter is `false` - props: WordPressComponentProps< ComponentOwnProps, 'div', true > - ) { /* ... */ } - ``` - - 7. As shown in the previous examples, make sure you have a **named** export for the component, not just the default export ([example](https://github.com/WordPress/gutenberg/blob/trunk/packages/components/src/divider/component.tsx)). This ensures that the docgen can properly extract the types data. The naming should be so that the connected/forwarded component has the plain component name (`MyComponent`), and the raw component is prefixed (`UnconnectedMyComponent` or `UnforwardedMyComponent`). This makes the component's `displayName` look nicer in React devtools and in the autogenerated Storybook code snippets. - - ```jsx - function UnconnectedMyComponent() { /* ... */ } - - // 👇 Without this named export, the docgen will not work! - export const MyComponent = contextConnect( UnconnectedMyComponent, 'MyComponent' ); - export default MyComponent; - ``` - - 8. Use JSDocs syntax for each TypeScript property that is part of the public API of a component. The docs used here should be aligned with the component’s README. Add `@default` values where appropriate. - 9. Prefer `unknown` to `any`, and in general avoid it when possible. -8. On the component's main named export, add a JSDoc comment that includes the main description and the example code snippet from the README ([example](https://github.com/WordPress/gutenberg/blob/43d9c82922619c1d1ff6b454f86f75c3157d3de6/packages/components/src/date-time/date-time/index.tsx#L193-L217)). _At the time of writing, the `@example` JSDoc keyword is not recognized by StoryBook's docgen, so please avoid using it_. -9. Make sure that: - 1. tests still pass; - 2. storybook examples work as expected. - 3. the component still works as expected in its usage in Gutenberg; - 4. the JSDocs comments on `types.ts` and README docs are aligned. -10. Convert Storybook examples to TypeScript (and from knobs to controls, if necessary) ([example](https://github.com/WordPress/gutenberg/pull/39320)). - 1. Update all consumers of the component to potentially extend the newly added types (e.g. make `UnitControl` props extend `NumberControl` props after `NumberControl` types are made available). - 2. Rename Story extension from `.js` to `.tsx`. - 3. Rewrite the `meta` story object, and export it as default. In particular, make sure you add the following settings under the `parameters` key: - - ```tsx - const meta: Meta< typeof MyComponent > = { - parameters: { - controls: { expanded: true }, - docs: { canvas: { sourceState: 'shown' } }, - }, - }; - ``` - - These options will display prop descriptions in the `Canvas ▸ Controls` tab, and expand code snippets in the `Docs` tab. - - 4. Go to the component in Storybook and check the props table in the Docs tab. If there are props that shouldn't be there, check that your types are correct, or consider `Omit`-ing props that shouldn't be exposed. - 1. Use the `parameters.controls.exclude` property on the `meta` object to hide props from the docs. - 2. Use the `argTypes` prop on the `meta` object to customize how each prop in the docs can be interactively controlled by the user (tip: use `control: { type: null }` to remove the interactive controls from a prop, without hiding the prop from the docs). - 3. See the [official docs](https://storybook.js.org/docs/react/essentials/controls) for more details. - 5. Comment out all existing stories. - 6. Create a default template, where the component is being used in the most “vanilla” way possible. - 7. Use the template for the `Default` story, which will serve as an interactive doc playground. - 8. Add more focused stories as you see fit. These non-default stories should illustrate specific scenarios and usages of the component. A developer looking at the Docs tab should be able to understand what each story is demonstrating. Add JSDoc comments to stories when necessary. -11. Convert unit tests. - 1. Rename test file extensions from `.js` to `.tsx`. - 2. Fix all TypeScript errors. - ## Using Radix UI primitives Useful links: From 5f4c7fe6e8dcaa6d1c3810ac1aa41cf882b28978 Mon Sep 17 00:00:00 2001 From: Jon Surrell <sirreal@users.noreply.github.com> Date: Thu, 21 Dec 2023 16:00:05 +0100 Subject: [PATCH 325/325] Add missing block/theme json $schema property (#57201) * Add missing block.json $schema properties * Add missing theme.json $schema * Fix schema violations for phpunit fixtures --- lib/theme.json | 1 + packages/e2e-tests/plugins/iframed-block/block.json | 1 + packages/e2e-tests/plugins/iframed-inline-styles/block.json | 1 + packages/e2e-tests/plugins/iframed-masonry-block/block.json | 1 + .../e2e-tests/plugins/iframed-multiple-stylesheets/block.json | 1 + .../plugins/interactive-blocks/directive-bind/block.json | 1 + .../plugins/interactive-blocks/directive-body/block.json | 1 + .../plugins/interactive-blocks/directive-class/block.json | 1 + .../plugins/interactive-blocks/directive-context/block.json | 1 + .../plugins/interactive-blocks/directive-init/block.json | 1 + .../plugins/interactive-blocks/directive-key/block.json | 1 + .../plugins/interactive-blocks/directive-on/block.json | 1 + .../plugins/interactive-blocks/directive-priorities/block.json | 1 + .../plugins/interactive-blocks/directive-slots/block.json | 1 + .../plugins/interactive-blocks/directive-style/block.json | 1 + .../plugins/interactive-blocks/directive-text/block.json | 1 + .../plugins/interactive-blocks/directive-watch/block.json | 1 + .../plugins/interactive-blocks/negation-operator/block.json | 1 + .../plugins/interactive-blocks/router-navigate/block.json | 1 + .../plugins/interactive-blocks/router-regions/block.json | 1 + .../e2e-tests/plugins/interactive-blocks/store-tag/block.json | 1 + .../plugins/interactive-blocks/tovdom-islands/block.json | 1 + .../e2e-tests/plugins/interactive-blocks/tovdom/block.json | 1 + packages/edit-widgets/src/blocks/widget-area/block.json | 1 + packages/widgets/src/blocks/legacy-widget/block.json | 1 + packages/widgets/src/blocks/widget-group/block.json | 1 + .../themedir1/block-theme-child-with-fluid-layout/theme.json | 1 + .../block-theme-child-with-fluid-typography-config/theme.json | 1 + .../block-theme-child-with-fluid-typography/theme.json | 1 + phpunit/data/themedir1/block-theme-child/theme.json | 1 + phpunit/data/themedir1/block-theme/theme.json | 1 + phpunit/data/themedir1/fonts-block-theme/theme.json | 1 + phpunit/fixtures/block.json | 3 ++- phpunit/fixtures/hooked-block/block.json | 2 ++ test/gutenberg-test-themes/style-variations/theme.json | 1 + 35 files changed, 37 insertions(+), 1 deletion(-) diff --git a/lib/theme.json b/lib/theme.json index 671dd50227852b..c2ed7fdca39ed5 100644 --- a/lib/theme.json +++ b/lib/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "appearanceTools": false, diff --git a/packages/e2e-tests/plugins/iframed-block/block.json b/packages/e2e-tests/plugins/iframed-block/block.json index 85f86dea8131d9..0665636a253af2 100644 --- a/packages/e2e-tests/plugins/iframed-block/block.json +++ b/packages/e2e-tests/plugins/iframed-block/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "test/iframed-block", "title": "Iframed Block", diff --git a/packages/e2e-tests/plugins/iframed-inline-styles/block.json b/packages/e2e-tests/plugins/iframed-inline-styles/block.json index 293b5af5e971f4..1231f1d33404bc 100644 --- a/packages/e2e-tests/plugins/iframed-inline-styles/block.json +++ b/packages/e2e-tests/plugins/iframed-inline-styles/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "test/iframed-inline-styles", "title": "Iframed Inline Styles", diff --git a/packages/e2e-tests/plugins/iframed-masonry-block/block.json b/packages/e2e-tests/plugins/iframed-masonry-block/block.json index b9b3f17234e30d..a77600805752cd 100644 --- a/packages/e2e-tests/plugins/iframed-masonry-block/block.json +++ b/packages/e2e-tests/plugins/iframed-masonry-block/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "test/iframed-masonry-block", "title": "Iframed Masonry Block", diff --git a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json index 4db03f471177d7..82d60867af99e2 100644 --- a/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json +++ b/packages/e2e-tests/plugins/iframed-multiple-stylesheets/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "test/iframed-multiple-stylesheets", "title": "Iframed Multiple Stylesheets", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/block.json index f0775cecd8ae69..ed7df314224623 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-bind/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-bind/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-bind", "title": "E2E Interactivity tests - directive bind", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json index 61b48396be08ae..44ae61f90d10b8 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-body/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-body", "title": "E2E Interactivity tests - directive body", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-class/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-class/block.json index af2764db986919..ed38c4c4cc899a 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-class/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-class/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-class", "title": "E2E Interactivity tests - directive class", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-context/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-context/block.json index 1b3c448cc62aac..4936761deeb5ed 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-context/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-context/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-context", "title": "E2E Interactivity tests - directive context", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json index a7e195d2e4884a..852269a49ce5b2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-init/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-init", "title": "E2E Interactivity tests - directive init", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json index 0cbdd065e63a1d..4a15051f8f5f60 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-key/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-key", "title": "E2E Interactivity tests - directive key", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json index b9d8aa5f9ce57d..bb7c1abf48717b 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-on/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-on", "title": "E2E Interactivity tests - directive on", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/block.json index c7361c3d5f121a..12b107956f0c6a 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-priorities/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-priorities", "title": "E2E Interactivity tests - directive priorities", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json index f79f89a6e81b8a..6139c4e0620409 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-slots/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-slots", "title": "E2E Interactivity tests - directive slots", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-style/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-style/block.json index 6cbfa57b0784f1..59e9c39f9e01e2 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-style/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-style/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-style", "title": "E2E Interactivity tests - directive style", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-text/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-text/block.json index 7295849b9912d7..a64eb3fdc9161c 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-text/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-text/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-text", "title": "E2E Interactivity tests - directive text", diff --git a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/block.json b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/block.json index 89285837ede3e4..a99d2e9df8a5a9 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/directive-watch/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/directive-watch/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/directive-watch", "title": "E2E Interactivity tests - directive watch", diff --git a/packages/e2e-tests/plugins/interactive-blocks/negation-operator/block.json b/packages/e2e-tests/plugins/interactive-blocks/negation-operator/block.json index 68da53367ad63a..7f1aae30a70693 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/negation-operator/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/negation-operator/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/negation-operator", "title": "E2E Interactivity tests - negation operator", diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json index bdb5a19e030624..f602917587461c 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/router-navigate/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/router-navigate", "title": "E2E Interactivity tests - router navigate", diff --git a/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json b/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json index 44cc260d87d3f6..30776eacad7517 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/router-regions/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/router-regions", "title": "E2E Interactivity tests - router regions", diff --git a/packages/e2e-tests/plugins/interactive-blocks/store-tag/block.json b/packages/e2e-tests/plugins/interactive-blocks/store-tag/block.json index 4611288de796c9..cdf595f74c6c66 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/store-tag/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/store-tag/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/store-tag", "title": "E2E Interactivity tests - store tag", diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/block.json b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/block.json index fb852acefb9116..b06aa2dda95a1c 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom-islands/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/tovdom-islands", "title": "E2E Interactivity tests - tovdom islands", diff --git a/packages/e2e-tests/plugins/interactive-blocks/tovdom/block.json b/packages/e2e-tests/plugins/interactive-blocks/tovdom/block.json index b685919e164821..b46017039375f4 100644 --- a/packages/e2e-tests/plugins/interactive-blocks/tovdom/block.json +++ b/packages/e2e-tests/plugins/interactive-blocks/tovdom/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 2, "name": "test/tovdom", "title": "E2E Interactivity tests - tovdom", diff --git a/packages/edit-widgets/src/blocks/widget-area/block.json b/packages/edit-widgets/src/blocks/widget-area/block.json index 448bf1e8e7357c..040cf9e5055aa2 100644 --- a/packages/edit-widgets/src/blocks/widget-area/block.json +++ b/packages/edit-widgets/src/blocks/widget-area/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "name": "core/widget-area", "category": "widgets", "attributes": { diff --git a/packages/widgets/src/blocks/legacy-widget/block.json b/packages/widgets/src/blocks/legacy-widget/block.json index 6b0c1e2a916fdd..a03eb090633fc7 100644 --- a/packages/widgets/src/blocks/legacy-widget/block.json +++ b/packages/widgets/src/blocks/legacy-widget/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "core/legacy-widget", "title": "Legacy Widget", diff --git a/packages/widgets/src/blocks/widget-group/block.json b/packages/widgets/src/blocks/widget-group/block.json index c29e811554ac11..0e59e58aca2248 100644 --- a/packages/widgets/src/blocks/widget-group/block.json +++ b/packages/widgets/src/blocks/widget-group/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "core/widget-group", "category": "widgets", diff --git a/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json b/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json index 6985da16c60636..710ec336df70b2 100644 --- a/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json +++ b/phpunit/data/themedir1/block-theme-child-with-fluid-layout/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "appearanceTools": true, diff --git a/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json index 65ed480f20e166..dcd3745f1630cc 100644 --- a/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json +++ b/phpunit/data/themedir1/block-theme-child-with-fluid-typography-config/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "appearanceTools": true, diff --git a/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json b/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json index 93234766eddd2a..7b345242702956 100644 --- a/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json +++ b/phpunit/data/themedir1/block-theme-child-with-fluid-typography/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "appearanceTools": true, diff --git a/phpunit/data/themedir1/block-theme-child/theme.json b/phpunit/data/themedir1/block-theme-child/theme.json index ebfa68d9f74811..1157fa91280303 100644 --- a/phpunit/data/themedir1/block-theme-child/theme.json +++ b/phpunit/data/themedir1/block-theme-child/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "color": { diff --git a/phpunit/data/themedir1/block-theme/theme.json b/phpunit/data/themedir1/block-theme/theme.json index aa9f93a3c0aa1a..78c7ff4768cf60 100644 --- a/phpunit/data/themedir1/block-theme/theme.json +++ b/phpunit/data/themedir1/block-theme/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "color": { diff --git a/phpunit/data/themedir1/fonts-block-theme/theme.json b/phpunit/data/themedir1/fonts-block-theme/theme.json index a8212b79e139dd..a5d40da2b5bb2e 100644 --- a/phpunit/data/themedir1/fonts-block-theme/theme.json +++ b/phpunit/data/themedir1/fonts-block-theme/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "settings": { "appearanceTools": true, diff --git a/phpunit/fixtures/block.json b/phpunit/fixtures/block.json index 2ab91788dc0d43..f4ef1b313c4e5f 100644 --- a/phpunit/fixtures/block.json +++ b/phpunit/fixtures/block.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "my-plugin/notice", "title": "Notice", @@ -11,7 +12,7 @@ "icon": "star", "description": "Shows warning, error or success notices…", "keywords": [ "alert", "message" ], - "textDomain": "my-plugin", + "textdomain": "my-plugin", "attributes": { "message": { "type": "string", diff --git a/phpunit/fixtures/hooked-block/block.json b/phpunit/fixtures/hooked-block/block.json index 0d97cf8b8bac61..791bc7b0a7dba5 100644 --- a/phpunit/fixtures/hooked-block/block.json +++ b/phpunit/fixtures/hooked-block/block.json @@ -1,4 +1,6 @@ { + "$schema": "https://schemas.wp.org/trunk/block.json", + "title": "Hooked Block", "name": "tests/hooked-block", "blockHooks": { "tests/group-first-child": "firstChild", diff --git a/test/gutenberg-test-themes/style-variations/theme.json b/test/gutenberg-test-themes/style-variations/theme.json index 5fa12e236592d8..f0fc1ae54042a2 100644 --- a/test/gutenberg-test-themes/style-variations/theme.json +++ b/test/gutenberg-test-themes/style-variations/theme.json @@ -1,4 +1,5 @@ { + "$schema": "https://schemas.wp.org/trunk/theme.json", "version": 2, "styles": { "color": {