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? 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/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() ); 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/package-lock.json b/package-lock.json index 80a3f384e808ed..e4b4aeb47f6bad 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", @@ -56153,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", @@ -56166,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" @@ -56177,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": { @@ -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/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", 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 = (
{ blockEdit } @@ -221,7 +225,7 @@ function BlockListBlock( { isTemporarilyEditingAsBlocks, }, dataAlign && themeSupportsLayout && `align${ dataAlign }`, - className + ! ( dataAlign && isSticky ) && className ), wrapperProps: restWrapperProps, isAligned, 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 ( 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 ) { ); } + +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 && ( ) } { !! settings?.spacing?.margin && ( ) } ); } +export const DimensionsPanel = pure( DimensionsPanelPure ); + /** * @deprecated */ 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 ); 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' && ( <> - - - - - + + + + + ) } 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 ) 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(); 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. 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' ); 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 /> } 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( 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; + } ); } } ); } 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 } 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 def29b6523139d..88dc99374afbeb 100644 --- a/packages/env/lib/commands/run.js +++ b/packages/env/lib/commands/run.js @@ -74,10 +74,13 @@ 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 = [ + 'compose', '-f', config.dockerComposeConfigPath, 'exec', @@ -98,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/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 feb93a2d1f8956..a2422d1a2a049e 100644 Binary files a/packages/interactivity/docs/assets/state-directives.png and b/packages/interactivity/docs/assets/state-directives.png differ diff --git a/packages/interactivity/docs/assets/store-server-client.png b/packages/interactivity/docs/assets/store-server-client.png index 089268cdc7d9c7..37818e37faa3dc 100644 Binary files a/packages/interactivity/docs/assets/store-server-client.png and b/packages/interactivity/docs/assets/store-server-client.png differ 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, 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", 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" ], 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(); } ); } ); 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',