diff --git a/changelog.txt b/changelog.txt index c18a18eaf30dc..e86e1129ecb9e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,7 +1,6 @@ == Changelog == -= 18.0.0-rc.1 = - += 18.0.0 = ## Changelog @@ -81,6 +80,10 @@ - Template Parts: Fix typo in translatable string. ([59816](https://github.com/WordPress/gutenberg/pull/59816)) - Ensure consistent return type in `WP_Navigation_Block_Renderer::Get_markup_for_inner_block()`. ([59820](https://github.com/WordPress/gutenberg/pull/59820)) - Return early from saving meta data for the navigation without a $post->ID. ([59875](https://github.com/WordPress/gutenberg/pull/59875)) +- Fix root ID calculation when check if block can be transformed. ([60167](https://github.com/WordPress/gutenberg/pull/60167)) +- Featured Image: Fix overlay rendering in the editor. ([60187](https://github.com/WordPress/gutenberg/pull/60187)) +- Fix self closing navigation overlay. ([60130](https://github.com/WordPress/gutenberg/pull/60130)) +- Navigation: Avoid content loss when only specific entity fields are edited. ([60071](https://github.com/WordPress/gutenberg/pull/60071)) #### Font Library - Avoid auto-removing font families without font faces. ([59910](https://github.com/WordPress/gutenberg/pull/59910)) @@ -90,6 +93,8 @@ - Polish Google Fonts consent box. ([59631](https://github.com/WordPress/gutenberg/pull/59631)) - Refactors the upload handler in order to check if files being uploaded are valid font files. ([59648](https://github.com/WordPress/gutenberg/pull/59648)) - Reset notices when navigating away from the collection. ([59981](https://github.com/WordPress/gutenberg/pull/59981)) +- Activate the fonts coming from the backend and not the data from the frontend. ([60093](https://github.com/WordPress/gutenberg/pull/60093)) +- Install fonts in sequence to work around race condition. ([60180](https://github.com/WordPress/gutenberg/pull/60180)) #### Interactivity API - Backport fixes from Core. ([59903](https://github.com/WordPress/gutenberg/pull/59903)) @@ -122,6 +127,8 @@ - Remove filter for same number of settings. ([59590](https://github.com/WordPress/gutenberg/pull/59590)) - Site editor: Find font families for typography presets crashes editor. ([59806](https://github.com/WordPress/gutenberg/pull/59806)) - Force root min-height of 100% for backgrounds. ([59809](https://github.com/WordPress/gutenberg/pull/59809)) +- Featured Image: Fix block support selectors after shadow support addition. ([60184](https://github.com/WordPress/gutenberg/pull/60184)) +- Fix list of base theme fonts when a theme variation is applied.. ([59959](https://github.com/WordPress/gutenberg/pull/59959)) #### Patterns - Add pattern title in create modal in post editor. ([59550](https://github.com/WordPress/gutenberg/pull/59550)) @@ -224,6 +231,7 @@ - Don't memoize callbacks in 'BlockSettingsDropdown'. ([59397](https://github.com/WordPress/gutenberg/pull/59397)) - Link dialog: Remove CSS hack. ([59746](https://github.com/WordPress/gutenberg/pull/59746)) - Pattern Explorer: Remove leftover source filter state handlers. ([60019](https://github.com/WordPress/gutenberg/pull/60019)) +- Fix code formatting in Nav block view file. ([60162](https://github.com/WordPress/gutenberg/pull/60162)) #### Components - Button : Deprecate `isSmall` prop. ([59734](https://github.com/WordPress/gutenberg/pull/59734)) @@ -304,6 +312,8 @@ The following contributors merged PRs in this release: @aaronrobertshaw @afercia @afragen @ajlende @alexstine @andrewfleming @anton-vlasenko @artemiomorales @bacoords @c4rl0sbr4v0 @carolinan @chrisbellboy @colinduwe @creativecoder @DAreRodz @dcalhoun @draganescu @ellatrix @enejb @enodekciw @flexseth @fluiddot @gaambo @georgestephanis @geriux @getdave @huzaifaalmesbah @inc2734 @J0n-92 @jaclync @jameskoster @jasmussen @Jayanth-Parthsarathy @jeryj @jorgefilipecosta @jsnajdr @kevin940726 @krokodok @luislard @Mamaduka @matiasbenedetto @mattsherman @mcsf @megane9988 @michalczaplinski @mirka @mujuonly @mzahir @ndiego @noisysocks @ntsekouras @oandregal @pbking @ramonjd @rcoll @SahilThakur02 @Sam-Xronn @scruffian @shail-mehta @SiobhyB @sirreal @Soean @Strangehill @sunil25393 @swissspidy @t-hamano @talldan @tellthemachines @TeresaGobble @tjcafferkey @tomepajk @tyxla @vcanales @youknowriad + + = 17.9.0 = diff --git a/docs/contributors/code/back-merging-to-wp-core.md b/docs/contributors/code/back-merging-to-wp-core.md index c01fd74382fbc..d6c283629e193 100644 --- a/docs/contributors/code/back-merging-to-wp-core.md +++ b/docs/contributors/code/back-merging-to-wp-core.md @@ -31,7 +31,7 @@ There are however certain exceptions to that rule. PRs with the following criter - Does not contain changes to PHP code. - Has label `Backport from WordPress Core` - this code is already in WP Core and is being synchronized back to Gutenberg. -- Has label `Backport to WordPress Core` - this code has already been syncrhonized to WP Core. +- Has label `Backport to WordPress Core` - this code has already been synchronized to WP Core. ## Further Reading diff --git a/docs/contributors/code/release.md b/docs/contributors/code/release.md index 05194ecd83417..4c8950eb5e7cd 100644 --- a/docs/contributors/code/release.md +++ b/docs/contributors/code/release.md @@ -223,7 +223,7 @@ Once approved, the new Gutenberg version will be available to WordPress users al The final step is to write a release post on [make.wordpress.org/core](https://make.wordpress.org/core/). You can find some tips on that below. -#### Troubleshooting the release +### Troubleshooting the release > The plugin was published to the WordPress.org plugin directory but the workflow failed. diff --git a/docs/explanations/architecture/styles.md b/docs/explanations/architecture/styles.md index 94a8e91f94edb..952c6c49caad2 100644 --- a/docs/explanations/architecture/styles.md +++ b/docs/explanations/architecture/styles.md @@ -78,7 +78,7 @@ For example: The paragraph declares support for font size in its `block.json`. This means the block will show a UI control for users to tweak its font size, unless it's disabled by the theme (learn more about how themes can disable UI controls in [the `theme.json` reference](https://developer.wordpress.org/block-editor/reference-guides/theme-json-reference/)). The system will also take care of setting up the UI control data (the font size of the block if it has one already assigned, the list of available font sizes to show), and will serialize the block data into HTML markup upon user changes (attach classes and inline styles appropriately). -By using the block supports mechanism via `block.json`, the block author is able to create the same experience as before just by writing a couple of lines. Check the tutorials for adding block supports to [static](/docs/how-to-guides/block-tutorial/block-supports-in-static-blocks.md) and [dynamic](/docs/how-to-guides/block-tutorial/block-supports-in-dynamic-blocks.md) blocks. +By using the block supports mechanism via `block.json`, the block author is able to create the same experience as before just by writing a couple of lines. Check the [block supports api](/docs/reference-guides/block-api/block-supports.md) for adding block supports to static or dynamic blocks. Besides the benefit of having to do less work to achieve the same results, there's a few other advantages: diff --git a/docs/getting-started/fundamentals/registration-of-a-block.md b/docs/getting-started/fundamentals/registration-of-a-block.md index d8d547912c748..5c80422f6f857 100644 --- a/docs/getting-started/fundamentals/registration-of-a-block.md +++ b/docs/getting-started/fundamentals/registration-of-a-block.md @@ -33,7 +33,7 @@ register_block_type( ); ``` -Here is a more complete example, including the `init` hook. +Here is a more complete example, including the `init` hook. ```php function minimal_block_ca6eda___register_block() { @@ -46,7 +46,7 @@ _See the [full block example](https://github.com/WordPress/block-development-exa ## Registering a block with JavaScript (client-side) -When the block has already been registered on the server, you only need to register the client-side settings in JavaScipt using the [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) method from the `@wordpress/blocks` package. You just need to make sure you use the same block name as defined in the block's `block.json` file. Here's an example: +When the block has already been registered on the server, you only need to register the client-side settings in JavaScript using the [`registerBlockType`](https://developer.wordpress.org/block-editor/reference-guides/packages/packages-blocks/#registerblocktype) method from the `@wordpress/blocks` package. You just need to make sure you use the same block name as defined in the block's `block.json` file. Here's an example: ```js import { registerBlockType } from '@wordpress/blocks'; @@ -71,7 +71,7 @@ The function accepts two parameters: The `settings` object passed as the second parameter includes many properties, but these are the two most important ones: - **`edit`:** The React component that gets used in the Editor for our block. -- **`save`:** The function that returns the static HTML markup that gets saved to the database. +- **`save`:** The function that returns the static HTML markup that gets saved to the database. The `registerBlockType()` function returns the registered block type (`WPBlock`) on success or `undefined` on failure. Here's an example: diff --git a/docs/getting-started/fundamentals/static-dynamic-rendering.md b/docs/getting-started/fundamentals/static-dynamic-rendering.md index c7a692a8ecb65..8d199f66cccd2 100644 --- a/docs/getting-started/fundamentals/static-dynamic-rendering.md +++ b/docs/getting-started/fundamentals/static-dynamic-rendering.md @@ -6,11 +6,11 @@ A block's front-end markup can either be dynamically generated server-side upon The post Static vs. dynamic blocks: What’s the difference? provides a great introduction to this topic. -## Static rendering +## Static rendering Blocks with "static rendering" produce front-end output that is fixed and stored in the database upon saving. These blocks rely solely on their `save` function to define their [HTML markup](https://developer.wordpress.org/block-editor/getting-started/fundamentals/markup-representation-block/), which remains unchanged unless manually edited in the Block Editor. -If a block does not use a dynamic rendering method—meaning it doesn't generate content on the fly via PHP when the page loads—it's considered a "static block." +If a block does not use a dynamic rendering method—meaning it doesn't generate content on the fly via PHP when the page loads—it's considered a "static block." The diagram below illustrates how static block content is saved in the database and then retrieved and rendered as HTML on the front end. @@ -24,7 +24,7 @@ Blocks in WordPress are encapsulated within special comment tags that serve as u
View an example of static rendering in the Preformatted block
-The following save function for the Preformatted core block looks like this: +The following save function for the Preformatted core block looks like this: ```js import { RichText, useBlockProps } from '@wordpress/block-editor'; @@ -160,7 +160,7 @@ On the front end, the `render_callback` is used to dynamically render the markup ### HTML representation of dynamic blocks in the database (`save`) -For dynamic blocks, the `save` callback function can return just `null`, which tells the editor to save only the block delimiter comment (along with any existing [block attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/)) to the database. These attributes are then passed into the server-side rendering callback, which will determine how to display the block on the front end of your site. +For dynamic blocks, the `save` callback function can return just `null`, which tells the editor to save only the block delimiter comment (along with any existing [block attributes](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/)) to the database. These attributes are then passed into the server-side rendering callback, which will determine how to display the block on the front end of your site. When `save` is `null`, the Block Editor will skip the [block markup validation process](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-edit-save/#validation), avoiding issues with frequently changing markup. @@ -176,4 +176,4 @@ If you are using [InnerBlocks](https://developer.wordpress.org/block-editor/how- ## Additional resources - [Static vs. dynamic blocks: What’s the difference?](https://developer.wordpress.org/news/2023/02/27/static-vs-dynamic-blocks-whats-the-difference/) | Developer Blog -- [Block deprecation – a tutorial](https://developer.wordpress.org/news/2023/03/10/block-deprecation-a-tutorial/) | Developer Blog \ No newline at end of file +- [Block deprecation – a tutorial](https://developer.wordpress.org/news/2023/03/10/block-deprecation-a-tutorial/) | Developer Blog diff --git a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md index 23803888f9522..2491d12ce9482 100644 --- a/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md +++ b/docs/how-to-guides/curating-the-editor-experience/disable-editor-functionality.md @@ -1,14 +1,14 @@ # Disable Editor functionality -This page is dedicated to the many ways you can disable specific functionality in the Post Editor and Site Editor that are not covered in other areas of the curation documentation. +This page is dedicated to the many ways you can disable specific functionality in the Post Editor and Site Editor that are not covered in other areas of the curation documentation. ## Restrict block options -There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). +There might be times when you don’t want access to a block at all to be available for users. To control what’s available in the inserter, you can take two approaches: [an allow list](/docs/reference-guides/filters/block-filters.md#using-an-allow-list) that disables all blocks except those on the list or a [deny list that unregisters specific blocks](/docs/reference-guides/filters/block-filters.md#using-a-deny-list). ## Disable the Pattern Directory -To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your `functions.php` file: +To fully remove patterns bundled with WordPress core from being accessed in the Inserter, the following can be added to your `functions.php` file: ```php function example_theme_support() { @@ -21,7 +21,7 @@ add_action( 'after_setup_theme', 'example_theme_support' ); Some Core blocks are actually [block variations](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/). A great example is the Row and Stack blocks, which are actually variations of the Group block. If you want to disable these "blocks", you actually need to disable the respective variations. -Block variations are registered using JavaScript and need to be disabled with JavaScript. The code below will disable the Row variation. +Block variations are registered using JavaScript and need to be disabled with JavaScript. The code below will disable the Row variation. ```js wp.domReady( () => { @@ -48,7 +48,7 @@ add_action( 'enqueue_block_editor_assets', 'example_disable_variations_script' ) There are a few Core blocks that include their own [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/). An example is the Image block, which includes a block style for rounded images called "Rounded". You many not want your users to round images, or you might prefer to use the border-radius control instead of the block style. Either way, it's easy to disable any unwanted block styles. -Unlike block variations, you can register styles in either JavaScript or PHP. If a style was registered in JavaScript, it must be disabled with JavaScript. If registered using PHP, the style can be disabled with either. All Core block styles are registed in JavaScript. +Unlike block variations, you can register styles in either JavaScript or PHP. If a style was registered in JavaScript, it must be disabled with JavaScript. If registered using PHP, the style can be disabled with either. All Core block styles are registered in JavaScript. So, you would use the following code to disable the "Rounded" block style for the Image block. @@ -58,7 +58,7 @@ wp.domReady( () => { }); ``` -This JavaScript should be enqueued much like the block variation example above. Refer to the [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/) documentation for how to register and unregister styles using PHP. +This JavaScript should be enqueued much like the block variation example above. Refer to the [block styles](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-styles/) documentation for how to register and unregister styles using PHP. ## Disable access to the Template Editor @@ -71,7 +71,7 @@ function example_theme_support() { add_action( 'after_setup_theme', 'example_theme_support' ); ``` -This prevents both the ability to create new block templates or edit them from within the Post Editor. +This prevents both the ability to create new block templates or edit them from within the Post Editor. ## Disable access to the Code Editor @@ -86,4 +86,4 @@ function example_restrict_code_editor_access( $settings, $context ) { add_filter( 'block_editor_settings_all', 'example_restrict_code_editor_access', 10, 2 ); ``` -This code prevents all users from accessing the Code Editor. You could also add [capability](https://wordpress.org/documentation/article/roles-and-capabilities/) checks to disable access for specific users. \ No newline at end of file +This code prevents all users from accessing the Code Editor. You could also add [capability](https://wordpress.org/documentation/article/roles-and-capabilities/) checks to disable access for specific users. diff --git a/docs/reference-guides/filters/block-filters.md b/docs/reference-guides/filters/block-filters.md index ce7eb4d0b2d12..7fa2024976040 100644 --- a/docs/reference-guides/filters/block-filters.md +++ b/docs/reference-guides/filters/block-filters.md @@ -283,6 +283,24 @@ wp.hooks.addFilter( ``` +### `editor.postContentBlockTypes` + +Used to modify the list of blocks that should be enabled even when used inside a locked template. Any block that saves data to a post should be added here. Examples of this are the post featured image block. Which often gets used in templates but should still allow selecting the image even when the template is locked. + +_Example:_ + +```js +const addExampleBlockToPostContentBlockTypes = ( blockTypes ) => { + return [ ...blockTypes, 'namespace/example' ]; +}; + +wp.hooks.addFilter( + 'editor.postContentBlockTypes', + 'my-plugin/post-content-block-types', + addExampleBlockToPostContentBlockTypes +); +``` + ## Removing Blocks ### Using a deny list diff --git a/lib/class-wp-theme-json-gutenberg.php b/lib/class-wp-theme-json-gutenberg.php index 789e6b2a53183..bf55d08c1d4b3 100644 --- a/lib/class-wp-theme-json-gutenberg.php +++ b/lib/class-wp-theme-json-gutenberg.php @@ -286,26 +286,31 @@ class WP_Theme_JSON_Gutenberg { * * Indirect properties are not output directly by `compute_style_properties`, * but are used elsewhere in the processing of global styles. The indirect - * property is used to validate whether or not a style value is allowed. + * property is used to validate whether a style value is allowed. * * @since 6.2.0 + * @since 6.6.0 Added background-image properties. * * @var array */ const INDIRECT_PROPERTIES_METADATA = array( - 'gap' => array( + 'gap' => array( array( 'spacing', 'blockGap' ), ), - 'column-gap' => array( + 'column-gap' => array( array( 'spacing', 'blockGap', 'left' ), ), - 'row-gap' => array( + 'row-gap' => array( array( 'spacing', 'blockGap', 'top' ), ), - 'max-width' => array( + 'max-width' => array( array( 'layout', 'contentSize' ), array( 'layout', 'wideSize' ), ), + 'background-image' => array( + array( 'background', 'backgroundImage', 'url' ), + array( 'background', 'backgroundImage', 'source' ), + ), ); /** @@ -1359,7 +1364,7 @@ public function get_block_custom_css_nodes() { * * @since 6.6.0 * - * @param array $css The block css node. + * @param array $css The block css node. * @param string $selector The block selector. * * @return string The global styles custom CSS for the block. @@ -2680,7 +2685,7 @@ static function ( $pseudo_selector ) use ( $selector ) { } // 2. Generate and append the rules that use the general selector. - $block_rules .= static::to_ruleset( $selector, $declarations ); + $block_rules .= static::to_ruleset( ":where($selector)", $declarations ); // 3. Generate and append the rules that use the duotone selector. if ( isset( $block_metadata['duotone'] ) && ! empty( $declarations_duotone ) ) { @@ -2697,7 +2702,7 @@ static function ( $pseudo_selector ) use ( $selector ) { // 5. Generate and append the feature level rulesets. foreach ( $feature_declarations as $feature_selector => $individual_feature_declarations ) { - $block_rules .= static::to_ruleset( $feature_selector, $individual_feature_declarations ); + $block_rules .= static::to_ruleset( ":where($feature_selector)", $individual_feature_declarations ); } // 6. Generate and append the style variation rulesets. diff --git a/package-lock.json b/package-lock.json index 5a23420f5d888..b0bb35d0e4071 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "gutenberg", - "version": "18.0.0-rc.1", + "version": "18.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "gutenberg", - "version": "18.0.0-rc.1", + "version": "18.0.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -55757,8 +55757,7 @@ "is-plain-object": "^5.0.0", "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.2", - "remove-accents": "^0.5.0" + "rememo": "^4.0.2" }, "engines": { "node": ">=12" @@ -56646,7 +56645,7 @@ }, "packages/react-native-aztec": { "name": "@wordpress/react-native-aztec", - "version": "1.115.0", + "version": "1.116.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/element": "file:../element", @@ -56659,7 +56658,7 @@ }, "packages/react-native-bridge": { "name": "@wordpress/react-native-bridge", - "version": "1.115.0", + "version": "1.116.0", "license": "GPL-2.0-or-later", "dependencies": { "@wordpress/react-native-aztec": "file:../react-native-aztec" @@ -56670,7 +56669,7 @@ }, "packages/react-native-editor": { "name": "@wordpress/react-native-editor", - "version": "1.115.0", + "version": "1.116.0", "hasInstallScript": true, "license": "GPL-2.0-or-later", "dependencies": { @@ -71531,8 +71530,7 @@ "is-plain-object": "^5.0.0", "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.2", - "remove-accents": "^0.5.0" + "rememo": "^4.0.2" } }, "@wordpress/edit-widgets": { diff --git a/package.json b/package.json index 5c63beee49d2a..fcb742cb379fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gutenberg", - "version": "18.0.0-rc.1", + "version": "18.0.0", "private": true, "description": "A new WordPress editor experience.", "author": "The WordPress Contributors", diff --git a/packages/block-editor/src/components/block-list/block.js b/packages/block-editor/src/components/block-list/block.js index 3bfe8eb5cdef2..237fecd05b6e0 100644 --- a/packages/block-editor/src/components/block-list/block.js +++ b/packages/block-editor/src/components/block-list/block.js @@ -225,6 +225,11 @@ function BlockListBlock( { // We set a new context with the adjusted and filtered wrapperProps (through // `editor.BlockListBlock`), which the `BlockListBlockProvider` did not have // access to. + // Note that the context value doesn't have to be memoized in this case + // because when it changes, this component will be re-rendered anyway, and + // none of the consumers (BlockListBlock and useBlockProps) are memoized or + // "pure". This is different from the public BlockEditContext, where + // consumers might be memoized or "pure". return ( { const { getBlockAttributes, @@ -32,11 +32,13 @@ export default function Shuffle( { clientId, as = Container } ) { } = select( blockEditorStore ); const attributes = getBlockAttributes( clientId ); const _categories = attributes?.metadata?.categories || EMPTY_ARRAY; + const _patternName = attributes?.metadata?.patternName; const rootBlock = getBlockRootClientId( clientId ); const _patterns = __experimentalGetAllowedPatterns( rootBlock ); return { categories: _categories, patterns: _patterns, + patternName: _patternName, }; }, [ clientId ] @@ -65,28 +67,32 @@ export default function Shuffle( { clientId, as = Container } ) { if ( sameCategoryPatternsWithSingleWrapper.length === 0 ) { return null; } + + function getNextPattern() { + const numberOfPatterns = sameCategoryPatternsWithSingleWrapper.length; + const patternIndex = sameCategoryPatternsWithSingleWrapper.findIndex( + ( { name } ) => name === patternName + ); + const nextPatternIndex = + patternIndex + 1 < numberOfPatterns ? patternIndex + 1 : 0; + return sameCategoryPatternsWithSingleWrapper[ nextPatternIndex ]; + } + const ComponentToUse = as; return ( { - const randomPattern = - sameCategoryPatternsWithSingleWrapper[ - Math.floor( - // eslint-disable-next-line no-restricted-syntax - Math.random() * - sameCategoryPatternsWithSingleWrapper.length - ) - ]; - randomPattern.blocks[ 0 ].attributes = { - ...randomPattern.blocks[ 0 ].attributes, + const nextPattern = getNextPattern(); + nextPattern.blocks[ 0 ].attributes = { + ...nextPattern.blocks[ 0 ].attributes, metadata: { - ...randomPattern.blocks[ 0 ].attributes.metadata, + ...nextPattern.blocks[ 0 ].attributes.metadata, categories, }, }; - replaceBlocks( clientId, randomPattern.blocks ); + replaceBlocks( clientId, nextPattern.blocks ); } } /> ); diff --git a/packages/block-editor/src/components/block-tools/block-selection-button.js b/packages/block-editor/src/components/block-tools/block-selection-button.js index e8fad9a62a5f1..47a7001d2ef69 100644 --- a/packages/block-editor/src/components/block-tools/block-selection-button.js +++ b/packages/block-editor/src/components/block-tools/block-selection-button.js @@ -123,10 +123,11 @@ function BlockSelectionButton( { clientId, rootClientId } ) { // Focus the breadcrumb in navigation mode. useEffect( () => { - ref.current.focus(); - - speak( label ); - }, [ label ] ); + if ( editorMode === 'navigation' ) { + ref.current.focus(); + speak( label ); + } + }, [ label, editorMode ] ); const blockElement = useBlockElement( clientId ); const { diff --git a/packages/block-editor/src/components/block-tools/index.js b/packages/block-editor/src/components/block-tools/index.js index 3959257ecf4e8..17e4e026cbbab 100644 --- a/packages/block-editor/src/components/block-tools/index.js +++ b/packages/block-editor/src/components/block-tools/index.js @@ -2,6 +2,7 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; +import { isTextField } from '@wordpress/dom'; import { Popover } from '@wordpress/components'; import { __unstableUseShortcutEventMatch as useShortcutEventMatch } from '@wordpress/keyboard-shortcuts'; import { useRef } from '@wordpress/element'; @@ -20,6 +21,7 @@ import { store as blockEditorStore } from '../../store'; import usePopoverScroll from '../block-popover/use-popover-scroll'; import ZoomOutModeInserters from './zoom-out-mode-inserters'; import { useShowBlockTools } from './use-show-block-tools'; +import { unlock } from '../../lock-unlock'; function selector( select ) { const { @@ -79,7 +81,8 @@ export default function BlockTools( { selectBlock, moveBlocksUp, moveBlocksDown, - } = useDispatch( blockEditorStore ); + expandBlock, + } = unlock( useDispatch( blockEditorStore ) ); function onKeyDown( event ) { if ( event.defaultPrevented ) return; @@ -140,6 +143,20 @@ export default function BlockTools( { // In effect, to the user this feels like deselecting the multi-selection. selectBlock( clientIds[ 0 ] ); } + } else if ( isMatch( 'core/block-editor/collapse-list-view', event ) ) { + // If focus is currently within a text field, such as a rich text block or other editable field, + // skip collapsing the list view, and allow the keyboard shortcut to be handled by the text field. + // This condition checks for both the active element and the active element within an iframed editor. + if ( + isTextField( event.target ) || + isTextField( + event.target?.contentWindow?.document?.activeElement + ) + ) { + return; + } + event.preventDefault(); + expandBlock( clientId ); } } diff --git a/packages/block-editor/src/components/button-block-appender/content.scss b/packages/block-editor/src/components/button-block-appender/content.scss index 50d93234b93f5..87243ea927182 100644 --- a/packages/block-editor/src/components/button-block-appender/content.scss +++ b/packages/block-editor/src/components/button-block-appender/content.scss @@ -56,7 +56,11 @@ } .block-editor-inserter { - visibility: hidden; + opacity: 0; + + &:focus-within { + opacity: 1; + } } &.is-drag-over { diff --git a/packages/block-editor/src/components/global-styles/background-panel.js b/packages/block-editor/src/components/global-styles/background-panel.js new file mode 100644 index 0000000000000..1288ff823b46c --- /dev/null +++ b/packages/block-editor/src/components/global-styles/background-panel.js @@ -0,0 +1,591 @@ +/** + * External dependencies + */ +import classnames from 'classnames'; + +/** + * WordPress dependencies + */ +import { + __experimentalToolsPanel as ToolsPanel, + __experimentalToolsPanelItem as ToolsPanelItem, + ToggleControl, + __experimentalToggleGroupControl as ToggleGroupControl, + __experimentalToggleGroupControlOption as ToggleGroupControlOption, + __experimentalUnitControl as UnitControl, + __experimentalVStack as VStack, + DropZone, + FlexItem, + FocalPointPicker, + MenuItem, + VisuallyHidden, + __experimentalItemGroup as ItemGroup, + __experimentalHStack as HStack, + __experimentalTruncate as Truncate, +} from '@wordpress/components'; +import { __, sprintf } from '@wordpress/i18n'; +import { store as noticesStore } from '@wordpress/notices'; +import { getFilename } from '@wordpress/url'; +import { useCallback, Platform, useRef } from '@wordpress/element'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { focus } from '@wordpress/dom'; +import { isBlobURL } from '@wordpress/blob'; + +/** + * Internal dependencies + */ +import { TOOLSPANEL_DROPDOWNMENU_PROPS } from './utils'; +import { setImmutably } from '../../utils/object'; +import MediaReplaceFlow from '../media-replace-flow'; +import { store as blockEditorStore } from '../../store'; + +const IMAGE_BACKGROUND_TYPE = 'image'; + +/** + * Checks site settings to see if the background panel may be used. + * `settings.background.backgroundSize` exists also, + * but can only be used if settings?.background?.backgroundImage is `true`. + * + * @param {Object} settings Site settings + * @return {boolean} Whether site settings has activated background panel. + */ +export function useHasBackgroundPanel( settings ) { + return Platform.OS === 'web' && settings?.background?.backgroundImage; +} + +/** + * Checks if there is a current value in the background size block support + * attributes. Background size values include background size as well + * as background position. + * + * @param {Object} style Style attribute. + * @return {boolean} Whether the block has a background size value set. + */ +export function hasBackgroundSizeValue( style ) { + return ( + style?.background?.backgroundPosition !== undefined || + style?.background?.backgroundSize !== undefined + ); +} + +/** + * Checks if there is a current value in the background image block support + * attributes. + * + * @param {Object} style Style attribute. + * @return {boolean} Whether the block has a background image value set. + */ +export function hasBackgroundImageValue( style ) { + return ( + !! style?.background?.backgroundImage?.id || + !! style?.background?.backgroundImage?.url + ); +} + +/** + * Get the help text for the background size control. + * + * @param {string} value backgroundSize value. + * @return {string} Translated help text. + */ +function backgroundSizeHelpText( value ) { + if ( value === 'cover' || value === undefined ) { + return __( 'Image covers the space evenly.' ); + } + if ( value === 'contain' ) { + return __( 'Image is contained without distortion.' ); + } + return __( 'Specify a fixed width.' ); +} + +/** + * Converts decimal x and y coords from FocalPointPicker to percentage-based values + * to use as backgroundPosition value. + * + * @param {{x?:number, y?:number}} value FocalPointPicker coords. + * @return {string} backgroundPosition value. + */ +export const coordsToBackgroundPosition = ( value ) => { + if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { + return undefined; + } + + const x = isNaN( value.x ) ? 0.5 : value.x; + const y = isNaN( value.y ) ? 0.5 : value.y; + + return `${ x * 100 }% ${ y * 100 }%`; +}; + +/** + * Converts backgroundPosition value to x and y coords for FocalPointPicker. + * + * @param {string} value backgroundPosition value. + * @return {{x?:number, y?:number}} FocalPointPicker coords. + */ +export const backgroundPositionToCoords = ( value ) => { + if ( ! value ) { + return { x: undefined, y: undefined }; + } + + let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); + x = isNaN( x ) ? undefined : x; + y = isNaN( y ) ? x : y; + + return { x, y }; +}; + +function InspectorImagePreview( { label, filename, url: imgUrl } ) { + const imgLabel = label || getFilename( imgUrl ); + return ( + + + + { imgUrl && ( + + ) } + + + + { imgLabel } + + + { filename + ? sprintf( + /* translators: %s: file name */ + __( 'Selected image: %s' ), + filename + ) + : __( 'No image selected' ) } + + + + + ); +} + +function BackgroundImageToolsPanelItem( { + panelId, + isShownByDefault, + onChange, + style, + inheritedValue, +} ) { + const mediaUpload = useSelect( + ( select ) => select( blockEditorStore ).getSettings().mediaUpload, + [] + ); + + const { id, title, url } = style?.background?.backgroundImage || { + ...inheritedValue?.background?.backgroundImage, + }; + + const replaceContainerRef = useRef(); + + const { createErrorNotice } = useDispatch( noticesStore ); + const onUploadError = ( message ) => { + createErrorNotice( message, { type: 'snackbar' } ); + }; + + const resetBackgroundImage = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundImage' ], + undefined + ) + ); + + const onSelectMedia = ( media ) => { + if ( ! media || ! media.url ) { + resetBackgroundImage(); + return; + } + + if ( isBlobURL( media.url ) ) { + return; + } + + // For media selections originated from a file upload. + if ( + ( media.media_type && + media.media_type !== IMAGE_BACKGROUND_TYPE ) || + ( ! media.media_type && + media.type && + media.type !== IMAGE_BACKGROUND_TYPE ) + ) { + onUploadError( + __( 'Only images can be used as a background image.' ) + ); + return; + } + + onChange( + setImmutably( style, [ 'background', 'backgroundImage' ], { + url: media.url, + id: media.id, + source: 'file', + title: media.title || undefined, + } ) + ); + }; + + const onFilesDrop = ( filesList ) => { + mediaUpload( { + allowedTypes: [ 'image' ], + filesList, + onFileChange( [ image ] ) { + if ( isBlobURL( image?.url ) ) { + return; + } + onSelectMedia( image ); + }, + onError: onUploadError, + } ); + }; + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + style: { + ...previousValue.style, + background: undefined, + }, + }; + }, [] ); + + const hasValue = + hasBackgroundImageValue( style ) || + hasBackgroundImageValue( inheritedValue ); + + return ( + hasValue } + label={ __( 'Background image' ) } + onDeselect={ resetBackgroundImage } + isShownByDefault={ isShownByDefault } + resetAllFilter={ resetAllFilter } + panelId={ panelId } + > +
+ + } + variant="secondary" + > + { hasValue && ( + { + const [ toggleButton ] = focus.tabbable.find( + replaceContainerRef.current + ); + // Focus the toggle button and close the dropdown menu. + // This ensures similar behaviour as to selecting an image, where the dropdown is + // closed and focus is redirected to the dropdown toggle button. + toggleButton?.focus(); + toggleButton?.click(); + resetBackgroundImage(); + } } + > + { __( 'Reset ' ) } + + ) } + + +
+
+ ); +} + +function BackgroundSizeToolsPanelItem( { + panelId, + isShownByDefault, + onChange, + style, + inheritedValue, + defaultValues, +} ) { + const sizeValue = + style?.background?.backgroundSize || + inheritedValue?.background?.backgroundSize; + const repeatValue = + style?.background?.backgroundRepeat || + inheritedValue?.background?.backgroundRepeat; + const imageValue = + style?.background?.backgroundImage?.url || + inheritedValue?.background?.backgroundImage?.url; + const positionValue = + style?.background?.backgroundPosition || + inheritedValue?.background?.backgroundPosition; + + /* + * An `undefined` value is replaced with any supplied + * default control value for the toggle group control. + * An empty string is treated as `auto` - this allows a user + * to select "Size" and then enter a custom value, with an + * empty value being treated as `auto`. + */ + const currentValueForToggle = + ( sizeValue !== undefined && + sizeValue !== 'cover' && + sizeValue !== 'contain' ) || + sizeValue === '' + ? 'auto' + : sizeValue || defaultValues?.backgroundSize; + + /* + * If the current value is `cover` and the repeat value is `undefined`, then + * the toggle should be unchecked as the default state. Otherwise, the toggle + * should reflect the current repeat value. + */ + const repeatCheckedValue = ! ( + repeatValue === 'no-repeat' || + ( currentValueForToggle === 'cover' && repeatValue === undefined ) + ); + + const hasValue = hasBackgroundSizeValue( style ); + + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + style: { + ...previousValue.style, + background: { + ...previousValue.style?.background, + backgroundRepeat: undefined, + backgroundSize: undefined, + }, + }, + }; + }, [] ); + + const updateBackgroundSize = ( next ) => { + // When switching to 'contain' toggle the repeat off. + let nextRepeat = repeatValue; + + if ( next === 'contain' ) { + nextRepeat = 'no-repeat'; + } + + if ( next === 'cover' ) { + nextRepeat = undefined; + } + + if ( + ( currentValueForToggle === 'cover' || + currentValueForToggle === 'contain' ) && + next === 'auto' + ) { + nextRepeat = undefined; + } + + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundRepeat: nextRepeat, + backgroundSize: next, + } ) + ); + }; + + const updateBackgroundPosition = ( next ) => { + onChange( + setImmutably( + style, + [ 'background', 'backgroundPosition' ], + coordsToBackgroundPosition( next ) + ) + ); + }; + + const toggleIsRepeated = () => + onChange( + setImmutably( + style, + [ 'background', 'backgroundRepeat' ], + repeatCheckedValue === true ? 'no-repeat' : undefined + ) + ); + + const resetBackgroundSize = () => + onChange( + setImmutably( style, [ 'background' ], { + ...style?.background, + backgroundPosition: undefined, + backgroundRepeat: undefined, + backgroundSize: undefined, + } ) + ); + + return ( + hasValue } + label={ __( 'Size' ) } + onDeselect={ resetBackgroundSize } + isShownByDefault={ isShownByDefault } + resetAllFilter={ resetAllFilter } + panelId={ panelId } + > + + + + + + + { sizeValue !== undefined && + sizeValue !== 'cover' && + sizeValue !== 'contain' ? ( + + ) : null } + { currentValueForToggle !== 'cover' && ( + + ) } + + ); +} + +function BackgroundToolsPanel( { + resetAllFilter, + onChange, + value, + panelId, + children, +} ) { + const resetAll = () => { + const updatedValue = resetAllFilter( value ); + onChange( updatedValue ); + }; + + return ( + + { children } + + ); +} + +const DEFAULT_CONTROLS = { + backgroundImage: true, + backgroundSize: true, +}; + +export default function BackgroundPanel( { + as: Wrapper = BackgroundToolsPanel, + value, + onChange, + inheritedValue = value, + settings, + panelId, + defaultControls = DEFAULT_CONTROLS, + defaultValues = {}, +} ) { + const resetAllFilter = useCallback( ( previousValue ) => { + return { + ...previousValue, + background: {}, + }; + }, [] ); + const shouldShowBackgroundSizeControls = + settings?.background?.backgroundSize; + + return ( + + + { shouldShowBackgroundSizeControls && ( + + ) } + + ); +} diff --git a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js index 05ac3429e4b65..c525cf45c6d52 100644 --- a/packages/block-editor/src/components/global-styles/get-global-styles-changes.js +++ b/packages/block-editor/src/components/global-styles/get-global-styles-changes.js @@ -26,6 +26,7 @@ const translationMap = { 'settings.typography': __( 'Typography' ), 'styles.color': __( 'Colors' ), 'styles.spacing': __( 'Spacing' ), + 'styles.background': __( 'Background' ), 'styles.typography': __( 'Typography' ), }; const getBlockNames = memoize( () => @@ -126,6 +127,7 @@ export function getGlobalStylesChangelist( next, previous ) { const changedValueTree = deepCompare( { styles: { + background: next?.styles?.background, color: next?.styles?.color, typography: next?.styles?.typography, spacing: next?.styles?.spacing, @@ -136,6 +138,7 @@ export function getGlobalStylesChangelist( next, previous ) { }, { styles: { + background: previous?.styles?.background, color: previous?.styles?.color, typography: previous?.styles?.typography, spacing: previous?.styles?.spacing, diff --git a/packages/block-editor/src/components/global-styles/hooks.js b/packages/block-editor/src/components/global-styles/hooks.js index 6be5481a633da..bdda9563edae0 100644 --- a/packages/block-editor/src/components/global-styles/hooks.js +++ b/packages/block-editor/src/components/global-styles/hooks.js @@ -27,6 +27,7 @@ const VALID_SETTINGS = [ 'background.backgroundImage', 'background.backgroundRepeat', 'background.backgroundSize', + 'background.backgroundPosition', 'border.color', 'border.radius', 'border.style', diff --git a/packages/block-editor/src/components/global-styles/index.js b/packages/block-editor/src/components/global-styles/index.js index ca8d1168d02d0..7ad192fac9b4f 100644 --- a/packages/block-editor/src/components/global-styles/index.js +++ b/packages/block-editor/src/components/global-styles/index.js @@ -31,5 +31,9 @@ export { useHasImageSettingsPanel, } from './image-settings-panel'; export { default as AdvancedPanel } from './advanced-panel'; +export { + default as BackgroundPanel, + useHasBackgroundPanel, +} from './background-panel'; export { areGlobalStyleConfigsEqual } from './utils'; export { default as getGlobalStylesChanges } from './get-global-styles-changes'; diff --git a/packages/block-editor/src/components/global-styles/style.scss b/packages/block-editor/src/components/global-styles/style.scss index 712590921c040..ab4407cd9b911 100644 --- a/packages/block-editor/src/components/global-styles/style.scss +++ b/packages/block-editor/src/components/global-styles/style.scss @@ -70,3 +70,80 @@ /*rtl:ignore*/ direction: ltr; } + +.block-editor-global-styles-background-panel__inspector-media-replace-container { + position: relative; + // Since there is no option to skip rendering the drag'n'drop icon in drop + // zone, we hide it for now. + .components-drop-zone__content-icon { + display: none; + } + + button.components-button { + color: $gray-900; + box-shadow: inset 0 0 0 $border-width $gray-300; + width: 100%; + display: block; + height: $grid-unit-50; + + &:hover { + color: var(--wp-admin-theme-color); + } + + &:focus { + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } + } + + .block-editor-global-styles-background-panel__inspector-media-replace-title { + word-break: break-all; + // The Button component is white-space: nowrap, and that won't work with line-clamp. + white-space: normal; + + // Without this, the ellipsis can sometimes be partially hidden by the Button padding. + text-align: start; + text-align-last: center; + } + + .components-dropdown { + display: block; + } +} + +.block-editor-global-styles-background-panel__inspector-image-indicator-wrapper { + background: #fff linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); // Show a diagonal line (crossed out) for empty background image. + border-radius: $radius-round !important; // Override the default border-radius inherited from FlexItem. + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); + display: block; + width: 20px; + height: 20px; + flex: none; + + &.has-image { + background: #fff; // No diagonal line for non-empty background image. A background color is in use to account for partially transparent images. + } +} + +.block-editor-global-styles-background-panel__inspector-image-indicator { + background-size: cover; + border-radius: $radius-round; + width: 20px; + height: 20px; + display: block; + position: relative; +} + +.block-editor-global-styles-background-panel__inspector-image-indicator::after { + content: ""; + position: absolute; + top: -1px; + left: -1px; + bottom: -1px; + right: -1px; + border-radius: $radius-round; + box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); + // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. + border: 1px solid transparent; + box-sizing: inherit; +} + diff --git a/packages/block-editor/src/hooks/test/background.js b/packages/block-editor/src/components/global-styles/test/background-panel.js similarity index 60% rename from packages/block-editor/src/hooks/test/background.js rename to packages/block-editor/src/components/global-styles/test/background-panel.js index cbc9033c2256f..d0b3a8ad60170 100644 --- a/packages/block-editor/src/hooks/test/background.js +++ b/packages/block-editor/src/components/global-styles/test/background-panel.js @@ -5,7 +5,8 @@ import { backgroundPositionToCoords, coordsToBackgroundPosition, -} from '../background'; + hasBackgroundImageValue, +} from '../background-panel'; describe( 'backgroundPositionToCoords', () => { it( 'should return the correct coordinates for a percentage value using 2-value syntax', () => { @@ -48,3 +49,37 @@ describe( 'coordsToBackgroundPosition', () => { expect( coordsToBackgroundPosition( {} ) ).toBeUndefined(); } ); } ); + +describe( 'hasBackgroundImageValue', () => { + it( 'should return `true` when id and url exist', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { id: 1, url: 'url' } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `true` when only url exists', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { url: 'url' } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `true` when only id exists', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: { id: 1 } }, + } ) + ).toBe( true ); + } ); + + it( 'should return `false` when id and url do not exist', () => { + expect( + hasBackgroundImageValue( { + background: { backgroundImage: {} }, + } ) + ).toBe( false ); + } ); +} ); diff --git a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js index 9ff840dc76730..6eb4974ec041a 100644 --- a/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js +++ b/packages/block-editor/src/components/global-styles/test/get-global-styles-changes.js @@ -17,6 +17,15 @@ import { describe( 'getGlobalStylesChanges and utils', () => { const next = { styles: { + background: { + backgroundImage: { + url: 'https://example.com/image.jpg', + source: 'file', + }, + backgroundSize: 'contain', + backgroundPosition: '30% 30%', + backgroundRepeat: 'no-repeat', + }, typography: { fontSize: 'var(--wp--preset--font-size--potato)', fontStyle: 'normal', @@ -84,6 +93,15 @@ describe( 'getGlobalStylesChanges and utils', () => { }; const previous = { styles: { + background: { + backgroundImage: { + url: 'https://example.com/image_new.jpg', + source: 'file', + }, + backgroundSize: 'contain', + backgroundPosition: '40% 77%', + backgroundRepeat: 'repeat', + }, typography: { fontSize: 'var(--wp--preset--font-size--fungus)', fontStyle: 'normal', @@ -195,7 +213,7 @@ describe( 'getGlobalStylesChanges and utils', () => { it( 'returns a list of changes', () => { const result = getGlobalStylesChanges( next, previous ); expect( result ).toEqual( [ - 'Colors, Typography styles.', + 'Background, Colors, Typography styles.', 'Test pumpkin flowers block.', 'H3, Caption, H6, Link elements.', 'Color, Typography settings.', @@ -204,10 +222,10 @@ describe( 'getGlobalStylesChanges and utils', () => { it( 'returns a list of truncated changes', () => { const resultA = getGlobalStylesChanges( next, previous, { - maxResults: 3, + maxResults: 4, } ); expect( resultA ).toEqual( [ - 'Colors, Typography styles.', + 'Background, Colors, Typography styles.', 'Test pumpkin flowers block.', ] ); } ); @@ -254,6 +272,7 @@ describe( 'getGlobalStylesChanges and utils', () => { const resultA = getGlobalStylesChangelist( next, previous ); expect( resultA ).toEqual( [ + [ 'styles', 'Background' ], [ 'styles', 'Colors' ], [ 'styles', 'Typography' ], [ 'blocks', 'Test pumpkin flowers' ], diff --git a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js index 5599fcfca19ab..304c7f56fb101 100644 --- a/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/test/use-global-styles-output.js @@ -481,8 +481,8 @@ describe( 'global styles renderer', () => { expect( toStyles( tree, blockSelectors ) ).toEqual( 'body {margin: 0;}body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }body .is-layout-grid { display:grid; }body .is-layout-grid > * { margin: 0; }' + - 'body{background-color: red;margin: 10px;padding: 10px;}a:where(:not(.wp-element-button)){color: blue;}a:where(:not(.wp-element-button)):hover{color: orange;}a:where(:not(.wp-element-button)):focus{color: orange;}h1{font-size: 42px;}.wp-block-group{margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}h1,h2,h3,h4,h5,h6{color: orange;}h1 a:where(:not(.wp-element-button)),h2 a:where(:not(.wp-element-button)),h3 a:where(:not(.wp-element-button)),h4 a:where(:not(.wp-element-button)),h5 a:where(:not(.wp-element-button)),h6 a:where(:not(.wp-element-button)){color: hotpink;}h1 a:where(:not(.wp-element-button)):hover,h2 a:where(:not(.wp-element-button)):hover,h3 a:where(:not(.wp-element-button)):hover,h4 a:where(:not(.wp-element-button)):hover,h5 a:where(:not(.wp-element-button)):hover,h6 a:where(:not(.wp-element-button)):hover{color: red;}h1 a:where(:not(.wp-element-button)):focus,h2 a:where(:not(.wp-element-button)):focus,h3 a:where(:not(.wp-element-button)):focus,h4 a:where(:not(.wp-element-button)):focus,h5 a:where(:not(.wp-element-button)):focus,h6 a:where(:not(.wp-element-button)):focus{color: red;}' + - '.wp-block-image img, .wp-block-image .wp-crop-area{border-radius: 9999px;}.wp-block-image{color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + + ':where(body){background-color: red;margin: 10px;padding: 10px;}:where(a:where(:not(.wp-element-button))){color: blue;}a:where(:not(.wp-element-button)):hover{color: orange;}a:where(:not(.wp-element-button)):focus{color: orange;}:where(h1){font-size: 42px;}:where(.wp-block-group){margin-top: 10px;margin-right: 20px;margin-bottom: 30px;margin-left: 40px;padding-top: 11px;padding-right: 22px;padding-bottom: 33px;padding-left: 44px;}:where(h1,h2,h3,h4,h5,h6){color: orange;}:where(h1 a:where(:not(.wp-element-button)),h2 a:where(:not(.wp-element-button)),h3 a:where(:not(.wp-element-button)),h4 a:where(:not(.wp-element-button)),h5 a:where(:not(.wp-element-button)),h6 a:where(:not(.wp-element-button))){color: hotpink;}h1 a:where(:not(.wp-element-button)):hover,h2 a:where(:not(.wp-element-button)):hover,h3 a:where(:not(.wp-element-button)):hover,h4 a:where(:not(.wp-element-button)):hover,h5 a:where(:not(.wp-element-button)):hover,h6 a:where(:not(.wp-element-button)):hover{color: red;}h1 a:where(:not(.wp-element-button)):focus,h2 a:where(:not(.wp-element-button)):focus,h3 a:where(:not(.wp-element-button)):focus,h4 a:where(:not(.wp-element-button)):focus,h5 a:where(:not(.wp-element-button)):focus,h6 a:where(:not(.wp-element-button)):focus{color: red;}' + + ':where(.wp-block-image img, .wp-block-image .wp-crop-area){border-radius: 9999px;}:where(.wp-block-image){color: red;}.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' + '.has-white-color{color: var(--wp--preset--color--white) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}h1.has-blue-color,h2.has-blue-color,h3.has-blue-color,h4.has-blue-color,h5.has-blue-color,h6.has-blue-color{color: var(--wp--preset--color--blue) !important;}h1.has-blue-background-color,h2.has-blue-background-color,h3.has-blue-background-color,h4.has-blue-background-color,h5.has-blue-background-color,h6.has-blue-background-color{background-color: var(--wp--preset--color--blue) !important;}h1.has-blue-border-color,h2.has-blue-border-color,h3.has-blue-border-color,h4.has-blue-border-color,h5.has-blue-border-color,h6.has-blue-border-color{border-color: var(--wp--preset--color--blue) !important;}' ); } ); @@ -524,7 +524,7 @@ describe( 'global styles renderer', () => { expect( toStyles( Object.freeze( tree ), blockSelectors ) ).toEqual( 'body {margin: 0;}body .is-layout-flow > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-flow > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-flow > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignleft { float: left; margin-inline-start: 0; margin-inline-end: 2em; }body .is-layout-constrained > .alignright { float: right; margin-inline-start: 2em; margin-inline-end: 0; }body .is-layout-constrained > .aligncenter { margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)) { max-width: var(--wp--style--global--content-size); margin-left: auto !important; margin-right: auto !important; }body .is-layout-constrained > .alignwide { max-width: var(--wp--style--global--wide-size); }body .is-layout-flex { display:flex; }body .is-layout-flex { flex-wrap: wrap; align-items: center; }body .is-layout-flex > * { margin: 0; }body .is-layout-grid { display:grid; }body .is-layout-grid > * { margin: 0; }' + - '.wp-image-spacing{padding-top: 1px;}.wp-image-border-color{border-color: red;}.wp-image-border{border-radius: 9999px;}.wp-image{color: red;}' + + ':where(.wp-image-spacing){padding-top: 1px;}:where(.wp-image-border-color){border-color: red;}:where(.wp-image-border){border-radius: 9999px;}:where(.wp-image){color: red;}' + '.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }' ); } ); diff --git a/packages/block-editor/src/components/global-styles/use-global-styles-output.js b/packages/block-editor/src/components/global-styles/use-global-styles-output.js index f1ba157d9c12f..990cd6a534921 100644 --- a/packages/block-editor/src/components/global-styles/use-global-styles-output.js +++ b/packages/block-editor/src/components/global-styles/use-global-styles-output.js @@ -844,7 +844,7 @@ export const toStyles = ( ( [ cssSelector, declarations ] ) => { if ( declarations.length ) { const rules = declarations.join( ';' ); - ruleset += `${ cssSelector }{${ rules };}`; + ruleset += `:where(${ cssSelector }){${ rules };}`; } } ); @@ -937,7 +937,9 @@ export const toStyles = ( isTemplate ); if ( declarations?.length ) { - ruleset += `${ selector }{${ declarations.join( ';' ) };}`; + ruleset += `:where(${ selector }){${ declarations.join( + ';' + ) };}`; } // Check for pseudo selector in `styles` and handle separately. diff --git a/packages/block-editor/src/components/iframe/index.js b/packages/block-editor/src/components/iframe/index.js index 4236bae62b1e7..e59e564da3e6a 100644 --- a/packages/block-editor/src/components/iframe/index.js +++ b/packages/block-editor/src/components/iframe/index.js @@ -244,12 +244,12 @@ function Iframe( { height: auto !important; min-height: 100%; } - - body { + /* Lowest specificity to not override global styles */ + :where(body) { margin: 0; /* Default background color in case zoom out mode background colors the html element */ - background: white; + background-color: white; } ${ styles } diff --git a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js index f548a4632eb35..7a5a9eb8d989e 100644 --- a/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js +++ b/packages/block-editor/src/components/inserter/block-patterns-tab/pattern-category-preview-panel.js @@ -10,6 +10,7 @@ import { focus } from '@wordpress/dom'; */ import { PatternCategoryPreviews } from './pattern-category-previews'; +import { useZoomOut } from '../../../hooks/use-zoom-out'; export function PatternCategoryPreviewPanel( { rootClientId, @@ -29,6 +30,10 @@ export function PatternCategoryPreviewPanel( { return () => clearTimeout( timeout ); }, [ category ] ); + // Move to zoom out mode when this component is mounted + // and back to the previous mode when unmounted. + useZoomOut(); + return (
{}; @@ -50,10 +49,6 @@ export function PatternCategoryPreviews( { const [ patternSyncFilter, setPatternSyncFilter ] = useState( 'all' ); const [ patternSourceFilter, setPatternSourceFilter ] = useState( 'all' ); - // Move to zoom out mode when this component is mounted - // and back to the previous mode when unmounted. - useZoomOut(); - const availableCategories = usePatternCategories( rootClientId, patternSourceFilter diff --git a/packages/block-editor/src/components/inserter/search-items.js b/packages/block-editor/src/components/inserter/search-items.js index 35346fca09f32..154098fc1a837 100644 --- a/packages/block-editor/src/components/inserter/search-items.js +++ b/packages/block-editor/src/components/inserter/search-items.js @@ -12,6 +12,17 @@ const defaultGetKeywords = ( item ) => item.keywords || []; const defaultGetCategory = ( item ) => item.category; const defaultGetCollection = () => null; +// Normalization regexes +const splitRegexp = [ + /([\p{Ll}\p{Lo}\p{N}])([\p{Lu}\p{Lt}])/gu, // One lowercase or digit, followed by one uppercase. + /([\p{Lu}\p{Lt}])([\p{Lu}\p{Lt}][\p{Ll}\p{Lo}])/gu, // One uppercase followed by one uppercase and one lowercase. +]; +const stripRegexp = /(\p{C}|\p{P}|\p{S})+/giu; // Anything that's not a punctuation, symbol or control/format character. + +// Normalization cache +const extractedWords = new Map(); +const normalizedStrings = new Map(); + /** * Extracts words from an input string. * @@ -19,16 +30,21 @@ const defaultGetCollection = () => null; * * @return {Array} Words, extracted from the input string. */ -function extractWords( input = '' ) { - return noCase( input, { - splitRegexp: [ - /([\p{Ll}\p{Lo}\p{N}])([\p{Lu}\p{Lt}])/gu, // One lowercase or digit, followed by one uppercase. - /([\p{Lu}\p{Lt}])([\p{Lu}\p{Lt}][\p{Ll}\p{Lo}])/gu, // One uppercase followed by one uppercase and one lowercase. - ], - stripRegexp: /(\p{C}|\p{P}|\p{S})+/giu, // Anything that's not a punctuation, symbol or control/format character. +export function extractWords( input = '' ) { + if ( extractedWords.has( input ) ) { + return extractedWords.get( input ); + } + + const result = noCase( input, { + splitRegexp, + stripRegexp, } ) .split( ' ' ) .filter( Boolean ); + + extractedWords.set( input, result ); + + return result; } /** @@ -38,20 +54,26 @@ function extractWords( input = '' ) { * * @return {string} The normalized search input. */ -function normalizeSearchInput( input = '' ) { +export function normalizeString( input = '' ) { + if ( normalizedStrings.has( input ) ) { + return normalizedStrings.get( input ); + } + // Disregard diacritics. // Input: "média" - input = removeAccents( input ); + let result = removeAccents( input ); // Accommodate leading slash, matching autocomplete expectations. // Input: "/media" - input = input.replace( /^\//, '' ); + result = result.replace( /^\//, '' ); // Lowercase. // Input: "MEDIA" - input = input.toLowerCase(); + result = result.toLowerCase(); + + normalizedStrings.set( input, result ); - return input; + return result; } /** @@ -62,7 +84,7 @@ function normalizeSearchInput( input = '' ) { * @return {string[]} The normalized list of search terms. */ export const getNormalizedSearchTerms = ( input = '' ) => { - return extractWords( normalizeSearchInput( input ) ); + return extractWords( normalizeString( input ) ); }; const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => { @@ -148,8 +170,8 @@ export function getItemSearchRank( item, searchTerm, config = {} ) { const category = getCategory( item ); const collection = getCollection( item ); - const normalizedSearchInput = normalizeSearchInput( searchTerm ); - const normalizedTitle = normalizeSearchInput( title ); + const normalizedSearchInput = normalizeString( searchTerm ); + const normalizedTitle = normalizeString( title ); let rank = 0; diff --git a/packages/block-editor/src/components/keyboard-shortcuts/index.js b/packages/block-editor/src/components/keyboard-shortcuts/index.js index 0e0a57257becc..7ea36a14aa7a8 100644 --- a/packages/block-editor/src/components/keyboard-shortcuts/index.js +++ b/packages/block-editor/src/components/keyboard-shortcuts/index.js @@ -132,6 +132,17 @@ function KeyboardShortcutsRegister() { character: 'y', }, } ); + + // List view shortcuts. + registerShortcut( { + name: 'core/block-editor/collapse-list-view', + category: 'list-view', + description: __( 'Collapse all other items.' ), + keyCombination: { + modifier: 'alt', + character: 'l', + }, + } ); }, [ registerShortcut ] ); return null; diff --git a/packages/block-editor/src/components/list-view/block-select-button.js b/packages/block-editor/src/components/list-view/block-select-button.js index 7fc5f91012f87..d36d0ca998654 100644 --- a/packages/block-editor/src/components/list-view/block-select-button.js +++ b/packages/block-editor/src/components/list-view/block-select-button.js @@ -64,6 +64,7 @@ function ListViewBlockSelectButton( getPreviousBlockClientId, getBlockRootClientId, getBlockOrder, + getBlockParents, getBlocksByClientId, canRemoveBlocks, } = useSelect( blockEditorStore ); @@ -72,7 +73,7 @@ function ListViewBlockSelectButton( const isMatch = useShortcutEventMatch(); const isSticky = blockInformation?.positionType === 'sticky'; const images = useListViewImages( { clientId, isExpanded } ); - const { rootClientId } = useListViewContext(); + const { collapseAll, expand, rootClientId } = useListViewContext(); const positionLabel = blockInformation?.positionLabel ? sprintf( @@ -227,6 +228,17 @@ function ListViewBlockSelectButton( blockClientIds[ blockClientIds.length - 1 ], null ); + } else if ( isMatch( 'core/block-editor/collapse-list-view', event ) ) { + if ( event.defaultPrevented ) { + return; + } + event.preventDefault(); + const { firstBlockClientId } = getBlocksToUpdate(); + const blockParents = getBlockParents( firstBlockClientId, false ); + // Collapse all blocks. + collapseAll(); + // Expand all parents of the current block. + expand( blockParents ); } } diff --git a/packages/block-editor/src/components/list-view/index.js b/packages/block-editor/src/components/list-view/index.js index 03942d72a74b5..8a696c6f56c24 100644 --- a/packages/block-editor/src/components/list-view/index.js +++ b/packages/block-editor/src/components/list-view/index.js @@ -37,6 +37,7 @@ import ListViewDropIndicatorPreview from './drop-indicator'; import useBlockSelection from './use-block-selection'; import useListViewBlockIndexes from './use-list-view-block-indexes'; import useListViewClientIds from './use-list-view-client-ids'; +import useListViewCollapseItems from './use-list-view-collapse-items'; import useListViewDropZone from './use-list-view-drop-zone'; import useListViewExpandSelectedItem from './use-list-view-expand-selected-item'; import { store as blockEditorStore } from '../../store'; @@ -45,6 +46,9 @@ import { focusListItem } from './utils'; import useClipboardHandler from './use-clipboard-handler'; const expanded = ( state, action ) => { + if ( action.type === 'clear' ) { + return {}; + } if ( Array.isArray( action.clientIds ) ) { return { ...state, @@ -194,7 +198,10 @@ function ListViewComponent( if ( ! clientId ) { return; } - setExpandedState( { type: 'expand', clientIds: [ clientId ] } ); + const clientIds = Array.isArray( clientId ) + ? clientId + : [ clientId ]; + setExpandedState( { type: 'expand', clientIds } ); }, [ setExpandedState ] ); @@ -207,6 +214,9 @@ function ListViewComponent( }, [ setExpandedState ] ); + const collapseAll = useCallback( () => { + setExpandedState( { type: 'clear' } ); + }, [ setExpandedState ] ); const expandRow = useCallback( ( row ) => { expand( row?.dataset?.block ); @@ -232,6 +242,11 @@ function ListViewComponent( [ updateBlockSelection ] ); + useListViewCollapseItems( { + collapseAll, + expand, + } ); + const firstDraggedBlockClientId = draggedClientIds?.[ 0 ]; // Convert a blockDropTarget into indexes relative to the blocks in the list view. @@ -282,6 +297,7 @@ function ListViewComponent( expand, firstDraggedBlockIndex, collapse, + collapseAll, BlockSettingsMenu, listViewInstanceId: instanceId, AdditionalBlockContent, @@ -299,6 +315,7 @@ function ListViewComponent( expand, firstDraggedBlockIndex, collapse, + collapseAll, BlockSettingsMenu, instanceId, AdditionalBlockContent, diff --git a/packages/block-editor/src/components/list-view/use-list-view-collapse-items.js b/packages/block-editor/src/components/list-view/use-list-view-collapse-items.js new file mode 100644 index 0000000000000..930c6424c3b0b --- /dev/null +++ b/packages/block-editor/src/components/list-view/use-list-view-collapse-items.js @@ -0,0 +1,33 @@ +/** + * WordPress dependencies + */ +import { useEffect } from '@wordpress/element'; +import { useSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import { store as blockEditorStore } from '../../store'; +import { unlock } from '../../lock-unlock'; + +export default function useListViewCollapseItems( { collapseAll, expand } ) { + const { expandedBlock, getBlockParents } = useSelect( ( select ) => { + const { getBlockParents: _getBlockParents, getExpandedBlock } = unlock( + select( blockEditorStore ) + ); + return { + expandedBlock: getExpandedBlock(), + getBlockParents: _getBlockParents, + }; + }, [] ); + + // Collapse all but the specified block when the expanded block client Id changes. + useEffect( () => { + if ( expandedBlock ) { + const blockParents = getBlockParents( expandedBlock, false ); + // Collapse all blocks and expand the block's parents. + collapseAll(); + expand( blockParents ); + } + }, [ collapseAll, expand, expandedBlock, getBlockParents ] ); +} diff --git a/packages/block-editor/src/hooks/background.js b/packages/block-editor/src/hooks/background.js index 5a1a00306973f..7c8d62d5dd5a9 100644 --- a/packages/block-editor/src/hooks/background.js +++ b/packages/block-editor/src/hooks/background.js @@ -1,77 +1,28 @@ -/** - * External dependencies - */ -import classnames from 'classnames'; - /** * WordPress dependencies */ -import { isBlobURL } from '@wordpress/blob'; import { getBlockSupport } from '@wordpress/blocks'; -import { focus } from '@wordpress/dom'; -import { - ToggleControl, - __experimentalToggleGroupControl as ToggleGroupControl, - __experimentalToggleGroupControlOption as ToggleGroupControlOption, - __experimentalToolsPanelItem as ToolsPanelItem, - __experimentalUnitControl as UnitControl, - __experimentalVStack as VStack, - DropZone, - FlexItem, - FocalPointPicker, - MenuItem, - VisuallyHidden, - __experimentalItemGroup as ItemGroup, - __experimentalHStack as HStack, - __experimentalTruncate as Truncate, -} from '@wordpress/components'; -import { useDispatch, useSelect } from '@wordpress/data'; -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 { useSelect } from '@wordpress/data'; +import { useCallback } from '@wordpress/element'; /** * Internal dependencies */ import InspectorControls from '../components/inspector-controls'; -import MediaReplaceFlow from '../components/media-replace-flow'; -import { useSettings } from '../components/use-settings'; import { cleanEmptyObject } from './utils'; import { store as blockEditorStore } from '../store'; +import { + default as StylesBackgroundPanel, + useHasBackgroundPanel, + hasBackgroundImageValue, +} from '../components/global-styles/background-panel'; export const BACKGROUND_SUPPORT_KEY = 'background'; -export const IMAGE_BACKGROUND_TYPE = 'image'; - -/** - * Checks if there is a current value in the background image block support - * attributes. - * - * @param {Object} style Style attribute. - * @return {boolean} Whether or not the block has a background image value set. - */ -export function hasBackgroundImageValue( style ) { - const hasValue = - !! style?.background?.backgroundImage?.id || - !! style?.background?.backgroundImage?.url; - - return hasValue; -} -/** - * Checks if there is a current value in the background size block support - * attributes. Background size values include background size as well - * as background position. - * - * @param {Object} style Style attribute. - * @return {boolean} Whether or not the block has a background size value set. - */ -export function hasBackgroundSizeValue( style ) { - return ( - style?.background?.backgroundPosition !== undefined || - style?.background?.backgroundSize !== undefined - ); -} +// Initial control values where no block style is set. +const BACKGROUND_BLOCK_DEFAULT_VALUES = { + backgroundSize: 'cover', +}; /** * Determine whether there is block support for background. @@ -82,10 +33,6 @@ export function hasBackgroundSizeValue( style ) { * @return {boolean} Whether there is support. */ export function hasBackgroundSupport( blockName, feature = 'any' ) { - if ( Platform.OS !== 'web' ) { - return false; - } - const support = getBlockSupport( blockName, BACKGROUND_SUPPORT_KEY ); if ( support === true ) { @@ -103,84 +50,54 @@ export function hasBackgroundSupport( blockName, feature = 'any' ) { return !! support?.[ feature ]; } -function useBlockProps( { name, style } ) { - if ( - ! hasBackgroundSupport( name ) || - ! style?.background?.backgroundImage - ) { +export function setBackgroundStyleDefaults( backgroundStyle ) { + if ( ! backgroundStyle ) { return; } - const backgroundImage = style?.background?.backgroundImage; - let props; + const backgroundImage = backgroundStyle?.backgroundImage; + let backgroundStylesWithDefaults; // Set block background defaults. if ( backgroundImage?.source === 'file' && !! backgroundImage?.url ) { - if ( ! style?.background?.backgroundSize ) { - props = { - style: { - backgroundSize: 'cover', - }, + if ( ! backgroundStyle?.backgroundSize ) { + backgroundStylesWithDefaults = { + backgroundSize: 'cover', }; } if ( - 'contain' === style?.background?.backgroundSize && - ! style?.background?.backgroundPosition + 'contain' === backgroundStyle?.backgroundSize && + ! backgroundStyle?.backgroundPosition ) { - props = { - style: { - backgroundPosition: 'center', - }, + backgroundStylesWithDefaults = { + backgroundPosition: 'center', }; } } - if ( ! props ) { + return backgroundStylesWithDefaults; +} + +function useBlockProps( { name, style } ) { + if ( + ! hasBackgroundSupport( name ) || + ! style?.background?.backgroundImage + ) { return; } - return props; -} + const backgroundStyles = setBackgroundStyleDefaults( style?.background ); -/** - * 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} style Style attribute. - * @param {Function} setAttributes Function to set block's attributes. - */ -export function resetBackgroundImage( style = {}, setAttributes ) { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundImage: undefined, - }, - } ), - } ); -} + if ( ! backgroundStyles ) { + return; + } -/** - * Resets the background size block support attributes. This can be used when disabling - * the background size controls for a block via a `ToolsPanel`. - * - * @param {Object} style Style attribute. - * @param {Function} setAttributes Function to set block's attributes. - */ -function resetBackgroundSize( style = {}, setAttributes ) { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundPosition: undefined, - backgroundRepeat: undefined, - backgroundSize: undefined, - }, - } ), - } ); + return { + style: { + ...backgroundStyles, + }, + }; } /** @@ -194,252 +111,28 @@ export function getBackgroundImageClasses( style ) { return hasBackgroundImageValue( style ) ? 'has-background' : ''; } -function InspectorImagePreview( { label, filename, url: imgUrl } ) { - const imgLabel = label || getFilename( imgUrl ); - return ( - - - - { imgUrl && ( - - ) } - - - - { imgLabel } - - - { filename - ? sprintf( - /* translators: %s: file name */ - __( 'Selected image: %s' ), - filename - ) - : __( 'No image selected' ) } - - - - - ); -} - -function BackgroundImagePanelItem( { - clientId, - isShownByDefault, - setAttributes, -} ) { - const { style, mediaUpload } = useSelect( - ( select ) => { - const { getBlockAttributes, getSettings } = - select( blockEditorStore ); - - return { - style: getBlockAttributes( clientId )?.style, - mediaUpload: getSettings().mediaUpload, - }; - }, - [ clientId ] - ); - const { id, title, url } = style?.background?.backgroundImage || {}; - - const replaceContainerRef = useRef(); - - const { createErrorNotice } = useDispatch( noticesStore ); - const onUploadError = ( message ) => { - createErrorNotice( message, { type: 'snackbar' } ); - }; - - const onSelectMedia = ( media ) => { - if ( ! media || ! media.url ) { - const newStyle = { - ...style, - background: { - ...style?.background, - backgroundImage: undefined, - }, - }; - - const newAttributes = { - style: cleanEmptyObject( newStyle ), - }; - - setAttributes( newAttributes ); - return; - } - - if ( isBlobURL( media.url ) ) { - return; - } - - // For media selections originated from a file upload. - if ( - ( media.media_type && - media.media_type !== IMAGE_BACKGROUND_TYPE ) || - ( ! media.media_type && - media.type && - media.type !== IMAGE_BACKGROUND_TYPE ) - ) { - onUploadError( - __( 'Only images can be used as a background image.' ) - ); - return; - } - - const newStyle = { - ...style, - background: { - ...style?.background, - backgroundImage: { - url: media.url, - id: media.id, - source: 'file', - title: media.title || undefined, - }, - }, - }; - - const newAttributes = { - style: cleanEmptyObject( newStyle ), - }; - - setAttributes( newAttributes ); - }; - - const onFilesDrop = ( filesList ) => { - mediaUpload( { - allowedTypes: [ 'image' ], - filesList, - onFileChange( [ image ] ) { - if ( isBlobURL( image?.url ) ) { - return; - } - onSelectMedia( image ); - }, - onError: onUploadError, - } ); - }; - - const resetAllFilter = useCallback( ( previousValue ) => { +function BackgroundInspectorControl( { children } ) { + const resetAllFilter = useCallback( ( attributes ) => { return { - ...previousValue, + ...attributes, style: { - ...previousValue.style, + ...attributes.style, background: undefined, }, }; }, [] ); - - const hasValue = hasBackgroundImageValue( style ); - return ( - hasValue } - label={ __( 'Background image' ) } - onDeselect={ () => resetBackgroundImage( style, setAttributes ) } - isShownByDefault={ isShownByDefault } - resetAllFilter={ resetAllFilter } - panelId={ clientId } - > -
- - } - variant="secondary" - > - { hasValue && ( - { - const [ toggleButton ] = focus.tabbable.find( - replaceContainerRef.current - ); - // Focus the toggle button and close the dropdown menu. - // This ensures similar behaviour as to selecting an image, where the dropdown is - // closed and focus is redirected to the dropdown toggle button. - toggleButton?.focus(); - toggleButton?.click(); - resetBackgroundImage( style, setAttributes ); - } } - > - { __( 'Reset ' ) } - - ) } - - -
-
+ + { children } + ); } -function backgroundSizeHelpText( value ) { - if ( value === 'cover' || value === undefined ) { - return __( 'Image covers the space evenly.' ); - } - if ( value === 'contain' ) { - return __( 'Image is contained without distortion.' ); - } - return __( 'Specify a fixed width.' ); -} - -export const coordsToBackgroundPosition = ( value ) => { - if ( ! value || ( isNaN( value.x ) && isNaN( value.y ) ) ) { - return undefined; - } - - const x = isNaN( value.x ) ? 0.5 : value.x; - const y = isNaN( value.y ) ? 0.5 : value.y; - - return `${ x * 100 }% ${ y * 100 }%`; -}; - -export const backgroundPositionToCoords = ( value ) => { - if ( ! value ) { - return { x: undefined, y: undefined }; - } - - let [ x, y ] = value.split( ' ' ).map( ( v ) => parseFloat( v ) / 100 ); - x = isNaN( x ) ? undefined : x; - y = isNaN( y ) ? x : y; - - return { x, y }; -}; - -function BackgroundSizePanelItem( { +export function BackgroundImagePanel( { clientId, - isShownByDefault, + name, setAttributes, + settings, } ) { const style = useSelect( ( select ) => @@ -447,198 +140,44 @@ function BackgroundSizePanelItem( { [ clientId ] ); - const sizeValue = style?.background?.backgroundSize; - const repeatValue = style?.background?.backgroundRepeat; - - // An `undefined` value is treated as `cover` by the toggle group control. - // An empty string is treated as `auto` by the toggle group control. This - // allows a user to select "Size" and then enter a custom value, with an - // empty value being treated as `auto`. - const currentValueForToggle = - ( sizeValue !== undefined && - sizeValue !== 'cover' && - sizeValue !== 'contain' ) || - sizeValue === '' - ? 'auto' - : sizeValue || 'cover'; - - // If the current value is `cover` and the repeat value is `undefined`, then - // the toggle should be unchecked as the default state. Otherwise, the toggle - // should reflect the current repeat value. - const repeatCheckedValue = ! ( - repeatValue === 'no-repeat' || - ( currentValueForToggle === 'cover' && repeatValue === undefined ) - ); - - const hasValue = hasBackgroundSizeValue( style ); - - const resetAllFilter = useCallback( ( previousValue ) => { - return { - ...previousValue, - style: { - ...previousValue.style, - background: { - ...previousValue.style?.background, - backgroundRepeat: undefined, - backgroundSize: undefined, - }, - }, - }; - }, [] ); - - const updateBackgroundSize = ( next ) => { - // When switching to 'contain' toggle the repeat off. - let nextRepeat = repeatValue; - - if ( next === 'contain' ) { - nextRepeat = 'no-repeat'; - } - - if ( - ( currentValueForToggle === 'cover' || - currentValueForToggle === 'contain' ) && - next === 'auto' - ) { - nextRepeat = undefined; - } - - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundRepeat: nextRepeat, - backgroundSize: next, - }, - } ), - } ); - }; - - const updateBackgroundPosition = ( next ) => { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundPosition: coordsToBackgroundPosition( next ), - }, - } ), - } ); - }; - - const toggleIsRepeated = () => { - setAttributes( { - style: cleanEmptyObject( { - ...style, - background: { - ...style?.background, - backgroundRepeat: - repeatCheckedValue === true ? 'no-repeat' : undefined, - }, - } ), - } ); - }; - - return ( - hasValue } - label={ __( 'Size' ) } - onDeselect={ () => resetBackgroundSize( style, setAttributes ) } - isShownByDefault={ isShownByDefault } - resetAllFilter={ resetAllFilter } - panelId={ clientId } - > - - - - - - - { sizeValue !== undefined && - sizeValue !== 'cover' && - sizeValue !== 'contain' ? ( - - ) : null } - { currentValueForToggle !== 'cover' && ( - - ) } - - ); -} - -export function BackgroundImagePanel( props ) { - const [ backgroundImage, backgroundSize ] = useSettings( - 'background.backgroundImage', - 'background.backgroundSize' - ); - if ( - ! backgroundImage || - ! hasBackgroundSupport( props.name, 'backgroundImage' ) + ! useHasBackgroundPanel( settings ) || + ! hasBackgroundSupport( name, 'backgroundImage' ) ) { return null; } - const showBackgroundSize = !! ( - backgroundSize && hasBackgroundSupport( props.name, 'backgroundSize' ) - ); - - const defaultControls = getBlockSupport( props.name, [ + const defaultControls = getBlockSupport( name, [ BACKGROUND_SUPPORT_KEY, '__experimentalDefaultControls', ] ); + const onChange = ( newStyle ) => { + setAttributes( { + style: cleanEmptyObject( newStyle ), + } ); + }; + + const updatedSettings = { + ...settings, + background: { + ...settings.background, + backgroundSize: + settings?.background?.backgroundSize && + hasBackgroundSupport( name, 'backgroundSize' ), + }, + }; + return ( - - - { showBackgroundSize && ( - - ) } - + ); } diff --git a/packages/block-editor/src/hooks/background.scss b/packages/block-editor/src/hooks/background.scss deleted file mode 100644 index a81b6acfce2de..0000000000000 --- a/packages/block-editor/src/hooks/background.scss +++ /dev/null @@ -1,75 +0,0 @@ -.block-editor-hooks__background__inspector-media-replace-container { - position: relative; - // Since there is no option to skip rendering the drag'n'drop icon in drop - // zone, we hide it for now. - .components-drop-zone__content-icon { - display: none; - } - - button.components-button { - color: $gray-900; - box-shadow: inset 0 0 0 $border-width $gray-300; - width: 100%; - display: block; - height: $grid-unit-50; - - &:hover { - color: var(--wp-admin-theme-color); - } - - &:focus { - box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); - } - } - - .block-editor-hooks__background__inspector-media-replace-title { - word-break: break-all; - // The Button component is white-space: nowrap, and that won't work with line-clamp. - white-space: normal; - - // Without this, the ellipsis can sometimes be partially hidden by the Button padding. - text-align: start; - text-align-last: center; - } - - .components-dropdown { - display: block; - } -} - -.block-editor-hooks__background__inspector-image-indicator-wrapper { - background: #fff linear-gradient(-45deg, transparent 48%, $gray-300 48%, $gray-300 52%, transparent 52%); // Show a diagonal line (crossed out) for empty background image. - border-radius: $radius-round !important; // Override the default border-radius inherited from FlexItem. - box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); - display: block; - width: 20px; - height: 20px; - flex: none; - - &.has-image { - background: #fff; // No diagonal line for non-empty background image. A background color is in use to account for partially transparent images. - } -} - -.block-editor-hooks__background__inspector-image-indicator { - background-size: cover; - border-radius: $radius-round; - width: 20px; - height: 20px; - display: block; - position: relative; -} - -.block-editor-hooks__background__inspector-image-indicator::after { - content: ""; - position: absolute; - top: -1px; - left: -1px; - bottom: -1px; - right: -1px; - border-radius: $radius-round; - box-shadow: inset 0 0 0 $border-width rgba(0, 0, 0, 0.2); - // Show a thin outline in Windows high contrast mode, otherwise the button is invisible. - border: 1px solid transparent; - box-sizing: inherit; -} diff --git a/packages/block-editor/src/hooks/use-zoom-out.js b/packages/block-editor/src/hooks/use-zoom-out.js index 8c2aff8819b63..84603c0161dd4 100644 --- a/packages/block-editor/src/hooks/use-zoom-out.js +++ b/packages/block-editor/src/hooks/use-zoom-out.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useRef } from '@wordpress/element'; +import { useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -20,26 +20,15 @@ export function useZoomOut() { }; }, [] ); - const shouldRevertInitialMode = useRef( null ); - useEffect( () => { - // ignore changes to zoom-out mode as we explictily change to it on mount. - if ( mode !== 'zoom-out' ) { - shouldRevertInitialMode.current = false; - } - }, [ mode ] ); - // Intentionality left without any dependency. - // This effect should only run the first time the component is rendered. + // This effect should only run when the component is rendered and unmounted. // The effect opens the zoom-out view if it is not open before when applying a style variation. useEffect( () => { if ( mode !== 'zoom-out' ) { __unstableSetEditorMode( 'zoom-out' ); - shouldRevertInitialMode.current = true; return () => { - // if there were not mode changes revert to the initial mode when unmounting. - if ( shouldRevertInitialMode.current ) { - __unstableSetEditorMode( mode ); - } + // Revert to original mode + __unstableSetEditorMode( mode ); }; } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/block-editor/src/private-apis.js b/packages/block-editor/src/private-apis.js index 92da4b8719632..b51c019e79178 100644 --- a/packages/block-editor/src/private-apis.js +++ b/packages/block-editor/src/private-apis.js @@ -8,6 +8,11 @@ import { getRichTextValues } from './components/rich-text/get-rich-text-values'; import ResizableBoxPopover from './components/resizable-box-popover'; import { ComposedPrivateInserter as PrivateInserter } from './components/inserter'; import { default as PrivateQuickInserter } from './components/inserter/quick-inserter'; +import { + extractWords, + getNormalizedSearchTerms, + normalizeString, +} from './components/inserter/search-items'; import { PrivateListView } from './components/list-view'; import BlockInfo from './components/block-info-slot-fill'; import { useShowBlockTools } from './components/block-tools/use-show-block-tools'; @@ -26,7 +31,10 @@ import { usesContextKey } from './components/rich-text/format-edit'; import { ExperimentalBlockCanvas } from './components/block-canvas'; import { getDuotoneFilter } from './components/duotone/utils'; import { useFlashEditableBlocks } from './components/use-flash-editable-blocks'; -import { selectBlockPatternsKey } from './store/private-keys'; +import { + selectBlockPatternsKey, + reusableBlocksSelectKey, +} from './store/private-keys'; import { requiresWrapperOnCopy } from './components/writing-flow/utils'; import { PrivateRichText } from './components/rich-text/'; @@ -42,6 +50,9 @@ lock( privateApis, { getRichTextValues, PrivateInserter, PrivateQuickInserter, + extractWords, + getNormalizedSearchTerms, + normalizeString, PrivateListView, ResizableBoxPopover, BlockInfo, @@ -62,4 +73,5 @@ lock( privateApis, { selectBlockPatternsKey, requiresWrapperOnCopy, PrivateRichText, + reusableBlocksSelectKey, } ); diff --git a/packages/block-editor/src/store/private-actions.js b/packages/block-editor/src/store/private-actions.js index d402d45657704..85d624e048318 100644 --- a/packages/block-editor/src/store/private-actions.js +++ b/packages/block-editor/src/store/private-actions.js @@ -376,3 +376,15 @@ export function stopDragging() { type: 'STOP_DRAGGING', }; } + +/** + * @param {string|null} clientId The block's clientId, or `null` to clear. + * + * @return {Object} Action object. + */ +export function expandBlock( clientId ) { + return { + type: 'SET_BLOCK_EXPANDED_IN_LIST_VIEW', + clientId, + }; +} diff --git a/packages/block-editor/src/store/private-keys.js b/packages/block-editor/src/store/private-keys.js index 8bfa4bb68297f..f48612e7491c9 100644 --- a/packages/block-editor/src/store/private-keys.js +++ b/packages/block-editor/src/store/private-keys.js @@ -1 +1,2 @@ export const selectBlockPatternsKey = Symbol( 'selectBlockPatternsKey' ); +export const reusableBlocksSelectKey = Symbol( 'reusableBlocksSelect' ); diff --git a/packages/block-editor/src/store/private-selectors.js b/packages/block-editor/src/store/private-selectors.js index e4885cbbd9e1e..75163273008c4 100644 --- a/packages/block-editor/src/store/private-selectors.js +++ b/packages/block-editor/src/store/private-selectors.js @@ -18,11 +18,18 @@ import { getSettings, canInsertBlockType, } from './selectors'; -import { checkAllowListRecursive, getAllPatternsDependants } from './utils'; +import { + checkAllowListRecursive, + getAllPatternsDependants, + getInsertBlockTypeDependants, +} from './utils'; import { INSERTER_PATTERN_TYPES } from '../components/inserter/block-patterns-tab/utils'; import { STORE_NAME } from './constants'; import { unlock } from '../lock-unlock'; -import { selectBlockPatternsKey } from './private-keys'; +import { + selectBlockPatternsKey, + reusableBlocksSelectKey, +} from './private-keys'; export { getBlockSettings } from './get-block-settings'; @@ -282,11 +289,8 @@ export const hasAllowedPatterns = createRegistrySelector( ( select ) => } ); }, ( state, rootClientId ) => [ - getAllPatternsDependants( select )( state ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), + ...getAllPatternsDependants( select )( state ), + ...getInsertBlockTypeDependants( state, rootClientId ), ] ) ); @@ -299,26 +303,27 @@ export const getAllPatterns = createRegistrySelector( ( select ) => __experimentalUserPatternCategories = [], __experimentalReusableBlocks = [], } = state.settings; - const userPatterns = ( __experimentalReusableBlocks ?? [] ).map( - ( userPattern ) => { - return { - name: `core/block/${ userPattern.id }`, - id: userPattern.id, - type: INSERTER_PATTERN_TYPES.user, - title: userPattern.title.raw, - categories: userPattern.wp_pattern_category.map( - ( catId ) => { - const category = ( - __experimentalUserPatternCategories ?? [] - ).find( ( { id } ) => id === catId ); - return category ? category.slug : catId; - } - ), - content: userPattern.content.raw, - syncStatus: userPattern.wp_pattern_sync_status, - }; - } - ); + const reusableBlocksSelect = state.settings[ reusableBlocksSelectKey ]; + const userPatterns = ( + reusableBlocksSelect + ? reusableBlocksSelect( select ) + : __experimentalReusableBlocks ?? [] + ).map( ( userPattern ) => { + return { + name: `core/block/${ userPattern.id }`, + id: userPattern.id, + type: INSERTER_PATTERN_TYPES.user, + title: userPattern.title.raw, + categories: userPattern.wp_pattern_category.map( ( catId ) => { + const category = ( + __experimentalUserPatternCategories ?? [] + ).find( ( { id } ) => id === catId ); + return category ? category.slug : catId; + } ), + content: userPattern.content.raw, + syncStatus: userPattern.wp_pattern_sync_status, + }; + } ); return [ ...userPatterns, ...__experimentalBlockPatterns, @@ -330,6 +335,17 @@ export const getAllPatterns = createRegistrySelector( ( select ) => }, getAllPatternsDependants( select ) ) ); +const EMPTY_ARRAY = []; + +export const getReusableBlocks = createRegistrySelector( + ( select ) => ( state ) => { + const reusableBlocksSelect = state.settings[ reusableBlocksSelectKey ]; + return reusableBlocksSelect + ? reusableBlocksSelect( select ) + : state.settings.__experimentalReusableBlocks ?? EMPTY_ARRAY; + } +); + /** * Returns the element of the last element that had focus when focus left the editor canvas. * @@ -353,3 +369,14 @@ export function getLastFocus( state ) { export function isDragging( state ) { return state.isDragging; } + +/** + * Retrieves the expanded block from the state. + * + * @param {Object} state Block editor state. + * + * @return {string|null} The client ID of the expanded block, if set. + */ +export function getExpandedBlock( state ) { + return state.expandedBlock; +} diff --git a/packages/block-editor/src/store/reducer.js b/packages/block-editor/src/store/reducer.js index e836a44e12012..13024d4d2e8fa 100644 --- a/packages/block-editor/src/store/reducer.js +++ b/packages/block-editor/src/store/reducer.js @@ -1889,6 +1889,27 @@ export function highlightedBlock( state, action ) { return state; } +/** + * Reducer returning current expanded block in the list view. + * + * @param {string|null} state Current expanded block. + * @param {Object} action Dispatched action. + * + * @return {string|null} Updated state. + */ +export function expandedBlock( state = null, action ) { + switch ( action.type ) { + case 'SET_BLOCK_EXPANDED_IN_LIST_VIEW': + return action.clientId; + case 'SELECT_BLOCK': + if ( action.clientId !== state ) { + return null; + } + } + + return state; +} + /** * Reducer returning the block insertion event list state. * @@ -2064,6 +2085,7 @@ const combinedReducers = combineReducers( { lastFocus, editorMode, hasBlockMovingClientId, + expandedBlock, highlightedBlock, lastBlockInserted, temporarilyEditingAsBlocks, diff --git a/packages/block-editor/src/store/selectors.js b/packages/block-editor/src/store/selectors.js index 8f2e5e4e5ccc8..d1a52ab8cb6a0 100644 --- a/packages/block-editor/src/store/selectors.js +++ b/packages/block-editor/src/store/selectors.js @@ -30,6 +30,7 @@ import { checkAllowListRecursive, checkAllowList, getAllPatternsDependants, + getInsertBlockTypeDependants, } from './utils'; import { orderBy } from '../utils/sorting'; import { STORE_NAME } from './constants'; @@ -1666,13 +1667,8 @@ const canInsertBlockTypeUnmemoized = ( */ export const canInsertBlockType = createSelector( canInsertBlockTypeUnmemoized, - ( state, blockName, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.blockEditingModes, - ] + ( state, blockName, rootClientId ) => + getInsertBlockTypeDependants( state, rootClientId ) ); /** @@ -1977,95 +1973,108 @@ const buildBlockTypeItem = * this item. * @property {number} frecency Heuristic that combines frequency and recency. */ -export const getInserterItems = createSelector( - ( state, rootClientId = null ) => { - const buildReusableBlockInserterItem = ( reusableBlock ) => { - const icon = ! reusableBlock.wp_pattern_sync_status - ? { - src: symbol, - foreground: 'var(--wp-block-synced-color)', - } - : symbol; - const id = `core/block/${ reusableBlock.id }`; - const { time, count = 0 } = getInsertUsage( state, id ) || {}; - const frecency = calculateFrecency( time, count ); - - return { - id, - name: 'core/block', - initialAttributes: { ref: reusableBlock.id }, - title: reusableBlock.title?.raw, - icon, - category: 'reusable', - keywords: [ 'reusable' ], - isDisabled: false, - utility: 1, // Deprecated. - frecency, - content: reusableBlock.content?.raw, - syncStatus: reusableBlock.wp_pattern_sync_status, +export const getInserterItems = createRegistrySelector( ( select ) => + createSelector( + ( state, rootClientId = null ) => { + const buildReusableBlockInserterItem = ( reusableBlock ) => { + const icon = ! reusableBlock.wp_pattern_sync_status + ? { + src: symbol, + foreground: 'var(--wp-block-synced-color)', + } + : symbol; + const id = `core/block/${ reusableBlock.id }`; + const { time, count = 0 } = getInsertUsage( state, id ) || {}; + const frecency = calculateFrecency( time, count ); + + return { + id, + name: 'core/block', + initialAttributes: { ref: reusableBlock.id }, + title: reusableBlock.title?.raw, + icon, + category: 'reusable', + keywords: [ 'reusable' ], + isDisabled: false, + utility: 1, // Deprecated. + frecency, + content: reusableBlock.content?.raw, + syncStatus: reusableBlock.wp_pattern_sync_status, + }; }; - }; - - const syncedPatternInserterItems = canInsertBlockTypeUnmemoized( - state, - 'core/block', - rootClientId - ) - ? getReusableBlocks( state ).map( buildReusableBlockInserterItem ) - : []; - - const buildBlockTypeInserterItem = buildBlockTypeItem( state, { - buildScope: 'inserter', - } ); - const blockTypeInserterItems = getBlockTypes() - .filter( ( blockType ) => - canIncludeBlockTypeInInserter( state, blockType, rootClientId ) + const syncedPatternInserterItems = canInsertBlockTypeUnmemoized( + state, + 'core/block', + rootClientId ) - .map( buildBlockTypeInserterItem ); + ? unlock( select( STORE_NAME ) ) + .getReusableBlocks() + .map( buildReusableBlockInserterItem ) + : []; - const items = blockTypeInserterItems.reduce( ( accumulator, item ) => { - const { variations = [] } = item; - // Exclude any block type item that is to be replaced by a default variation. - if ( ! variations.some( ( { isDefault } ) => isDefault ) ) { - accumulator.push( item ); - } - if ( variations.length ) { - const variationMapper = getItemFromVariation( state, item ); - accumulator.push( ...variations.map( variationMapper ) ); - } - return accumulator; - }, [] ); + const buildBlockTypeInserterItem = buildBlockTypeItem( state, { + buildScope: 'inserter', + } ); - // Ensure core blocks are prioritized in the returned results, - // because third party blocks can be registered earlier than - // the core blocks (usually by using the `init` action), - // thus affecting the display order. - // We don't sort reusable blocks as they are handled differently. - const groupByType = ( blocks, block ) => { - const { core, noncore } = blocks; - const type = block.name.startsWith( 'core/' ) ? core : noncore; - - type.push( block ); - return blocks; - }; - const { core: coreItems, noncore: nonCoreItems } = items.reduce( - groupByType, - { core: [], noncore: [] } - ); - const sortedBlockTypes = [ ...coreItems, ...nonCoreItems ]; - return [ ...sortedBlockTypes, ...syncedPatternInserterItems ]; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.blocks.order, - state.preferences.insertUsage, - state.settings.allowedBlockTypes, - state.settings.templateLock, - getReusableBlocks( state ), - getBlockTypes(), - ] + const blockTypeInserterItems = getBlockTypes() + .filter( ( blockType ) => + canIncludeBlockTypeInInserter( + state, + blockType, + rootClientId + ) + ) + .map( buildBlockTypeInserterItem ); + + const items = blockTypeInserterItems.reduce( + ( accumulator, item ) => { + const { variations = [] } = item; + // Exclude any block type item that is to be replaced by a default variation. + if ( ! variations.some( ( { isDefault } ) => isDefault ) ) { + accumulator.push( item ); + } + if ( variations.length ) { + const variationMapper = getItemFromVariation( + state, + item + ); + accumulator.push( + ...variations.map( variationMapper ) + ); + } + return accumulator; + }, + [] + ); + + // Ensure core blocks are prioritized in the returned results, + // because third party blocks can be registered earlier than + // the core blocks (usually by using the `init` action), + // thus affecting the display order. + // We don't sort reusable blocks as they are handled differently. + const groupByType = ( blocks, block ) => { + const { core, noncore } = blocks; + const type = block.name.startsWith( 'core/' ) ? core : noncore; + + type.push( block ); + return blocks; + }; + const { core: coreItems, noncore: nonCoreItems } = items.reduce( + groupByType, + { core: [], noncore: [] } + ); + const sortedBlockTypes = [ ...coreItems, ...nonCoreItems ]; + return [ ...sortedBlockTypes, ...syncedPatternInserterItems ]; + }, + ( state, rootClientId ) => [ + getBlockTypes(), + unlock( select( STORE_NAME ) ).getReusableBlocks(), + state.blocks.order, + state.preferences.insertUsage, + ...getInsertBlockTypeDependants( state, rootClientId ), + ] + ) ); /** @@ -2128,12 +2137,9 @@ export const getBlockTransformItems = createSelector( ); }, ( state, blocks, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.preferences.insertUsage, - state.settings.allowedBlockTypes, - state.settings.templateLock, getBlockTypes(), + state.preferences.insertUsage, + ...getInsertBlockTypeDependants( state, rootClientId ), ] ); @@ -2145,28 +2151,25 @@ export const getBlockTransformItems = createSelector( * * @return {boolean} Items that appear in inserter. */ -export const hasInserterItems = createSelector( - ( state, rootClientId = null ) => { - const hasBlockType = getBlockTypes().some( ( blockType ) => - canIncludeBlockTypeInInserter( state, blockType, rootClientId ) - ); - if ( hasBlockType ) { - return true; - } - const hasReusableBlock = - canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && - getReusableBlocks( state ).length > 0; +export const hasInserterItems = createRegistrySelector( + ( select ) => + ( state, rootClientId = null ) => { + const hasBlockType = getBlockTypes().some( ( blockType ) => + canIncludeBlockTypeInInserter( state, blockType, rootClientId ) + ); + if ( hasBlockType ) { + return true; + } + const hasReusableBlock = + canInsertBlockTypeUnmemoized( + state, + 'core/block', + rootClientId + ) && + unlock( select( STORE_NAME ) ).getReusableBlocks().length > 0; - return hasReusableBlock; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - getReusableBlocks( state ), - getBlockTypes(), - ] + return hasReusableBlock; + } ); /** @@ -2177,34 +2180,37 @@ export const hasInserterItems = createSelector( * * @return {Array?} The list of allowed block types. */ -export const getAllowedBlocks = createSelector( - ( state, rootClientId = null ) => { - if ( ! rootClientId ) { - return; - } +export const getAllowedBlocks = createRegistrySelector( ( select ) => + createSelector( + ( state, rootClientId = null ) => { + if ( ! rootClientId ) { + return; + } - const blockTypes = getBlockTypes().filter( ( blockType ) => - canIncludeBlockTypeInInserter( state, blockType, rootClientId ) - ); + const blockTypes = getBlockTypes().filter( ( blockType ) => + canIncludeBlockTypeInInserter( state, blockType, rootClientId ) + ); - const hasReusableBlock = - canInsertBlockTypeUnmemoized( state, 'core/block', rootClientId ) && - getReusableBlocks( state ).length > 0; + const hasReusableBlock = + canInsertBlockTypeUnmemoized( + state, + 'core/block', + rootClientId + ) && + unlock( select( STORE_NAME ) ).getReusableBlocks().length > 0; - if ( hasReusableBlock ) { - blockTypes.push( 'core/block' ); - } + if ( hasReusableBlock ) { + blockTypes.push( 'core/block' ); + } - return blockTypes; - }, - ( state, rootClientId ) => [ - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - getReusableBlocks( state ), - getBlockTypes(), - ] + return blockTypes; + }, + ( state, rootClientId ) => [ + getBlockTypes(), + unlock( select( STORE_NAME ) ).getReusableBlocks(), + ...getInsertBlockTypeDependants( state, rootClientId ), + ] + ) ); export const __experimentalGetAllowedBlocks = createSelector( @@ -2220,9 +2226,8 @@ export const __experimentalGetAllowedBlocks = createSelector( ); return getAllowedBlocks( state, rootClientId ); }, - ( state, rootClientId ) => [ - ...getAllowedBlocks.getDependants( state, rootClientId ), - ] + ( state, rootClientId ) => + getAllowedBlocks.getDependants( state, rootClientId ) ); /** @@ -2287,6 +2292,10 @@ export const __experimentalGetParsedPattern = createRegistrySelector( metadata: { ...( blocks[ 0 ].attributes.metadata || {} ), categories: pattern.categories, + patternName: pattern.name, + name: + blocks[ 0 ].attributes.metadata?.name || + pattern.title, }, }; } @@ -2297,15 +2306,10 @@ export const __experimentalGetParsedPattern = createRegistrySelector( }, getAllPatternsDependants( select ) ) ); -const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => { - return [ - ...getAllPatternsDependants( select )( state ), - state.settings.allowedBlockTypes, - state.settings.templateLock, - state.blockListSettings[ rootClientId ], - state.blocks.byClientId.get( rootClientId ), - ]; -}; +const getAllowedPatternsDependants = ( select ) => ( state, rootClientId ) => [ + ...getAllPatternsDependants( select )( state ), + ...getInsertBlockTypeDependants( state, rootClientId ), +]; /** * Returns the list of allowed patterns for inner blocks children. @@ -2526,18 +2530,21 @@ export const __experimentalGetBlockListSettingsForBlocks = createSelector( * * @return {string} The reusable block saved title. */ -export const __experimentalGetReusableBlockTitle = createSelector( - ( state, ref ) => { - const reusableBlock = getReusableBlocks( state ).find( - ( block ) => block.id === ref - ); - if ( ! reusableBlock ) { - return null; - } +export const __experimentalGetReusableBlockTitle = createRegistrySelector( + ( select ) => + createSelector( + ( state, ref ) => { + const reusableBlock = unlock( select( STORE_NAME ) ) + .getReusableBlocks() + .find( ( block ) => block.id === ref ); + if ( ! reusableBlock ) { + return null; + } - return reusableBlock.title?.raw; - }, - ( state ) => [ getReusableBlocks( state ) ] + return reusableBlock.title?.raw; + }, + () => [ unlock( select( STORE_NAME ) ).getReusableBlocks() ] + ) ); /** @@ -2571,17 +2578,6 @@ export function __experimentalGetLastBlockAttributeChanges( state ) { return state.lastBlockAttributesChange; } -/** - * Returns the available reusable blocks - * - * @param {Object} state Global application state. - * - * @return {Array} Reusable blocks - */ -function getReusableBlocks( state ) { - return state.settings.__experimentalReusableBlocks ?? EMPTY_ARRAY; -} - /** * Returns whether the navigation mode is enabled. * diff --git a/packages/block-editor/src/store/test/private-actions.js b/packages/block-editor/src/store/test/private-actions.js index 08370f731902d..7576b95866306 100644 --- a/packages/block-editor/src/store/test/private-actions.js +++ b/packages/block-editor/src/store/test/private-actions.js @@ -4,6 +4,7 @@ import { hideBlockInterface, showBlockInterface, + expandBlock, __experimentalUpdateSettings, setOpenedBlockSettingsMenu, startDragging, @@ -113,4 +114,13 @@ describe( 'private actions', () => { } ); } ); } ); + + describe( 'expandBlock', () => { + it( 'should return the SET_BLOCK_EXPANDED_IN_LIST_VIEW action', () => { + expect( expandBlock( 'block-1' ) ).toEqual( { + type: 'SET_BLOCK_EXPANDED_IN_LIST_VIEW', + clientId: 'block-1', + } ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/private-selectors.js b/packages/block-editor/src/store/test/private-selectors.js index f661271b570b4..185da1ffb9804 100644 --- a/packages/block-editor/src/store/test/private-selectors.js +++ b/packages/block-editor/src/store/test/private-selectors.js @@ -7,6 +7,7 @@ import { isBlockSubtreeDisabled, getEnabledClientIdsTree, getEnabledBlockParents, + getExpandedBlock, isDragging, } from '../private-selectors'; import { getBlockEditingMode } from '../selectors'; @@ -496,4 +497,16 @@ describe( 'private selectors', () => { expect( isDragging( state ) ).toBe( false ); } ); } ); + + describe( 'getExpandedBlock', () => { + it( 'should return the expanded block', () => { + const state = { + expandedBlock: '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f', + }; + + expect( getExpandedBlock( state ) ).toBe( + '9b9c5c3f-2e46-4f02-9e14-9fe9515b958f' + ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/reducer.js b/packages/block-editor/src/store/test/reducer.js index c99d914ba2175..cd472fa59ac72 100644 --- a/packages/block-editor/src/store/test/reducer.js +++ b/packages/block-editor/src/store/test/reducer.js @@ -35,6 +35,7 @@ import { lastBlockInserted, blockEditingModes, openedBlockSettingsMenu, + expandedBlock, } from '../reducer'; const noop = () => {}; @@ -3459,4 +3460,29 @@ describe( 'state', () => { expect( state ).toBe( null ); } ); } ); + + describe( 'expandedBlock', () => { + it( 'should return null by default', () => { + expect( expandedBlock( undefined, {} ) ).toBe( null ); + } ); + + it( 'should set client id for expanded block', () => { + const state = expandedBlock( null, { + type: 'SET_BLOCK_EXPANDED_IN_LIST_VIEW', + clientId: '14501cc2-90a6-4f52-aa36-ab6e896135d1', + } ); + expect( state ).toBe( '14501cc2-90a6-4f52-aa36-ab6e896135d1' ); + } ); + + it( 'should clear the state when a block is selected', () => { + const state = expandedBlock( + '14501cc2-90a6-4f52-aa36-ab6e896135d1', + { + type: 'SELECT_BLOCK', + clientId: 'a-different-block', + } + ); + expect( state ).toBe( null ); + } ); + } ); } ); diff --git a/packages/block-editor/src/store/test/selectors.js b/packages/block-editor/src/store/test/selectors.js index 29833611b17f4..85006621c4701 100644 --- a/packages/block-editor/src/store/test/selectors.js +++ b/packages/block-editor/src/store/test/selectors.js @@ -8,11 +8,13 @@ import { } from '@wordpress/blocks'; import { RawHTML } from '@wordpress/element'; import { symbol } from '@wordpress/icons'; +import { select, dispatch } from '@wordpress/data'; /** * Internal dependencies */ import * as selectors from '../selectors'; +import { store } from '../'; const { getBlockName, @@ -55,7 +57,6 @@ const { isSelectionEnabled, canInsertBlockType, canInsertBlocks, - getInserterItems, getBlockTransformItems, isValidTemplate, getTemplate, @@ -3286,41 +3287,26 @@ describe( 'selectors', () => { } ); describe( 'getInserterItems', () => { - it( 'should properly list block type and reusable block items', () => { - const state = { - blocks: { - byClientId: new Map(), - attributes: new Map(), - order: new Map(), - parents: new Map(), - tree: new Map( - Object.entries( { - '': { - innerBlocks: [], - }, - } ) - ), - }, - settings: { - __experimentalReusableBlocks: [ - { - id: 1, - isTemporary: false, - clientId: 'block1', - title: { raw: 'Reusable Block 1' }, - content: { raw: '' }, - }, - ], - }, - // Intentionally include a test case which considers - // `insertUsage` as not present within preferences. - // - // See: https://github.com/WordPress/gutenberg/issues/14580 - preferences: {}, - blockListSettings: {}, - blockEditingModes: new Map(), - }; - const items = getInserterItems( state ); + afterAll( async () => { + await dispatch( store ).updateSettings( { + __experimentalReusableBlocks: [], + } ); + await dispatch( store ).resetBlocks( [] ); + } ); + + it( 'should properly list block type and reusable block items', async () => { + await dispatch( store ).updateSettings( { + __experimentalReusableBlocks: [ + { + id: 1, + isTemporary: false, + clientId: 'block1', + title: { raw: 'Reusable Block 1' }, + content: { raw: '' }, + }, + ], + } ); + const items = select( store ).getInserterItems(); const testBlockAItem = items.find( ( item ) => item.id === 'core/test-block-a' ); @@ -3361,93 +3347,48 @@ describe( 'selectors', () => { } ); } ); - it( 'should correctly cache the return values', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block3: { name: 'core/test-block-a' }, - block4: { name: 'core/test-block-a' }, - } ) - ), - attributes: new Map( - Object.entries( { - block3: {}, - block4: {}, - } ) - ), - order: new Map( - Object.entries( { - '': [ 'block3', 'block4' ], - } ) - ), - parents: new Map( - Object.entries( { - block3: '', - block4: '', - } ) - ), - tree: new Map( - Object.entries( { - block3: { - clientId: 'block3', - name: 'core/test-block-a', - attributes: {}, - innerBlocks: [], - }, - block4: { - clientId: 'block4', - name: 'core/test-block-a', - attributes: {}, - innerBlocks: [], - }, - } ) - ), - controlledInnerBlocks: {}, - }, - settings: { - __experimentalReusableBlocks: [ - { - id: 1, - isTemporary: false, - clientId: 'block1', - title: { raw: 'Reusable Block 1' }, - content: { raw: '' }, - }, - { - id: 2, - isTemporary: false, - clientId: 'block2', - title: { raw: 'Reusable Block 2' }, - content: { raw: '' }, - }, - ], - }, - preferences: { - insertUsage: {}, - }, - blockListSettings: { - block3: {}, - block4: {}, - }, - blockEditingModes: new Map(), - }; - - const stateSecondBlockRestricted = { - ...state, - blockListSettings: { - ...state.blockListSettings, - block4: { - allowedBlocks: [ 'core/test-block-b' ], + it( 'should correctly cache the return values', async () => { + await dispatch( store ).updateSettings( { + __experimentalReusableBlocks: [ + { + id: 1, + isTemporary: false, + clientId: 'block1', + title: { raw: 'Reusable Block 1' }, + content: { raw: '' }, + }, + { + id: 2, + isTemporary: false, + clientId: 'block2', + title: { raw: 'Reusable Block 2' }, + content: { raw: '' }, }, + ], + } ); + await dispatch( store ).resetBlocks( [ + { + clientId: 'block3', + name: 'core/test-block-a', + innerBlocks: [], }, - }; + { + clientId: 'block4', + name: 'core/test-block-a', + innerBlocks: [], + }, + ] ); + await dispatch( store ).updateBlockListSettings( 'block3', {} ); + await dispatch( store ).updateBlockListSettings( 'block4', {} ); - const firstBlockFirstCall = getInserterItems( state, 'block3' ); - const firstBlockSecondCall = getInserterItems( - stateSecondBlockRestricted, - 'block3' - ); + const firstBlockFirstCall = + select( store ).getInserterItems( 'block3' ); + await dispatch( store ).updateBlockListSettings( 'block4', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + const firstBlockSecondCall = + select( store ).getInserterItems( 'block3' ); + await dispatch( store ).updateBlockListSettings( 'block4', {} ); expect( firstBlockFirstCall ).toBe( firstBlockSecondCall ); expect( firstBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-a', @@ -3459,11 +3400,14 @@ describe( 'selectors', () => { 'core/block/2', ] ); - const secondBlockFirstCall = getInserterItems( state, 'block4' ); - const secondBlockSecondCall = getInserterItems( - stateSecondBlockRestricted, - 'block4' - ); + const secondBlockFirstCall = + select( store ).getInserterItems( 'block4' ); + await dispatch( store ).updateBlockListSettings( 'block4', { + allowedBlocks: [ 'core/test-block-b' ], + } ); + const secondBlockSecondCall = + select( store ).getInserterItems( 'block4' ); + await dispatch( store ).updateBlockListSettings( 'block4', {} ); expect( secondBlockFirstCall ).not.toBe( secondBlockSecondCall ); expect( secondBlockFirstCall.map( ( item ) => item.id ) ).toEqual( [ 'core/test-block-a', @@ -3479,77 +3423,37 @@ describe( 'selectors', () => { ); } ); - it( 'should set isDisabled when a block with `multiple: false` has been used', () => { - const state = { - blocks: { - byClientId: new Map( - Object.entries( { - block1: { - clientId: 'block1', - name: 'core/test-block-b', - }, - } ) - ), - attributes: new Map( - Object.entries( { - block1: { attribute: {} }, - } ) - ), - order: new Map( - Object.entries( { - '': [ 'block1' ], - } ) - ), - tree: new Map( - Object.entries( { - block1: { - clientId: 'block1', - name: 'core/test-block-b', - attributes: {}, - innerBlocks: [], - }, - } ) - ), - controlledInnerBlocks: {}, - parents: new Map(), - }, - preferences: { - insertUsage: {}, + it( 'should set isDisabled when a block with `multiple: false` has been used', async () => { + await dispatch( store ).resetBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-b', + innerBlocks: [], }, - blockListSettings: {}, - settings: {}, - blockEditingModes: new Map(), - }; - const items = getInserterItems( state ); + ] ); + const items = select( store ).getInserterItems(); const testBlockBItem = items.find( ( item ) => item.id === 'core/test-block-b' ); expect( testBlockBItem.isDisabled ).toBe( true ); } ); - it( 'should set a frecency', () => { - const state = { - blocks: { - byClientId: new Map(), - attributes: new Map(), - order: new Map(), - parents: new Map(), - cache: {}, - }, - preferences: { - insertUsage: { - 'core/test-block-b': { count: 10, time: 1000 }, + it( 'should set a frecency', async () => { + for ( let i = 0; i < 10; i++ ) { + await dispatch( store ).insertBlocks( [ + { + clientId: 'block1', + name: 'core/test-block-b', + innerBlocks: [], }, - }, - blockListSettings: {}, - settings: {}, - blockEditingModes: new Map(), - }; - const items = getInserterItems( state ); + ] ); + } + + const items = select( store ).getInserterItems(); const reusableBlock2Item = items.find( ( item ) => item.id === 'core/test-block-b' ); - expect( reusableBlock2Item.frecency ).toBe( 2.5 ); + expect( reusableBlock2Item.frecency ).toBe( 40 ); } ); } ); @@ -4304,20 +4208,7 @@ describe( 'getInserterItems with core blocks prioritization', () => { ].forEach( unregisterBlockType ); } ); it( 'should prioritize core blocks by sorting them at the top of the returned list', () => { - const state = { - blocks: { - byClientId: new Map(), - attributes: new Map(), - order: new Map(), - parents: new Map(), - cache: {}, - }, - settings: {}, - preferences: {}, - blockListSettings: {}, - blockEditingModes: new Map(), - }; - const items = getInserterItems( state ); + const items = select( store ).getInserterItems(); const expectedResult = [ 'core/block', 'core/test-block-a', diff --git a/packages/block-editor/src/store/utils.js b/packages/block-editor/src/store/utils.js index 4d9d114946c1f..f236c4a7e56eb 100644 --- a/packages/block-editor/src/store/utils.js +++ b/packages/block-editor/src/store/utils.js @@ -2,6 +2,8 @@ * Internal dependencies */ import { selectBlockPatternsKey } from './private-keys'; +import { unlock } from '../lock-unlock'; +import { STORE_NAME } from './constants'; export const checkAllowList = ( list, item, defaultResult = null ) => { if ( typeof list === 'boolean' ) { @@ -52,5 +54,16 @@ export const getAllPatternsDependants = ( select ) => ( state ) => { state.settings.__experimentalReusableBlocks, state.settings[ selectBlockPatternsKey ]?.( select ), state.blockPatterns, + unlock( select( STORE_NAME ) ).getReusableBlocks(), ]; }; + +export function getInsertBlockTypeDependants( state, rootClientId ) { + return [ + state.blockListSettings[ rootClientId ], + state.blocks.byClientId.get( rootClientId ), + state.settings.allowedBlockTypes, + state.settings.templateLock, + state.blockEditingModes, + ]; +} diff --git a/packages/block-editor/src/style.scss b/packages/block-editor/src/style.scss index 015cffde42a23..f3a38490be986 100644 --- a/packages/block-editor/src/style.scss +++ b/packages/block-editor/src/style.scss @@ -49,7 +49,6 @@ @import "./components/url-popover/style.scss"; @import "./hooks/anchor.scss"; @import "./hooks/block-hooks.scss"; -@import "./hooks/background.scss"; @import "./hooks/border.scss"; @import "./hooks/color.scss"; @import "./hooks/dimensions.scss"; diff --git a/packages/block-editor/src/utils/transform-styles/index.js b/packages/block-editor/src/utils/transform-styles/index.js index 808566776f6ea..129a4cd0e3e7b 100644 --- a/packages/block-editor/src/utils/transform-styles/index.js +++ b/packages/block-editor/src/utils/transform-styles/index.js @@ -19,6 +19,7 @@ function transformStyle( return css; } + const postcssFriendlyCSS = css.replace( ':where(body)', 'body' ); try { return postcss( [ @@ -31,7 +32,7 @@ function transformStyle( } ), baseURL && rebaseUrl( { rootUrl: baseURL } ), ].filter( Boolean ) - ).process( css, {} ).css; // use sync PostCSS API + ).process( postcssFriendlyCSS, {} ).css; // use sync PostCSS API } catch ( error ) { if ( error instanceof CssSyntaxError ) { // eslint-disable-next-line no-console diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js index fbbb2d481bee6..0dc0e20caceed 100644 --- a/packages/block-library/src/image/edit.js +++ b/packages/block-library/src/image/edit.js @@ -6,7 +6,7 @@ import classnames from 'classnames'; /** * WordPress dependencies */ -import { getBlobByURL, isBlobURL, revokeBlobURL } from '@wordpress/blob'; +import { isBlobURL } from '@wordpress/blob'; import { store as blocksStore } from '@wordpress/blocks'; import { Placeholder } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; @@ -28,6 +28,7 @@ import { store as noticesStore } from '@wordpress/notices'; * Internal dependencies */ import { unlock } from '../lock-unlock'; +import { useUploadMediaFromBlobURL } from '../utils/hooks'; import Image from './image'; /** @@ -116,7 +117,9 @@ export function ImageEdit( { align, metadata, } = attributes; - const [ temporaryURL, setTemporaryURL ] = useState(); + const [ temporaryURL, setTemporaryURL ] = useState( () => { + return isTemporaryImage( id, url ) ? url : undefined; + } ); const altRef = useRef(); useEffect( () => { @@ -267,44 +270,12 @@ export function ImageEdit( { } } - let isTemp = isTemporaryImage( id, url ); - - // Upload a temporary image on mount. - useEffect( () => { - if ( ! isTemp ) { - return; - } - - const file = getBlobByURL( url ); - - if ( file ) { - const { mediaUpload } = getSettings(); - if ( ! mediaUpload ) { - return; - } - mediaUpload( { - filesList: [ file ], - onFileChange: ( [ img ] ) => { - onSelectImage( img ); - }, - allowedTypes: ALLOWED_MEDIA_TYPES, - onError: ( message ) => { - isTemp = false; - onUploadError( message ); - }, - } ); - } - }, [] ); - - // If an image is temporary, revoke the Blob url when it is uploaded (and is - // no longer temporary). - useEffect( () => { - if ( isTemp ) { - setTemporaryURL( url ); - return; - } - revokeBlobURL( temporaryURL ); - }, [ isTemp, url ] ); + useUploadMediaFromBlobURL( { + url, + allowedTypes: ALLOWED_MEDIA_TYPES, + onChange: onSelectImage, + onError: onUploadError, + } ); const isExternal = isExternalImage( id, url ); const src = isExternal ? url : undefined; diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js index 4d8e2b693f473..049110e40bc5e 100644 --- a/packages/block-library/src/image/image.js +++ b/packages/block-library/src/image/image.js @@ -934,9 +934,7 @@ export default function Image( { return ( <> - { /* Hide controls during upload to avoid component remount, - which causes duplicated image upload. */ } - { ! temporaryURL && controls } + { controls } { img } { metadata: { ...clonedBlocks[ 0 ].attributes.metadata, categories: selectedPattern.categories, + patternName: selectedPattern.name, + name: + clonedBlocks[ 0 ].attributes.metadata.name || + selectedPattern.title, }, }; } diff --git a/packages/block-library/src/utils/hooks.js b/packages/block-library/src/utils/hooks.js index a89031b5f99ce..85196fe67ed7a 100644 --- a/packages/block-library/src/utils/hooks.js +++ b/packages/block-library/src/utils/hooks.js @@ -67,6 +67,7 @@ export function useUploadMediaFromBlobURL( args = {} ) { onChange( media ); }, onError: ( message ) => { + revokeBlobURL( url ); onError( message ); }, } ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index 68368efca525b..d943753afff82 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -6,9 +6,19 @@ - `InputControl`: Ignore IME events when `isPressEnterToChange` is enabled ([#60090](https://github.com/WordPress/gutenberg/pull/60090)). +### Experimental + +- `CustomSelectControlV2`: Rename for consistency ([#60178](https://github.com/WordPress/gutenberg/pull/60178)). + ### Internal - `Popover`, `ColorPicker`: Obviate pointer event trap #59449 ([#59449](https://github.com/WordPress/gutenberg/pull/59449)). +- `Popover`, `ToggleGroupControl`: Use `useReducedMotion()` ([#60168](https://github.com/WordPress/gutenberg/pull/60168)). +- `NavigatorProvider`: Simplify the router state logic ([#60190](https://github.com/WordPress/gutenberg/pull/60190)). + +### Experimental + +- `CustomSelectControlV2`: Fix hint behavior in legacy ([#60183](https://github.com/WordPress/gutenberg/pull/60183)). ## 27.2.0 (2024-03-21) diff --git a/packages/components/src/custom-select-control-v2/README.md b/packages/components/src/custom-select-control-v2/README.md index 634b25808c7d4..e44801681128b 100644 --- a/packages/components/src/custom-select-control-v2/README.md +++ b/packages/components/src/custom-select-control-v2/README.md @@ -1,4 +1,4 @@ -# CustomSelect +# CustomSelectControlV2
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes. @@ -12,31 +12,31 @@ Used to render a customizable select control component. #### Uncontrolled Mode -CustomSelect can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initial selected value. If this prop is not set, the first value from the children will be selected by default. +`CustomSelectControlV2` can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initial selected value. If this prop is not set, the first value from the children will be selected by default. ```jsx -const UncontrolledCustomSelect = () => ( - - +const UncontrolledCustomSelectControlV2 = () => ( + + { /* The `defaultValue` since it wasn't defined */ } Blue - - + + Purple - - + + Pink - - + + ); ``` #### Controlled Mode -CustomSelect can also be used in a controlled mode, where the parent component specifies the `value` and the `onChange` props to control selection. +`CustomSelectControlV2` can also be used in a controlled mode, where the parent component specifies the `value` and the `onChange` props to control selection. ```jsx -const ControlledCustomSelect = () => { +const ControlledCustomSelectControlV2 = () => { const [ value, setValue ] = useState< string | string[] >(); const renderControlledValue = ( renderValue: string | string[] ) => ( @@ -46,7 +46,7 @@ const ControlledCustomSelect = () => { ); return ( - { setValue( nextValue ); @@ -55,11 +55,11 @@ const ControlledCustomSelect = () => { value={ value } > { [ 'blue', 'purple', 'pink' ].map( ( option ) => ( - + { renderControlledValue( option ) } - + ) ) } - + ); }; ``` @@ -70,23 +70,23 @@ Multiple selection can be enabled by using an array for the `value` and `defaultValue` props. The argument of the `onChange` function will also change accordingly. ```jsx -const MultiSelectCustomSelect = () => ( - +const MultiSelectCustomSelectControlV2 = () => ( + { [ 'blue', 'purple', 'pink' ].map( ( item ) => ( - + { item } - + ) ) } - + ); ``` ### Components and Sub-components -CustomSelect is comprised of two individual components: +`CustomSelectControlV2` is comprised of two individual components: -- `CustomSelect`: a wrapper component and context provider. It is responsible for managing the state of the `CustomSelectItem` children. -- `CustomSelectItem`: renders a single select item. The first `CustomSelectItem` child will be used as the `defaultValue` when `defaultValue` is undefined. +- `CustomSelectControlV2`: a wrapper component and context provider. It is responsible for managing the state of the `CustomSelectControlV2.Item` children. +- `CustomSelectControlV2.Item`: renders a single select item. The first `CustomSelectControlV2.Item` child will be used as the `defaultValue` when `defaultValue` is undefined. #### Props @@ -94,7 +94,7 @@ The component accepts the following props: ##### `children`: `React.ReactNode` -The child elements. This should be composed of CustomSelect.Item components. +The child elements. This should be composed of `CustomSelectControlV2.Item` components. - Required: yes @@ -142,7 +142,7 @@ Can be used to externally control the value of the control. - Required: no -### `CustomSelectItem` +### `CustomSelectControlV2.Item` Used to render a select item. diff --git a/packages/components/src/custom-select-control-v2/default-component/index.tsx b/packages/components/src/custom-select-control-v2/default-component/index.tsx index e5650202a4160..e9a2a9d8f5918 100644 --- a/packages/components/src/custom-select-control-v2/default-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/default-component/index.tsx @@ -9,8 +9,9 @@ import * as Ariakit from '@ariakit/react'; import _CustomSelect from '../custom-select'; import type { CustomSelectProps } from '../types'; import type { WordPressComponentProps } from '../../context'; +import Item from '../item'; -function CustomSelect( +function CustomSelectControlV2( props: WordPressComponentProps< CustomSelectProps, 'button', false > ) { const { defaultValue, onChange, value, ...restProps } = props; @@ -24,4 +25,6 @@ function CustomSelect( return <_CustomSelect { ...restProps } store={ store } />; } -export default CustomSelect; +CustomSelectControlV2.Item = Item; + +export default CustomSelectControlV2; diff --git a/packages/components/src/custom-select-control-v2/index.tsx b/packages/components/src/custom-select-control-v2/index.tsx index f05191ad8fc0b..f07e8f6f9f311 100644 --- a/packages/components/src/custom-select-control-v2/index.tsx +++ b/packages/components/src/custom-select-control-v2/index.tsx @@ -1,5 +1,4 @@ /** * Internal dependencies */ -export { default as CustomSelect } from './default-component'; -export { default as CustomSelectItem } from './custom-select-item'; +export { default } from './default-component'; diff --git a/packages/components/src/custom-select-control-v2/custom-select-item.tsx b/packages/components/src/custom-select-control-v2/item.tsx similarity index 93% rename from packages/components/src/custom-select-control-v2/custom-select-item.tsx rename to packages/components/src/custom-select-control-v2/item.tsx index b3e8cfeb1363e..bd60f974cc43a 100644 --- a/packages/components/src/custom-select-control-v2/custom-select-item.tsx +++ b/packages/components/src/custom-select-control-v2/item.tsx @@ -26,4 +26,6 @@ export function CustomSelectItem( { ); } +CustomSelectItem.displayName = 'CustomSelectControlV2.Item'; + export default CustomSelectItem; diff --git a/packages/components/src/custom-select-control-v2/legacy-component/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/index.tsx index 49c640581c377..8071a7e932a8b 100644 --- a/packages/components/src/custom-select-control-v2/legacy-component/index.tsx +++ b/packages/components/src/custom-select-control-v2/legacy-component/index.tsx @@ -11,12 +11,12 @@ import { useMemo } from '@wordpress/element'; * Internal dependencies */ import _CustomSelect from '../custom-select'; +import CustomSelectItem from '../item'; import type { LegacyCustomSelectProps } from '../types'; -import { CustomSelectItem } from '..'; import * as Styled from '../styles'; import { ContextSystemProvider } from '../../context'; -function CustomSelect( props: LegacyCustomSelectProps ) { +function CustomSelectControl( props: LegacyCustomSelectProps ) { const { __experimentalShowSelectedHint, __next40pxDefaultSize = false, @@ -68,9 +68,7 @@ function CustomSelect( props: LegacyCustomSelectProps ) { ); @@ -130,4 +128,4 @@ function CustomSelect( props: LegacyCustomSelectProps ) { ); } -export default CustomSelect; +export default CustomSelectControl; diff --git a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx index 906bf1cde0290..825a9b63464ca 100644 --- a/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/legacy-component/test/index.tsx @@ -12,7 +12,7 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import UncontrolledCustomSelect from '..'; +import UncontrolledCustomSelectControl from '..'; const customClass = 'amber-skies'; @@ -48,14 +48,14 @@ const legacyProps = { ], }; -const LegacyControlledCustomSelect = ( { +const ControlledCustomSelectControl = ( { options, onChange, ...restProps -}: React.ComponentProps< typeof UncontrolledCustomSelect > ) => { +}: React.ComponentProps< typeof UncontrolledCustomSelectControl > ) => { const [ value, setValue ] = useState( options[ 0 ] ); return ( - { @@ -70,8 +70,8 @@ const LegacyControlledCustomSelect = ( { }; describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', LegacyControlledCustomSelect ], + [ 'Uncontrolled', UncontrolledCustomSelectControl ], + [ 'Controlled', ControlledCustomSelectControl ], ] )( 'CustomSelectControl (%s)', ( ...modeAndComponent ) => { const [ , Component ] = modeAndComponent; @@ -248,7 +248,7 @@ describe.each( [ ); } ); - it( 'shows selected hint in list of options when added', async () => { + it( 'shows selected hint in list of options when added, regardless of __experimentalShowSelectedHint prop', async () => { render( ); diff --git a/packages/components/src/custom-select-control-v2/stories/default.story.tsx b/packages/components/src/custom-select-control-v2/stories/default.story.tsx index ee7dba0694c27..ac2f9a51d5951 100644 --- a/packages/components/src/custom-select-control-v2/stories/default.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/default.story.tsx @@ -11,15 +11,14 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import CustomSelect from '../default-component'; -import { CustomSelectItem } from '..'; +import CustomSelectControlV2 from '..'; -const meta: Meta< typeof CustomSelect > = { +const meta: Meta< typeof CustomSelectControlV2 > = { title: 'Components (Experimental)/CustomSelectControl v2/Default', - component: CustomSelect, + component: CustomSelectControlV2, subcomponents: { // @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170 - CustomSelectItem, + 'CustomSelectControlV2.Item': CustomSelectControlV2.Item, }, argTypes: { children: { control: { type: null } }, @@ -48,10 +47,10 @@ const meta: Meta< typeof CustomSelect > = { }; export default meta; -const Template: StoryFn< typeof CustomSelect > = ( props ) => { +const Template: StoryFn< typeof CustomSelectControlV2 > = ( props ) => { const [ value, setValue ] = useState< string | string[] >(); return ( - { setValue( nextValue ); @@ -68,15 +67,15 @@ Default.args = { defaultValue: 'Select a color...', children: ( <> - + Blue - - + + Purple - - + + Pink - + ), }; @@ -100,9 +99,9 @@ MultipleSelection.args = { 'maroon', 'tangerine', ].map( ( item ) => ( - + { item } - + ) ) } ), @@ -134,9 +133,9 @@ CustomSelectedValue.args = { <> { [ 'mystery-person', 'identicon', 'wavatar', 'retro' ].map( ( option ) => ( - + { renderItem( option ) } - + ) ) } diff --git a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx index f97b2376e9deb..120686ea84af6 100644 --- a/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx +++ b/packages/components/src/custom-select-control-v2/stories/legacy.story.tsx @@ -11,11 +11,12 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import CustomSelect from '../legacy-component'; +import CustomSelectControl from '../legacy-component'; +import * as V1Story from '../../custom-select-control/stories/index.story'; -const meta: Meta< typeof CustomSelect > = { +const meta: Meta< typeof CustomSelectControl > = { title: 'Components (Experimental)/CustomSelectControl v2/Legacy', - component: CustomSelect, + component: CustomSelectControl, argTypes: { onChange: { control: { type: null } }, value: { control: { type: null } }, @@ -42,46 +43,30 @@ const meta: Meta< typeof CustomSelect > = { }; export default meta; -const Template: StoryFn< typeof CustomSelect > = ( props ) => { - const [ fontSize, setFontSize ] = useState( props.options[ 0 ] ); +const Template: StoryFn< typeof CustomSelectControl > = ( props ) => { + const [ value, setValue ] = useState( props.options[ 0 ] ); const onChange: React.ComponentProps< - typeof CustomSelect + typeof CustomSelectControl >[ 'onChange' ] = ( changeObject ) => { - setFontSize( changeObject.selectedItem ); + setValue( changeObject.selectedItem ); props.onChange?.( changeObject ); }; return ( - + ); }; export const Default = Template.bind( {} ); -Default.args = { - label: 'Label text', - options: [ - { - key: 'small', - name: 'Small', - style: { fontSize: '50%' }, - __experimentalHint: '50%', - }, - { - key: 'normal', - name: 'Normal', - style: { fontSize: '100%' }, - className: 'can-apply-custom-class-to-option', - }, - { - key: 'large', - name: 'Large', - style: { fontSize: '200%' }, - }, - { - key: 'huge', - name: 'Huge', - style: { fontSize: '300%' }, - }, - ], -}; +Default.args = V1Story.Default.args; + +export const WithLongLabels = Template.bind( {} ); +WithLongLabels.args = V1Story.WithLongLabels.args; + +export const WithHints = Template.bind( {} ); +WithHints.args = V1Story.WithHints.args; diff --git a/packages/components/src/custom-select-control-v2/test/index.tsx b/packages/components/src/custom-select-control-v2/test/index.tsx index 52097e4f8bc5b..13421479c0dd5 100644 --- a/packages/components/src/custom-select-control-v2/test/index.tsx +++ b/packages/components/src/custom-select-control-v2/test/index.tsx @@ -12,7 +12,7 @@ import { useState } from '@wordpress/element'; /** * Internal dependencies */ -import { CustomSelect as UncontrolledCustomSelect, CustomSelectItem } from '..'; +import UncontrolledCustomSelectControlV2 from '..'; import type { CustomSelectProps } from '../types'; const items = [ @@ -41,14 +41,14 @@ const items = [ const defaultProps = { label: 'label!', children: items.map( ( { value, key } ) => ( - + ) ), }; -const ControlledCustomSelect = ( props: CustomSelectProps ) => { +const ControlledCustomSelectControl = ( props: CustomSelectProps ) => { const [ value, setValue ] = useState< string | string[] >(); return ( - { setValue( nextValue ); @@ -60,8 +60,8 @@ const ControlledCustomSelect = ( props: CustomSelectProps ) => { }; describe.each( [ - [ 'Uncontrolled', UncontrolledCustomSelect ], - [ 'Controlled', ControlledCustomSelect ], + [ 'Uncontrolled', UncontrolledCustomSelectControlV2 ], + [ 'Controlled', ControlledCustomSelectControl ], ] )( 'CustomSelectControlV2 (%s)', ( ...modeAndComponent ) => { const [ , Component ] = modeAndComponent; @@ -257,9 +257,12 @@ describe.each( [ 'rose blush', 'ultraviolet morning light', ].map( ( item ) => ( - + { item } - + ) ) } ); @@ -326,9 +329,12 @@ describe.each( [ render( { defaultValues.map( ( item ) => ( - + { item } - + ) ) } ); @@ -378,12 +384,12 @@ describe.each( [ render( - + { renderValue( 'april-29' ) } - - + + { renderValue( 'july-9' ) } - + ); diff --git a/packages/components/src/custom-select-control/stories/index.story.js b/packages/components/src/custom-select-control/stories/index.story.tsx similarity index 84% rename from packages/components/src/custom-select-control/stories/index.story.js rename to packages/components/src/custom-select-control/stories/index.story.tsx index ecfa3b06392ae..a27ad6b3894b6 100644 --- a/packages/components/src/custom-select-control/stories/index.story.js +++ b/packages/components/src/custom-select-control/stories/index.story.tsx @@ -1,3 +1,8 @@ +/** + * External dependencies + */ +import type { StoryFn } from '@storybook/react'; + /** * Internal dependencies */ @@ -18,7 +23,7 @@ export default { }, }; -export const Default = CustomSelectControl.bind( {} ); +export const Default: StoryFn = CustomSelectControl.bind( {} ); Default.args = { label: 'Label', options: [ @@ -46,7 +51,7 @@ Default.args = { ], }; -export const WithLongLabels = CustomSelectControl.bind( {} ); +export const WithLongLabels: StoryFn = CustomSelectControl.bind( {} ); WithLongLabels.args = { ...Default.args, options: [ @@ -65,7 +70,7 @@ WithLongLabels.args = { ], }; -export const WithHints = CustomSelectControl.bind( {} ); +export const WithHints: StoryFn = CustomSelectControl.bind( {} ); WithHints.args = { ...Default.args, options: [ diff --git a/packages/components/src/custom-select-control/test/index.js b/packages/components/src/custom-select-control/test/index.js index 715b2ecc99c7d..a9d5e7d9451ad 100644 --- a/packages/components/src/custom-select-control/test/index.js +++ b/packages/components/src/custom-select-control/test/index.js @@ -238,6 +238,30 @@ describe.each( [ ).toHaveTextContent( 'Hint' ); } ); + it( 'shows selected hint in list of options when added, regardless of __experimentalShowSelectedHint prop', async () => { + const user = userEvent.setup(); + + render( + + ); + + await user.click( + screen.getByRole( 'button', { name: 'Custom select' } ) + ); + + expect( screen.getByRole( 'option', { name: /hint/i } ) ).toBeVisible(); + } ); + describe( 'Keyboard behavior and accessibility', () => { it( 'Captures the keypress event and does not let it propagate', async () => { const user = userEvent.setup(); diff --git a/packages/components/src/navigator/navigator-provider/component.tsx b/packages/components/src/navigator/navigator-provider/component.tsx index cd38bea574813..c776b036d39b6 100644 --- a/packages/components/src/navigator/navigator-provider/component.tsx +++ b/packages/components/src/navigator/navigator-provider/component.tsx @@ -6,14 +6,7 @@ import type { ForwardedRef } from 'react'; /** * WordPress dependencies */ -import { - useMemo, - useState, - useCallback, - useReducer, - useRef, - useEffect, -} from '@wordpress/element'; +import { useMemo, useReducer } from '@wordpress/element'; import isShallowEqual from '@wordpress/is-shallow-equal'; /** @@ -30,26 +23,178 @@ import type { NavigatorProviderProps, NavigatorLocation, NavigatorContext as NavigatorContextType, + NavigateOptions, Screen, + NavigateToParentOptions, } from '../types'; type MatchedPath = ReturnType< typeof patternMatch >; -type ScreenAction = { type: string; screen: Screen }; + +type RouterAction = + | { type: 'add' | 'remove'; screen: Screen } + | { type: 'goback' } + | { type: 'goto'; path: string; options?: NavigateOptions } + | { type: 'gotoparent'; options?: NavigateToParentOptions }; + +type RouterState = { + screens: Screen[]; + locationHistory: NavigatorLocation[]; + matchedPath: MatchedPath; +}; const MAX_HISTORY_LENGTH = 50; -function screensReducer( - state: Screen[] = [], - action: ScreenAction -): Screen[] { +function addScreen( { screens }: RouterState, screen: Screen ) { + return [ ...screens, screen ]; +} + +function removeScreen( { screens }: RouterState, screen: Screen ) { + return screens.filter( ( s ) => s.id !== screen.id ); +} + +function goBack( { locationHistory }: RouterState ) { + if ( locationHistory.length <= 1 ) { + return locationHistory; + } + return [ + ...locationHistory.slice( 0, -2 ), + { + ...locationHistory[ locationHistory.length - 2 ], + isBack: true, + hasRestoredFocus: false, + }, + ]; +} + +function goTo( + state: RouterState, + path: string, + options: NavigateOptions = {} +) { + const { locationHistory } = state; + const { + focusTargetSelector, + isBack = false, + skipFocus = false, + replace = false, + ...restOptions + } = options; + + const isNavigatingToPreviousPath = + isBack && + locationHistory.length > 1 && + locationHistory[ locationHistory.length - 2 ].path === path; + + if ( isNavigatingToPreviousPath ) { + return goBack( state ); + } + + const newLocation = { + ...restOptions, + path, + isBack, + hasRestoredFocus: false, + skipFocus, + }; + + if ( locationHistory.length === 0 ) { + return replace ? [] : [ newLocation ]; + } + + const newLocationHistory = locationHistory.slice( + locationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0, + -1 + ); + + if ( ! replace ) { + newLocationHistory.push( + // Assign `focusTargetSelector` to the previous location in history + // (the one we just navigated from). + { + ...locationHistory[ locationHistory.length - 1 ], + focusTargetSelector, + } + ); + } + + newLocationHistory.push( newLocation ); + + return newLocationHistory; +} + +function goToParent( + state: RouterState, + options: NavigateToParentOptions = {} +) { + const { locationHistory, screens } = state; + const currentPath = locationHistory[ locationHistory.length - 1 ].path; + if ( currentPath === undefined ) { + return locationHistory; + } + const parentPath = findParent( currentPath, screens ); + if ( parentPath === undefined ) { + return locationHistory; + } + return goTo( state, parentPath, { + ...options, + isBack: true, + } ); +} + +function routerReducer( + state: RouterState, + action: RouterAction +): RouterState { + let { screens, locationHistory, matchedPath } = state; + switch ( action.type ) { case 'add': - return [ ...state, action.screen ]; + screens = addScreen( state, action.screen ); + break; case 'remove': - return state.filter( ( s: Screen ) => s.id !== action.screen.id ); + screens = removeScreen( state, action.screen ); + break; + case 'goback': + locationHistory = goBack( state ); + break; + case 'goto': + locationHistory = goTo( state, action.path, action.options ); + break; + case 'gotoparent': + locationHistory = goToParent( state, action.options ); + break; + } + + // Return early in case there is no change + if ( + screens === state.screens && + locationHistory === state.locationHistory + ) { + return state; } - return state; + // Compute the matchedPath + const currentPath = + locationHistory.length > 0 + ? locationHistory[ locationHistory.length - 1 ].path + : undefined; + matchedPath = + currentPath !== undefined + ? patternMatch( currentPath, screens ) + : undefined; + + // If the new match is the same as the previous match, + // return the previous one to keep immutability. + if ( + matchedPath && + state.matchedPath && + matchedPath.id === state.matchedPath.id && + isShallowEqual( matchedPath.params, state.matchedPath.params ) + ) { + matchedPath = state.matchedPath; + } + + return { screens, locationHistory, matchedPath }; } function UnconnectedNavigatorProvider( @@ -59,167 +204,33 @@ function UnconnectedNavigatorProvider( const { initialPath, children, className, ...otherProps } = useContextSystem( props, 'NavigatorProvider' ); - const [ locationHistory, setLocationHistory ] = useState< - NavigatorLocation[] - >( [ - { - path: initialPath, - }, - ] ); - const currentLocationHistory = useRef< NavigatorLocation[] >( [] ); - const [ screens, dispatch ] = useReducer( screensReducer, [] ); - const currentScreens = useRef< Screen[] >( [] ); - useEffect( () => { - currentScreens.current = screens; - }, [ screens ] ); - useEffect( () => { - currentLocationHistory.current = locationHistory; - }, [ locationHistory ] ); - const currentMatch = useRef< MatchedPath >(); - const matchedPath = useMemo( () => { - let currentPath: string | undefined; - if ( - locationHistory.length === 0 || - ( currentPath = - locationHistory[ locationHistory.length - 1 ].path ) === - undefined - ) { - currentMatch.current = undefined; - return undefined; - } - - const resolvePath = ( path: string ) => { - const newMatch = patternMatch( path, screens ); - - // If the new match is the same as the current match, - // return the previous one for performance reasons. - if ( - currentMatch.current && - newMatch && - isShallowEqual( - newMatch.params, - currentMatch.current.params - ) && - newMatch.id === currentMatch.current.id - ) { - return currentMatch.current; - } - - return newMatch; - }; - - const newMatch = resolvePath( currentPath ); - currentMatch.current = newMatch; - return newMatch; - }, [ screens, locationHistory ] ); - - const addScreen = useCallback( - ( screen: Screen ) => dispatch( { type: 'add', screen } ), - [] + const [ routerState, dispatch ] = useReducer( + routerReducer, + initialPath, + ( path ) => ( { + screens: [], + locationHistory: [ { path } ], + matchedPath: undefined, + } ) ); - const removeScreen = useCallback( - ( screen: Screen ) => dispatch( { type: 'remove', screen } ), + // The methods are constant forever, create stable references to them. + const methods = useMemo( + () => ( { + goBack: () => dispatch( { type: 'goback' } ), + goTo: ( path: string, options?: NavigateOptions ) => + dispatch( { type: 'goto', path, options } ), + goToParent: ( options: NavigateToParentOptions | undefined ) => + dispatch( { type: 'gotoparent', options } ), + addScreen: ( screen: Screen ) => + dispatch( { type: 'add', screen } ), + removeScreen: ( screen: Screen ) => + dispatch( { type: 'remove', screen } ), + } ), [] ); - const goBack: NavigatorContextType[ 'goBack' ] = useCallback( () => { - setLocationHistory( ( prevLocationHistory ) => { - if ( prevLocationHistory.length <= 1 ) { - return prevLocationHistory; - } - return [ - ...prevLocationHistory.slice( 0, -2 ), - { - ...prevLocationHistory[ prevLocationHistory.length - 2 ], - isBack: true, - hasRestoredFocus: false, - }, - ]; - } ); - }, [] ); - - const goTo: NavigatorContextType[ 'goTo' ] = useCallback( - ( path, options = {} ) => { - const { - focusTargetSelector, - isBack = false, - skipFocus = false, - replace = false, - ...restOptions - } = options; - - const isNavigatingToPreviousPath = - isBack && - currentLocationHistory.current.length > 1 && - currentLocationHistory.current[ - currentLocationHistory.current.length - 2 - ].path === path; - - if ( isNavigatingToPreviousPath ) { - goBack(); - return; - } - - setLocationHistory( ( prevLocationHistory ) => { - const newLocation = { - ...restOptions, - path, - isBack, - hasRestoredFocus: false, - skipFocus, - }; - - if ( prevLocationHistory.length === 0 ) { - return replace ? [] : [ newLocation ]; - } - - const newLocationHistory = prevLocationHistory.slice( - prevLocationHistory.length > MAX_HISTORY_LENGTH - 1 ? 1 : 0, - -1 - ); - - if ( ! replace ) { - newLocationHistory.push( - // Assign `focusTargetSelector` to the previous location in history - // (the one we just navigated from). - { - ...prevLocationHistory[ - prevLocationHistory.length - 1 - ], - focusTargetSelector, - } - ); - } - - newLocationHistory.push( newLocation ); - - return newLocationHistory; - } ); - }, - [ goBack ] - ); - - const goToParent: NavigatorContextType[ 'goToParent' ] = useCallback( - ( options = {} ) => { - const currentPath = - currentLocationHistory.current[ - currentLocationHistory.current.length - 1 - ].path; - if ( currentPath === undefined ) { - return; - } - const parentPath = findParent( - currentPath, - currentScreens.current - ); - if ( parentPath === undefined ) { - return; - } - goTo( parentPath, { ...options, isBack: true } ); - }, - [ goTo ] - ); + const { locationHistory, matchedPath } = routerState; const navigatorContextValue: NavigatorContextType = useMemo( () => ( { @@ -227,23 +238,11 @@ function UnconnectedNavigatorProvider( ...locationHistory[ locationHistory.length - 1 ], isInitial: locationHistory.length === 1, }, - params: matchedPath ? matchedPath.params : {}, - match: matchedPath ? matchedPath.id : undefined, - goTo, - goBack, - goToParent, - addScreen, - removeScreen, + params: matchedPath?.params ?? {}, + match: matchedPath?.id, + ...methods, } ), - [ - locationHistory, - matchedPath, - goTo, - goBack, - goToParent, - addScreen, - removeScreen, - ] + [ locationHistory, matchedPath, methods ] ); const cx = useCx(); diff --git a/packages/components/src/navigator/navigator-screen/component.tsx b/packages/components/src/navigator/navigator-screen/component.tsx index 29981d46770ee..bd240ef9ac04b 100644 --- a/packages/components/src/navigator/navigator-screen/component.tsx +++ b/packages/components/src/navigator/navigator-screen/component.tsx @@ -106,7 +106,7 @@ function UnconnectedNavigatorScreen( // When navigating back, if a selector is provided, use it to look for the // target element (assumed to be a node inside the current NavigatorScreen) - if ( location.isBack && location?.focusTargetSelector ) { + if ( location.isBack && location.focusTargetSelector ) { elementToFocus = wrapperRef.current.querySelector( location.focusTargetSelector ); diff --git a/packages/components/src/popover/index.tsx b/packages/components/src/popover/index.tsx index 1f41ffcd33108..5fcb5407e4a3f 100644 --- a/packages/components/src/popover/index.tsx +++ b/packages/components/src/popover/index.tsx @@ -16,7 +16,7 @@ import { // eslint-disable-next-line no-restricted-imports import type { HTMLMotionProps, MotionProps } from 'framer-motion'; // eslint-disable-next-line no-restricted-imports -import { motion, useReducedMotion } from 'framer-motion'; +import { motion } from 'framer-motion'; /** * WordPress dependencies @@ -33,6 +33,7 @@ import { createPortal, } from '@wordpress/element'; import { + useReducedMotion, useViewportMatch, useMergeRefs, __experimentalUseDialog as useDialog, diff --git a/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx b/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx index 3149a2d0464c8..421a6078b0495 100644 --- a/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx +++ b/packages/components/src/toggle-group-control/toggle-group-control-option-base/component.tsx @@ -5,12 +5,12 @@ import type { ForwardedRef } from 'react'; // eslint-disable-next-line no-restricted-imports import * as Ariakit from '@ariakit/react'; // eslint-disable-next-line no-restricted-imports -import { motion, useReducedMotion } from 'framer-motion'; +import { motion } from 'framer-motion'; /** * WordPress dependencies */ -import { useInstanceId } from '@wordpress/compose'; +import { useReducedMotion, useInstanceId } from '@wordpress/compose'; import { useMemo } from '@wordpress/element'; /** diff --git a/packages/create-block/CHANGELOG.md b/packages/create-block/CHANGELOG.md index 9bd132fc13672..22d04786d1088 100644 --- a/packages/create-block/CHANGELOG.md +++ b/packages/create-block/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### New Feature + +- Add new `namespacePascalCase` template variable ([#60223](https://github.com/WordPress/gutenberg/pull/60223)). + ## 4.38.0 (2024-03-21) ## 4.37.0 (2024-03-06) @@ -14,7 +18,7 @@ ### Internal -- Remove deprecated `viewModule` field ([#59198](https://github.com/WordPress/gutenberg/pull/59198)). +- Remove deprecated `viewModule` field ([#59198](https://github.com/WordPress/gutenberg/pull/59198)). ## 4.35.0 (2024-02-09) diff --git a/packages/create-block/lib/scaffold.js b/packages/create-block/lib/scaffold.js index da753d13ae394..b7d4addde6a82 100644 --- a/packages/create-block/lib/scaffold.js +++ b/packages/create-block/lib/scaffold.js @@ -98,7 +98,8 @@ module.exports = async ( const view = { ...transformedValues, - namespaceSnakeCase: snakeCase( transformedValues.slug ), + namespaceSnakeCase: snakeCase( transformedValues.namespace ), + namespacePascalCase: pascalCase( transformedValues.namespace ), slugSnakeCase: snakeCase( transformedValues.slug ), slugPascalCase: pascalCase( transformedValues.slug ), ...variantVars, diff --git a/packages/create-block/lib/templates/plugin/$slug.php.mustache b/packages/create-block/lib/templates/plugin/$slug.php.mustache index 90f293f1472f4..45487ef61006f 100644 --- a/packages/create-block/lib/templates/plugin/$slug.php.mustache +++ b/packages/create-block/lib/templates/plugin/$slug.php.mustache @@ -27,7 +27,7 @@ * Update URI: {{{updateURI}}} {{/updateURI}} * - * @package {{namespace}} + * @package {{namespacePascalCase}} */ if ( ! defined( 'ABSPATH' ) ) { diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index c8bd6dd0e3d82..57ee09ba4775c 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -242,7 +242,6 @@ white-space: nowrap; display: block; width: 100%; - overflow: hidden; a { text-decoration: none; @@ -334,7 +333,24 @@ } .dataviews-view-grid__field { - .dataviews-view-grid__field-value { + align-items: flex-start; + + &:not(.is-column) { + align-items: center; + + .dataviews-view-grid__field-name { + width: 35%; + } + + .dataviews-view-grid__field-value { + width: 65%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + } + + .dataviews-view-grid__field-name { color: $gray-700; } } @@ -420,6 +436,7 @@ } .dataviews-view-list__primary-field { min-height: $grid-unit-05 * 5; + overflow: hidden; } } diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 4236fdfbe408b..ec247311c18c4 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -10,8 +10,9 @@ import { __experimentalGrid as Grid, __experimentalHStack as HStack, __experimentalVStack as VStack, - Tooltip, Spinner, + Flex, + FlexItem, } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; import { useAsyncList } from '@wordpress/compose'; @@ -34,6 +35,7 @@ function GridItem( { mediaField, primaryField, visibleFields, + displayAsColumnFields, } ) { const hasBulkAction = useHasAPossibleBulkAction( actions, item ); const id = getItemId( item ); @@ -107,17 +109,34 @@ function GridItem( { return null; } return ( - - -
- { renderedValue } -
-
-
+ + { field.header } + + + { renderedValue } + + ); } ) } @@ -175,6 +194,9 @@ export default function ViewGrid( { mediaField={ mediaField } primaryField={ primaryField } visibleFields={ visibleFields } + displayAsColumnFields={ + view.layout.displayAsColumnFields + } /> ); } ) } diff --git a/packages/edit-post/src/components/keyboard-shortcut-help-modal/index.js b/packages/edit-post/src/components/keyboard-shortcut-help-modal/index.js index 9a7ce46704d47..666a24f2df4fc 100644 --- a/packages/edit-post/src/components/keyboard-shortcut-help-modal/index.js +++ b/packages/edit-post/src/components/keyboard-shortcut-help-modal/index.js @@ -136,6 +136,10 @@ export function KeyboardShortcutHelpModal( { isModalActive, toggleModal } ) { title={ __( 'Text formatting' ) } shortcuts={ textFormattingShortcuts } /> + ); } diff --git a/packages/edit-site/package.json b/packages/edit-site/package.json index 22c6289624d98..c467435407d82 100644 --- a/packages/edit-site/package.json +++ b/packages/edit-site/package.json @@ -76,8 +76,7 @@ "is-plain-object": "^5.0.0", "memize": "^2.1.0", "react-autosize-textarea": "^7.1.0", - "rememo": "^4.0.2", - "remove-accents": "^0.5.0" + "rememo": "^4.0.2" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/edit-site/src/components/actions/index.js b/packages/edit-site/src/components/actions/index.js index 8a2764c05de12..72217fba96a91 100644 --- a/packages/edit-site/src/components/actions/index.js +++ b/packages/edit-site/src/components/actions/index.js @@ -1,12 +1,12 @@ /** * WordPress dependencies */ -import { external, edit, backup } from '@wordpress/icons'; +import { external, trash, edit, backup } from '@wordpress/icons'; import { addQueryArgs } from '@wordpress/url'; import { useDispatch } from '@wordpress/data'; import { decodeEntities } from '@wordpress/html-entities'; import { store as coreStore } from '@wordpress/core-data'; -import { __, sprintf } from '@wordpress/i18n'; +import { __, _n, sprintf } from '@wordpress/i18n'; import { store as noticesStore } from '@wordpress/notices'; import { useMemo } from '@wordpress/element'; import { privateApis as routerPrivateApis } from '@wordpress/router'; @@ -27,6 +27,8 @@ const { useHistory } = unlock( routerPrivateApis ); export const trashPostAction = { id: 'move-to-trash', label: __( 'Move to Trash' ), + isPrimary: true, + icon: trash, isEligible( { status } ) { return status !== 'trash'; }, @@ -47,8 +49,10 @@ export const trashPostAction = { ) : sprintf( // translators: %d: The number of pages (2 or more). - __( - 'Are you sure you want to delete %d pages?' + _n( + 'Are you sure you want to delete %d page?', + 'Are you sure you want to delete %d pages?', + posts.length ), posts.length ) } diff --git a/packages/edit-site/src/components/global-styles/background-panel.js b/packages/edit-site/src/components/global-styles/background-panel.js new file mode 100644 index 0000000000000..e4760a810ecbc --- /dev/null +++ b/packages/edit-site/src/components/global-styles/background-panel.js @@ -0,0 +1,34 @@ +/** + * WordPress dependencies + */ +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { + useGlobalStyle, + useGlobalSetting, + BackgroundPanel: StylesBackgroundPanel, +} = unlock( blockEditorPrivateApis ); + +export default function BackgroundPanel() { + const [ style ] = useGlobalStyle( '', undefined, 'user', { + shouldDecodeEncode: false, + } ); + const [ inheritedStyle, setStyle ] = useGlobalStyle( '', undefined, 'all', { + shouldDecodeEncode: false, + } ); + const [ settings ] = useGlobalSetting( '' ); + + return ( + + ); +} 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 dc593c7dfbb29..ff8cfc1284b1e 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 @@ -281,11 +281,13 @@ function FontLibraryProvider( { children } ) { sucessfullyInstalledFontFaces?.length > 0 || alreadyInstalledFontFaces?.length > 0 ) { - fontFamilyToInstall.fontFace = [ + // Use font data from REST API not from client to ensure + // correct font information is used. + installedFontFamily.fontFace = [ ...sucessfullyInstalledFontFaces, - ...alreadyInstalledFontFaces, ]; - fontFamiliesToActivate.push( fontFamilyToInstall ); + + fontFamiliesToActivate.push( installedFontFamily ); } // If it's a system font but was installed successfully, activate it. @@ -402,14 +404,29 @@ function FontLibraryProvider( { children } ) { }; const activateCustomFontFamilies = ( fontsToAdd ) => { - // Merge the existing custom fonts with the new fonts. + // Removes the id from the families and faces to avoid saving that to global styles post content. + const fontsToActivate = fontsToAdd.map( + ( { id: _familyDbId, fontFace, ...font } ) => ( { + ...font, + ...( fontFace && fontFace.length > 0 + ? { + fontFace: fontFace.map( + ( { id: _faceDbId, ...face } ) => face + ), + } + : {} ), + } ) + ); + // Activate the fonts by set the new custom fonts array. setFontFamilies( { ...fontFamilies, - custom: mergeFontFamilies( fontFamilies?.custom, fontsToAdd ), + // Merge the existing custom fonts with the new fonts. + custom: mergeFontFamilies( fontFamilies?.custom, fontsToActivate ), } ); + // Add custom fonts to the browser. - fontsToAdd.forEach( ( font ) => { + fontsToActivate.forEach( ( font ) => { if ( font.fontFace ) { font.fontFace.forEach( ( face ) => { // Load font faces just in the iframe because they already are in the document. diff --git a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js index b3deb8fcec499..bd97b5b7fdcdd 100644 --- a/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js +++ b/packages/edit-site/src/components/global-styles/font-library-modal/utils/index.js @@ -234,10 +234,23 @@ export function makeFontFacesFormData( font ) { } export async function batchInstallFontFaces( fontFamilyId, fontFacesData ) { - const promises = fontFacesData.map( ( faceData ) => - fetchInstallFontFace( fontFamilyId, faceData ) - ); - const responses = await Promise.allSettled( promises ); + const responses = []; + + /* + * Uses the same response format as Promise.allSettled, but executes requests in sequence to work + * around a race condition that can cause an error when the fonts directory doesn't exist yet. + */ + for ( const faceData of fontFacesData ) { + try { + const response = await fetchInstallFontFace( + fontFamilyId, + faceData + ); + responses.push( { status: 'fulfilled', value: response } ); + } catch ( error ) { + responses.push( { status: 'rejected', reason: error } ); + } + } const results = { errors: [], diff --git a/packages/edit-site/src/components/global-styles/root-menu.js b/packages/edit-site/src/components/global-styles/root-menu.js index 9edfd064acbf7..97598635f7b85 100644 --- a/packages/edit-site/src/components/global-styles/root-menu.js +++ b/packages/edit-site/src/components/global-styles/root-menu.js @@ -2,7 +2,7 @@ * WordPress dependencies */ import { __experimentalItemGroup as ItemGroup } from '@wordpress/components'; -import { typography, color, layout } from '@wordpress/icons'; +import { typography, color, layout, image } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; @@ -18,6 +18,7 @@ const { useHasColorPanel, useGlobalSetting, useSettingsForBlockElement, + useHasBackgroundPanel, } = unlock( blockEditorPrivateApis ); function RootMenu() { @@ -27,6 +28,7 @@ function RootMenu() { const hasColorPanel = useHasColorPanel( settings ); const hasDimensionsPanel = useHasDimensionsPanel( settings ); const hasLayoutPanel = hasDimensionsPanel; + const hasBackgroundPanel = useHasBackgroundPanel( settings ); return ( <> @@ -58,6 +60,15 @@ function RootMenu() { { __( 'Layout' ) } ) } + { hasBackgroundPanel && ( + + { __( 'Background' ) } + + ) } ); diff --git a/packages/edit-site/src/components/global-styles/screen-background.js b/packages/edit-site/src/components/global-styles/screen-background.js new file mode 100644 index 0000000000000..5e8a7832a42b4 --- /dev/null +++ b/packages/edit-site/src/components/global-styles/screen-background.js @@ -0,0 +1,29 @@ +/** + * WordPress dependencies + */ +import { __ } from '@wordpress/i18n'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import BackgroundPanel from './background-panel'; +import ScreenHeader from './header'; +import { unlock } from '../../lock-unlock'; + +const { useHasBackgroundPanel, useGlobalSetting } = unlock( + blockEditorPrivateApis +); + +function ScreenBackground() { + const [ settings ] = useGlobalSetting( '' ); + const hasBackgroundPanel = useHasBackgroundPanel( settings ); + return ( + <> + + { hasBackgroundPanel && } + + ); +} + +export default ScreenBackground; diff --git a/packages/edit-site/src/components/global-styles/screen-style-variations.js b/packages/edit-site/src/components/global-styles/screen-style-variations.js index e206fb1e443a0..f9707b47f7670 100644 --- a/packages/edit-site/src/components/global-styles/screen-style-variations.js +++ b/packages/edit-site/src/components/global-styles/screen-style-variations.js @@ -19,7 +19,6 @@ function ScreenStyleVariations() { return ( <> + + + + { blocks.map( ( block ) => ( + ); } diff --git a/packages/edit-site/src/components/page-patterns/search-items.js b/packages/edit-site/src/components/page-patterns/search-items.js index 183cf3bad140d..b5231964a78d6 100644 --- a/packages/edit-site/src/components/page-patterns/search-items.js +++ b/packages/edit-site/src/components/page-patterns/search-items.js @@ -1,8 +1,16 @@ /** - * External dependencies + * WordPress dependencies */ -import removeAccents from 'remove-accents'; -import { noCase } from 'change-case'; +import { privateApis as blockEditorPrivateApis } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../../lock-unlock'; + +const { extractWords, getNormalizedSearchTerms, normalizeString } = unlock( + blockEditorPrivateApis +); /** * Internal dependencies @@ -20,59 +28,6 @@ const defaultGetDescription = ( item ) => item.description || ''; const defaultGetKeywords = ( item ) => item.keywords || []; const defaultHasCategory = () => false; -/** - * Extracts words from an input string. - * - * @param {string} input The input string. - * - * @return {Array} Words, extracted from the input string. - */ -function extractWords( input = '' ) { - return noCase( input, { - splitRegexp: [ - /([\p{Ll}\p{Lo}\p{N}])([\p{Lu}\p{Lt}])/gu, // One lowercase or digit, followed by one uppercase. - /([\p{Lu}\p{Lt}])([\p{Lu}\p{Lt}][\p{Ll}\p{Lo}])/gu, // One uppercase followed by one uppercase and one lowercase. - ], - stripRegexp: /(\p{C}|\p{P}|\p{S})+/giu, // Anything that's not a punctuation, symbol or control/format character. - } ) - .split( ' ' ) - .filter( Boolean ); -} - -/** - * Sanitizes the search input string. - * - * @param {string} input The search input to normalize. - * - * @return {string} The normalized search input. - */ -function normalizeSearchInput( input = '' ) { - // Disregard diacritics. - // Input: "média" - input = removeAccents( input ); - - // Accommodate leading slash, matching autocomplete expectations. - // Input: "/media" - input = input.replace( /^\//, '' ); - - // Lowercase. - // Input: "MEDIA" - input = input.toLowerCase(); - - return input; -} - -/** - * Converts the search term into a list of normalized terms. - * - * @param {string} input The search term to normalize. - * - * @return {string[]} The normalized list of search terms. - */ -export const getNormalizedSearchTerms = ( input = '' ) => { - return extractWords( normalizeSearchInput( input ) ); -}; - const removeMatchingTerms = ( unmatchedTerms, unprocessedTerms ) => { return unmatchedTerms.filter( ( term ) => @@ -162,8 +117,8 @@ function getItemSearchRank( item, searchTerm, config ) { const description = getDescription( item ); const keywords = getKeywords( item ); - const normalizedSearchInput = normalizeSearchInput( searchTerm ); - const normalizedTitle = normalizeSearchInput( title ); + const normalizedSearchInput = normalizeString( searchTerm ); + const normalizedTitle = normalizeString( title ); // Prefers exact matches // Then prefers if the beginning of the title matches the search term diff --git a/packages/edit-site/src/components/page-templates-template-parts/index.js b/packages/edit-site/src/components/page-templates-template-parts/index.js index c0bfb8a26fcb0..6e808aa6672ba 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/index.js +++ b/packages/edit-site/src/components/page-templates-template-parts/index.js @@ -64,6 +64,7 @@ const defaultConfigPerViewType = { [ LAYOUT_GRID ]: { mediaField: 'preview', primaryField: 'title', + displayAsColumnFields: [ 'description' ], }, [ LAYOUT_LIST ]: { primaryField: 'title', @@ -138,7 +139,7 @@ function AuthorField( { item, viewType } ) {
) } - { text } + { text } ); } diff --git a/packages/edit-site/src/components/page-templates-template-parts/style.scss b/packages/edit-site/src/components/page-templates-template-parts/style.scss index c7485bce79c57..79c999e50acdf 100644 --- a/packages/edit-site/src/components/page-templates-template-parts/style.scss +++ b/packages/edit-site/src/components/page-templates-template-parts/style.scss @@ -103,6 +103,11 @@ } } +.page-templates-author-field__name { + text-overflow: ellipsis; + overflow: hidden; +} + .edit-site-list__rename-modal { // The rename dropdown popover is open at the same time as the rename modal. The latter has to be higher. z-index: z-index(".edit-site-list__rename-modal"); diff --git a/packages/edit-site/src/components/plugin-template-setting-panel/index.js b/packages/edit-site/src/components/plugin-template-setting-panel/index.js index 0c279c91039a7..3ba04b091ba4f 100644 --- a/packages/edit-site/src/components/plugin-template-setting-panel/index.js +++ b/packages/edit-site/src/components/plugin-template-setting-panel/index.js @@ -5,11 +5,24 @@ /** * WordPress dependencies */ +import { store as editorStore } from '@wordpress/editor'; +import { useSelect } from '@wordpress/data'; import { createSlotFill } from '@wordpress/components'; const { Fill, Slot } = createSlotFill( 'PluginTemplateSettingPanel' ); -const PluginTemplateSettingPanel = Fill; +const PluginTemplateSettingPanel = ( { children } ) => { + const isCurrentEntityTemplate = useSelect( + ( select ) => + select( editorStore ).getCurrentPostType() === 'wp_template', + [] + ); + if ( ! isCurrentEntityTemplate ) { + return null; + } + return { children }; +}; + PluginTemplateSettingPanel.Slot = Slot; /** diff --git a/packages/edit-site/src/components/sidebar-edit-mode/index.js b/packages/edit-site/src/components/sidebar-edit-mode/index.js index bd9c43cc456f7..f3cb7cc9dae0e 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/index.js @@ -22,7 +22,6 @@ import { STORE_NAME } from '../../store/constants'; import SettingsHeader from './settings-header'; import PagePanels from './page-panels'; import TemplatePanel from './template-panel'; -import PluginTemplateSettingPanel from '../plugin-template-setting-panel'; import { SIDEBAR_BLOCK, SIDEBAR_TEMPLATE } from './constants'; import { store as editSiteStore } from '../../store'; import { unlock } from '../../lock-unlock'; @@ -96,7 +95,6 @@ const FillContents = ( { focusable={ false } > { isEditingPage ? : } - diff --git a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js index 403582a27a8d7..c19cf45e4551b 100644 --- a/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js +++ b/packages/edit-site/src/components/sidebar-edit-mode/template-panel/index.js @@ -25,6 +25,7 @@ import { privateApis as routerPrivateApis } from '@wordpress/router'; import { store as editSiteStore } from '../../../store'; import TemplateActions from '../../template-actions'; import TemplateAreas from './template-areas'; +import PluginTemplateSettingPanel from '../../plugin-template-setting-panel'; import { useAvailablePatterns } from './hooks'; import { TEMPLATE_PART_POST_TYPE } from '../../../utils/constants'; import { unlock } from '../../../lock-unlock'; @@ -115,6 +116,7 @@ export default function TemplatePanel() { > + { availablePatterns?.length > 0 && ( + ); } diff --git a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js index 2000ae5727190..48542e58a8732 100644 --- a/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js +++ b/packages/editor/src/components/editor-canvas/edit-template-blocks-notification.js @@ -1,9 +1,8 @@ /** * WordPress dependencies */ -import { useSelect, useDispatch } from '@wordpress/data'; -import { useEffect, useState, useRef } from '@wordpress/element'; -import { store as noticesStore } from '@wordpress/notices'; +import { useSelect } from '@wordpress/data'; +import { useEffect, useState } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { __experimentalConfirmDialog as ConfirmDialog } from '@wordpress/components'; @@ -38,73 +37,22 @@ export default function EditTemplateBlocksNotification( { contentRef } ) { }; }, [] ); - const { getNotices } = useSelect( noticesStore ); - - const { createInfoNotice, removeNotice } = useDispatch( noticesStore ); - const [ isDialogOpen, setIsDialogOpen ] = useState( false ); - const lastNoticeId = useRef( 0 ); - useEffect( () => { - const handleClick = async ( event ) => { - if ( ! event.target.classList.contains( 'is-root-container' ) ) { - return; - } - - const isNoticeAlreadyShowing = getNotices().some( - ( notice ) => notice.id === lastNoticeId.current - ); - if ( isNoticeAlreadyShowing ) { - return; - } - - const { notice } = await createInfoNotice( - __( 'Edit your template to edit this block.' ), - { - isDismissible: true, - type: 'snackbar', - actions: [ - { - label: __( 'Edit template' ), - onClick: () => - onNavigateToEntityRecord( { - postId: templateId, - postType: 'wp_template', - } ), - }, - ], - } - ); - lastNoticeId.current = notice.id; - }; - const handleDblClick = ( event ) => { if ( ! event.target.classList.contains( 'is-root-container' ) ) { return; } - if ( lastNoticeId.current ) { - removeNotice( lastNoticeId.current ); - } setIsDialogOpen( true ); }; const canvas = contentRef.current; - canvas?.addEventListener( 'click', handleClick ); canvas?.addEventListener( 'dblclick', handleDblClick ); return () => { - canvas?.removeEventListener( 'click', handleClick ); canvas?.removeEventListener( 'dblclick', handleDblClick ); }; - }, [ - lastNoticeId, - contentRef, - getNotices, - createInfoNotice, - onNavigateToEntityRecord, - templateId, - removeNotice, - ] ); + }, [ contentRef ] ); return ( setIsDialogOpen( false ) } > - { __( 'Edit your template to edit this block.' ) } + { __( + 'You’ve tried to select a block that is part of a template, which may be used on other posts and pages.' + ) } ); } diff --git a/packages/editor/src/components/editor-canvas/index.js b/packages/editor/src/components/editor-canvas/index.js index bc7d54583afbd..363d52b124aa0 100644 --- a/packages/editor/src/components/editor-canvas/index.js +++ b/packages/editor/src/components/editor-canvas/index.js @@ -29,6 +29,7 @@ import PostTitle from '../post-title'; import { store as editorStore } from '../../store'; import { unlock } from '../../lock-unlock'; import EditTemplateBlocksNotification from './edit-template-blocks-notification'; +import useSelectNearestEditableBlock from '../../hooks/use-select-nearest-editable-block'; const { LayoutStyle, @@ -313,6 +314,9 @@ function EditorCanvas( { useFlashEditableBlocks( { isEnabled: renderingMode === 'template-locked', } ), + useSelectNearestEditableBlock( { + isEnabled: renderingMode === 'template-locked', + } ), ] ); return ( diff --git a/packages/editor/src/components/post-card-panel/index.js b/packages/editor/src/components/post-card-panel/index.js index b0dab09286f3b..f426239d3477f 100644 --- a/packages/editor/src/components/post-card-panel/index.js +++ b/packages/editor/src/components/post-card-panel/index.js @@ -36,13 +36,13 @@ export default function PostCardPanel( { className, actions, children } ) { const { getEditedEntityRecord } = select( coreStore ); const _type = getCurrentPostType(); const _id = getCurrentPostId(); - let _templateInfo; const _record = getEditedEntityRecord( 'postType', _type, _id ); + const _templateInfo = __experimentalGetTemplateInfo( _record ); return { title: _templateInfo?.title || getEditedPostAttribute( 'title' ), modified: getEditedPostAttribute( 'modified' ), id: _id, - templateInfo: __experimentalGetTemplateInfo( _record ), + templateInfo: _templateInfo, icon: unlock( select( editorStore ) ).getPostIcon( _type, { area: _record?.area, } ), @@ -88,8 +88,8 @@ export default function PostCardPanel( { className, actions, children } ) { className="editor-post-card-panel__description" spacing={ 2 } > - { !! description && { description } } - { !! lastEditedText && ( + { description && { description } } + { lastEditedText && ( { lastEditedText } ) } diff --git a/packages/editor/src/components/provider/disable-non-page-content-blocks.js b/packages/editor/src/components/provider/disable-non-page-content-blocks.js index bfcee3365d412..01b45964348af 100644 --- a/packages/editor/src/components/provider/disable-non-page-content-blocks.js +++ b/packages/editor/src/components/provider/disable-non-page-content-blocks.js @@ -4,13 +4,14 @@ import { useSelect, useDispatch } from '@wordpress/data'; import { store as blockEditorStore } from '@wordpress/block-editor'; import { useEffect } from '@wordpress/element'; +import { applyFilters } from '@wordpress/hooks'; -const CONTENT_ONLY_BLOCKS = [ - 'core/post-content', - 'core/post-featured-image', +const CONTENT_ONLY_BLOCKS = applyFilters( 'editor.postContentBlockTypes', [ 'core/post-title', + 'core/post-featured-image', + 'core/post-content', 'core/template-part', -]; +] ); /** * Component that when rendered, makes it so that the site editor allows only diff --git a/packages/editor/src/components/provider/use-block-editor-settings.js b/packages/editor/src/components/provider/use-block-editor-settings.js index c7540645328f7..2f8bf920a4829 100644 --- a/packages/editor/src/components/provider/use-block-editor-settings.js +++ b/packages/editor/src/components/provider/use-block-editor-settings.js @@ -1,7 +1,7 @@ /** * WordPress dependencies */ -import { Platform, useMemo, useCallback } from '@wordpress/element'; +import { useMemo, useCallback } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; import { store as coreStore, @@ -24,6 +24,14 @@ import { unlock } from '../../lock-unlock'; const EMPTY_BLOCKS_LIST = []; +function __experimentalReusableBlocksSelect( select ) { + return ( + select( coreStore ).getEntityRecords( 'postType', 'wp_block', { + per_page: -1, + } ) ?? EMPTY_BLOCKS_LIST + ); +} + const BLOCK_EDITOR_SETTINGS = [ '__experimentalBlockDirectory', '__experimentalDiscussionSettings', @@ -92,7 +100,6 @@ function useBlockEditorSettings( settings, postType, postId ) { hasFixedToolbar, isDistractionFree, keepCaretInsideBlock, - reusableBlocks, hasUploadPermissions, hiddenBlockTypes, canUseUnfilteredHTML, @@ -103,13 +110,11 @@ function useBlockEditorSettings( settings, postType, postId ) { restBlockPatternCategories, } = useSelect( ( select ) => { - const isWeb = Platform.OS === 'web'; const { canUser, getRawEntityRecord, getEntityRecord, getUserPatternCategories, - getEntityRecords, getBlockPatternCategories, } = select( coreStore ); const { get } = select( preferencesStore ); @@ -135,11 +140,6 @@ function useBlockEditorSettings( settings, postType, postId ) { hiddenBlockTypes: get( 'core', 'hiddenBlockTypes' ), isDistractionFree: get( 'core', 'distractionFree' ), keepCaretInsideBlock: get( 'core', 'keepCaretInsideBlock' ), - reusableBlocks: isWeb - ? getEntityRecords( 'postType', 'wp_block', { - per_page: -1, - } ) - : EMPTY_BLOCKS_LIST, // Reusable blocks are fetched in the native version of this hook. hasUploadPermissions: canUser( 'create', 'media' ) ?? true, userCanCreatePages: canUser( 'create', 'pages' ), pageOnFront: siteSettings?.page_on_front, @@ -249,7 +249,8 @@ function useBlockEditorSettings( settings, postType, postId ) { unlock( select( coreStore ) ).getBlockPatternsForPostType( postType ), - __experimentalReusableBlocks: reusableBlocks, + [ unlock( privateApis ).reusableBlocksSelectKey ]: + __experimentalReusableBlocksSelect, __experimentalBlockPatternCategories: blockPatternCategories, __experimentalUserPatternCategories: userPatternCategories, __experimentalFetchLinkSuggestions: ( search, searchOptions ) => @@ -288,7 +289,6 @@ function useBlockEditorSettings( settings, postType, postId ) { keepCaretInsideBlock, settings, hasUploadPermissions, - reusableBlocks, userPatternCategories, blockPatterns, blockPatternCategories, diff --git a/packages/editor/src/hooks/use-select-nearest-editable-block.js b/packages/editor/src/hooks/use-select-nearest-editable-block.js new file mode 100644 index 0000000000000..f6e621a25bf43 --- /dev/null +++ b/packages/editor/src/hooks/use-select-nearest-editable-block.js @@ -0,0 +1,95 @@ +/** + * WordPress dependencies + */ +import { useRefEffect } from '@wordpress/compose'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; + +/** + * Internal dependencies + */ +import { unlock } from '../lock-unlock'; + +const DISTANCE_THRESHOLD = 500; + +function clamp( value, min, max ) { + return Math.min( Math.max( value, min ), max ); +} + +function distanceFromRect( x, y, rect ) { + const dx = x - clamp( x, rect.left, rect.right ); + const dy = y - clamp( y, rect.top, rect.bottom ); + return Math.sqrt( dx * dx + dy * dy ); +} + +export default function useSelectNearestEditableBlock( { + isEnabled = true, +} = {} ) { + const { getEnabledClientIdsTree, getBlockName, getBlockOrder } = unlock( + useSelect( blockEditorStore ) + ); + const { selectBlock } = useDispatch( blockEditorStore ); + + return useRefEffect( + ( element ) => { + if ( ! isEnabled ) { + return; + } + + const selectNearestEditableBlock = ( x, y ) => { + const editableBlockClientIds = + getEnabledClientIdsTree().flatMap( ( { clientId } ) => { + const blockName = getBlockName( clientId ); + if ( blockName === 'core/template-part' ) { + return []; + } + if ( blockName === 'core/post-content' ) { + const innerBlocks = getBlockOrder( clientId ); + if ( innerBlocks.length ) { + return innerBlocks; + } + } + return [ clientId ]; + } ); + + let nearestDistance = Infinity, + nearestClientId = null; + + for ( const clientId of editableBlockClientIds ) { + const block = element.querySelector( + `[data-block="${ clientId }"]` + ); + if ( ! block ) { + continue; + } + const rect = block.getBoundingClientRect(); + const distance = distanceFromRect( x, y, rect ); + if ( + distance < nearestDistance && + distance < DISTANCE_THRESHOLD + ) { + nearestDistance = distance; + nearestClientId = clientId; + } + } + + if ( nearestClientId ) { + selectBlock( nearestClientId ); + } + }; + + const handleClick = ( event ) => { + const shouldSelect = + event.target === element || + event.target.classList.contains( 'is-root-container' ); + if ( shouldSelect ) { + selectNearestEditableBlock( event.clientX, event.clientY ); + } + }; + + element.addEventListener( 'click', handleClick ); + return () => element.removeEventListener( 'click', handleClick ); + }, + [ isEnabled ] + ); +} diff --git a/packages/editor/src/store/selectors.js b/packages/editor/src/store/selectors.js index 5b7dae0977104..2fd8b6894eb83 100644 --- a/packages/editor/src/store/selectors.js +++ b/packages/editor/src/store/selectors.js @@ -1701,8 +1701,8 @@ export function __experimentalGetDefaultTemplateTypes( state ) { export const __experimentalGetDefaultTemplatePartAreas = createSelector( ( state ) => { const areas = - getEditorSettings( state )?.defaultTemplatePartAreas || []; - return areas?.map( ( item ) => { + getEditorSettings( state )?.defaultTemplatePartAreas ?? []; + return areas.map( ( item ) => { return { ...item, icon: getTemplatePartIcon( item.icon ) }; } ); }, @@ -1730,7 +1730,7 @@ export const __experimentalGetDefaultTemplateType = createSelector( ) ?? EMPTY_OBJECT ); }, - ( state, slug ) => [ __experimentalGetDefaultTemplateTypes( state ), slug ] + ( state ) => [ __experimentalGetDefaultTemplateTypes( state ) ] ); /** @@ -1741,32 +1741,39 @@ export const __experimentalGetDefaultTemplateType = createSelector( * @param {Object} template The template for which we need information. * @return {Object} Information about the template, including title, description, and icon. */ -export function __experimentalGetTemplateInfo( state, template ) { - if ( ! template ) { - return EMPTY_OBJECT; - } +export const __experimentalGetTemplateInfo = createSelector( + ( state, template ) => { + if ( ! template ) { + return EMPTY_OBJECT; + } - const { description, slug, title, area } = template; - const { title: defaultTitle, description: defaultDescription } = - __experimentalGetDefaultTemplateType( state, slug ); + const { description, slug, title, area } = template; + const { title: defaultTitle, description: defaultDescription } = + __experimentalGetDefaultTemplateType( state, slug ); - const templateTitle = typeof title === 'string' ? title : title?.rendered; - const templateDescription = - typeof description === 'string' ? description : description?.raw; - const templateIcon = - __experimentalGetDefaultTemplatePartAreas( state ).find( - ( item ) => area === item.area - )?.icon || layout; + const templateTitle = + typeof title === 'string' ? title : title?.rendered; + const templateDescription = + typeof description === 'string' ? description : description?.raw; + const templateIcon = + __experimentalGetDefaultTemplatePartAreas( state ).find( + ( item ) => area === item.area + )?.icon || layout; - return { - title: - templateTitle && templateTitle !== slug - ? templateTitle - : defaultTitle || slug, - description: templateDescription || defaultDescription, - icon: templateIcon, - }; -} + return { + title: + templateTitle && templateTitle !== slug + ? templateTitle + : defaultTitle || slug, + description: templateDescription || defaultDescription, + icon: templateIcon, + }; + }, + ( state ) => [ + __experimentalGetDefaultTemplateTypes( state ), + __experimentalGetDefaultTemplatePartAreas( state ), + ] +); /** * Returns a post type label depending on the current post. diff --git a/packages/react-native-aztec/package.json b/packages/react-native-aztec/package.json index 05d6f82277466..823529e9b268c 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.115.0", + "version": "1.116.0", "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 af59319907ea6..3653faec2df5c 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.115.0", + "version": "1.116.0", "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 8ad6a30d40f8f..5ad91131d6d9c 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -10,6 +10,8 @@ For each user feature we should also add a importance categorization label to i --> ## Unreleased + +## 1.116.0 - [**] Highlight color formatting style improvements [#57650] ## 1.115.0 diff --git a/packages/react-native-editor/ios/Podfile.lock b/packages/react-native-editor/ios/Podfile.lock index f3a7bff4e105b..4c28db5ad8986 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.73.3) - fmt (6.2.1) - glog (0.3.5) - - Gutenberg (1.115.0): + - Gutenberg (1.116.0): - React-Core (= 0.73.3) - React-CoreModules (= 0.73.3) - React-RCTImage (= 0.73.3) @@ -1109,7 +1109,7 @@ PODS: - React-Core - RNSVG (14.0.0): - React-Core - - RNTAztecView (1.115.0): + - RNTAztecView (1.116.0): - React-Core - WordPress-Aztec-iOS (= 1.19.11) - SDWebImage (5.11.1): @@ -1343,7 +1343,7 @@ SPEC CHECKSUMS: FBReactNativeSpec: 73b3972e2bd20b3235ff2014f06a3d3af675ed29 fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9 glog: c5d68082e772fa1c511173d6b30a9de2c05a69a2 - Gutenberg: 063e73c87f712ec62233eb93c12f1ab54adcc880 + Gutenberg: 76722ef03a74e5af38f54510c3d08bd472d0ca4a hermes-engine: 5420539d016f368cd27e008f65f777abd6098c56 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 libwebp: 60305b2e989864154bd9be3d772730f08fc6a59c @@ -1402,7 +1402,7 @@ SPEC CHECKSUMS: RNReanimated: 6936b41d8afb97175e7c0ab40425b53103f71046 RNScreens: 2b73f5eb2ac5d94fbd61fa4be0bfebd345716825 RNSVG: 255767813dac22db1ec2062c8b7e7b856d4e5ae6 - RNTAztecView: a7f3ef74bdd75250ae479b8027021576047aed0f + RNTAztecView: 15c61330fdc47174997d858975d92e62891e5ba3 SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 diff --git a/packages/react-native-editor/package.json b/packages/react-native-editor/package.json index f4ea0c80ed56e..10259886e6674 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.115.0", + "version": "1.116.0", "description": "Mobile WordPress gutenberg editor.", "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", diff --git a/packages/router/src/history.js b/packages/router/src/history.js index 8827f51b2fd80..4d01dfa6492ad 100644 --- a/packages/router/src/history.js +++ b/packages/router/src/history.js @@ -6,31 +6,26 @@ import { createBrowserHistory } from 'history'; /** * WordPress dependencies */ -import { addQueryArgs, getQueryArgs, removeQueryArgs } from '@wordpress/url'; +import { buildQueryString } from '@wordpress/url'; const history = createBrowserHistory(); const originalHistoryPush = history.push; const originalHistoryReplace = history.replace; +function buildSearch( params ) { + const queryString = buildQueryString( params ); + return queryString.length > 0 ? '?' + queryString : queryString; +} + function push( params, state ) { - const currentArgs = getQueryArgs( window.location.href ); - const currentUrlWithoutArgs = removeQueryArgs( - window.location.href, - ...Object.keys( currentArgs ) - ); - const newUrl = addQueryArgs( currentUrlWithoutArgs, params ); - return originalHistoryPush.call( history, newUrl, state ); + const search = buildSearch( params ); + return originalHistoryPush.call( history, { search }, state ); } function replace( params, state ) { - const currentArgs = getQueryArgs( window.location.href ); - const currentUrlWithoutArgs = removeQueryArgs( - window.location.href, - ...Object.keys( currentArgs ) - ); - const newUrl = addQueryArgs( currentUrlWithoutArgs, params ); - return originalHistoryReplace.call( history, newUrl, state ); + const search = buildSearch( params ); + return originalHistoryReplace.call( history, { search }, state ); } history.push = push; diff --git a/packages/url/src/get-filename.js b/packages/url/src/get-filename.js index 2941f18fe07b4..d744a8da6bccf 100644 --- a/packages/url/src/get-filename.js +++ b/packages/url/src/get-filename.js @@ -13,6 +13,11 @@ */ export function getFilename( url ) { let filename; + + if ( ! url ) { + return; + } + try { filename = new URL( url, 'http://example.com' ).pathname .split( '/' ) diff --git a/packages/url/src/test/index.js b/packages/url/src/test/index.js index e258e112b2987..0051b9b89fc73 100644 --- a/packages/url/src/test/index.js +++ b/packages/url/src/test/index.js @@ -283,6 +283,8 @@ describe( 'getFilename', () => { ); expect( getFilename( 'a/path/' ) ).toBe( undefined ); expect( getFilename( '/' ) ).toBe( undefined ); + expect( getFilename( undefined ) ).toBe( undefined ); + expect( getFilename( null ) ).toBe( undefined ); } ); } ); diff --git a/phpunit/blocks/block-navigation-block-hooks-test.php b/phpunit/blocks/block-navigation-block-hooks-test.php index ba30c2e26d25c..3cb2e785a8e11 100644 --- a/phpunit/blocks/block-navigation-block-hooks-test.php +++ b/phpunit/blocks/block-navigation-block-hooks-test.php @@ -122,4 +122,39 @@ public function test_block_core_navigation_dont_modify_no_post_id() { 'Post content did not match the original markup.' ); } + + /** + * @covers ::gutenberg_block_core_navigation_update_ignore_hooked_blocks_meta + */ + public function test_block_core_navigation_retains_content_if_not_set() { + if ( ! function_exists( 'set_ignored_hooked_blocks_metadata' ) ) { + $this->markTestSkipped( 'Test skipped on WordPress versions that do not included required Block Hooks functionality.' ); + } + + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/navigation' => 'last_child', + ), + ) + ); + + $post = new stdClass(); + $post->ID = self::$navigation_post->ID; + $post->post_title = 'Navigation Menu with changes'; + + $post = gutenberg_block_core_navigation_update_ignore_hooked_blocks_meta( $post ); + + $this->assertSame( + 'Navigation Menu with changes', + $post->post_title, + 'Post title was changed.' + ); + + $this->assertFalse( + isset( $post->post_content ), + 'Post content should not be set.' + ); + } } diff --git a/phpunit/class-wp-theme-json-test.php b/phpunit/class-wp-theme-json-test.php index 6692962911012..17dff10346e3a 100644 --- a/phpunit/class-wp-theme-json-test.php +++ b/phpunit/class-wp-theme-json-test.php @@ -553,7 +553,7 @@ public function test_get_stylesheet() { ); $variables = ':root{--wp--preset--color--grey: grey;--wp--preset--gradient--custom-gradient: linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%);--wp--preset--font-size--small: 14px;--wp--preset--font-size--big: 41px;--wp--preset--font-family--arial: Arial, serif;}.wp-block-group{--wp--custom--base-font: 16;--wp--custom--line-height--small: 1.2;--wp--custom--line-height--medium: 1.4;--wp--custom--line-height--large: 1.8;}'; - $styles = static::$base_styles . 'body{color: var(--wp--preset--color--grey);}a:where(:not(.wp-element-button)){background-color: #333;color: #111;}.wp-element-button, .wp-block-button__link{box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}.wp-block-cover{min-height: unset;aspect-ratio: 16/9;}.wp-block-group{background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}.wp-block-group a:where(:not(.wp-element-button)){color: #111;}.wp-block-heading{color: #123456;}.wp-block-heading a:where(:not(.wp-element-button)){background-color: #333;color: #111;font-size: 60px;}.wp-block-post-date{color: #123456;}.wp-block-post-date a:where(:not(.wp-element-button)){background-color: #777;color: #555;}.wp-block-post-excerpt{column-count: 2;}.wp-block-image{margin-bottom: 30px;}.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}.wp-block-image img, .wp-block-image .components-placeholder{filter: var(--wp--preset--duotone--custom-duotone);}'; + $styles = static::$base_styles . ':where(body){color: var(--wp--preset--color--grey);}:where(a:where(:not(.wp-element-button))){background-color: #333;color: #111;}:where(.wp-element-button, .wp-block-button__link){box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.66);}:where(.wp-block-cover){min-height: unset;aspect-ratio: 16/9;}:where(.wp-block-group){background: var(--wp--preset--gradient--custom-gradient);border-radius: 10px;min-height: 50vh;padding: 24px;}:where(.wp-block-group a:where(:not(.wp-element-button))){color: #111;}:where(.wp-block-heading){color: #123456;}:where(.wp-block-heading a:where(:not(.wp-element-button))){background-color: #333;color: #111;font-size: 60px;}:where(.wp-block-post-date){color: #123456;}:where(.wp-block-post-date a:where(:not(.wp-element-button))){background-color: #777;color: #555;}:where(.wp-block-post-excerpt){column-count: 2;}:where(.wp-block-image){margin-bottom: 30px;}:where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}:where(.wp-block-image img, .wp-block-image .components-placeholder){filter: var(--wp--preset--duotone--custom-duotone);}'; $presets = '.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}.has-custom-gradient-gradient-background{background: var(--wp--preset--gradient--custom-gradient) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-big-font-size{font-size: var(--wp--preset--font-size--big) !important;}.has-arial-font-family{font-family: var(--wp--preset--font-family--arial) !important;}'; $all = $variables . $styles . $presets; @@ -599,7 +599,7 @@ public function test_get_stylesheet_support_for_shorthand_and_longhand_values() ) ); - $styles = static::$base_styles . '.wp-block-group{border-radius: 10px;margin: 1em;padding: 24px;}.wp-block-image{margin-bottom: 30px;padding-top: 15px;}.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder{border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; + $styles = static::$base_styles . ':where(.wp-block-group){border-radius: 10px;margin: 1em;padding: 24px;}:where(.wp-block-image){margin-bottom: 30px;padding-top: 15px;}:where(.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder){border-top-left-radius: 10px;border-bottom-right-radius: 1em;}'; $this->assertSame( $styles, $theme_json->get_stylesheet() ); $this->assertSame( $styles, $theme_json->get_stylesheet( array( 'styles' ) ) ); } @@ -712,7 +712,7 @@ public function test_get_stylesheet_preset_rules_come_after_block_rules() { ) ); - $styles = static::$base_styles . '.wp-block-group{color: red;}'; + $styles = static::$base_styles . ':where(.wp-block-group){color: red;}'; $presets = '.wp-block-group.has-grey-color{color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.wp-block-group.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}'; $variables = '.wp-block-group{--wp--preset--color--grey: grey;}'; @@ -799,7 +799,7 @@ public function test_get_stylesheet_preset_values_are_marked_as_important() { ); $this->assertSame( - ':root{--wp--preset--color--grey: grey;}' . static::$base_styles . 'p{background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', + ':root{--wp--preset--color--grey: grey;}' . static::$base_styles . ':where(p){background-color: blue;color: red;font-size: 12px;line-height: 1.3;}.has-grey-color{color: var(--wp--preset--color--grey) !important;}.has-grey-background-color{background-color: var(--wp--preset--color--grey) !important;}.has-grey-border-color{border-color: var(--wp--preset--color--grey) !important;}', $theme_json->get_stylesheet() ); } @@ -837,7 +837,7 @@ public function test_get_stylesheet_handles_whitelisted_element_pseudo_selectors ) ); - $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + $element_styles = ':where(a:where(:not(.wp-element-button))){background-color: red;color: green;}:where(a:where(:not(.wp-element-button)):hover){background-color: green;color: red;font-size: 10em;text-transform: uppercase;}:where(a:where(:not(.wp-element-button)):focus){background-color: black;color: yellow;}'; $expected = static::$base_styles . $element_styles; @@ -874,7 +874,7 @@ public function test_get_stylesheet_handles_only_pseudo_selector_rules_for_given ) ); - $element_styles = 'a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + $element_styles = ':where(a:where(:not(.wp-element-button)):hover){background-color: green;color: red;font-size: 10em;text-transform: uppercase;}:where(a:where(:not(.wp-element-button)):focus){background-color: black;color: yellow;}'; $expected = static::$base_styles . $element_styles; @@ -911,7 +911,7 @@ public function test_get_stylesheet_ignores_pseudo_selectors_on_non_whitelisted_ ) ); - $element_styles = 'h4{background-color: red;color: green;}'; + $element_styles = ':where(h4){background-color: red;color: green;}'; $expected = static::$base_styles . $element_styles; @@ -948,7 +948,7 @@ public function test_get_stylesheet_ignores_non_whitelisted_pseudo_selectors() { ) ); - $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;}'; + $element_styles = ':where(a:where(:not(.wp-element-button))){background-color: red;color: green;}:where(a:where(:not(.wp-element-button)):hover){background-color: green;color: red;}'; $expected = static::$base_styles . $element_styles; @@ -994,7 +994,7 @@ public function test_get_stylesheet_handles_priority_of_elements_vs_block_elemen ) ); - $element_styles = '.wp-block-group a:where(:not(.wp-element-button)){background-color: red;color: green;}.wp-block-group a:where(:not(.wp-element-button)):hover{background-color: green;color: red;font-size: 10em;text-transform: uppercase;}.wp-block-group a:where(:not(.wp-element-button)):focus{background-color: black;color: yellow;}'; + $element_styles = ':where(.wp-block-group a:where(:not(.wp-element-button))){background-color: red;color: green;}:where(.wp-block-group a:where(:not(.wp-element-button)):hover){background-color: green;color: red;font-size: 10em;text-transform: uppercase;}:where(.wp-block-group a:where(:not(.wp-element-button)):focus){background-color: black;color: yellow;}'; $expected = static::$base_styles . $element_styles; @@ -1039,7 +1039,7 @@ public function test_get_stylesheet_handles_whitelisted_block_level_element_pseu ) ); - $element_styles = 'a:where(:not(.wp-element-button)){background-color: red;color: green;}a:where(:not(.wp-element-button)):hover{background-color: green;color: red;}.wp-block-group a:where(:not(.wp-element-button)):hover{background-color: black;color: yellow;}'; + $element_styles = ':where(a:where(:not(.wp-element-button))){background-color: red;color: green;}:where(a:where(:not(.wp-element-button)):hover){background-color: green;color: red;}:where(.wp-block-group a:where(:not(.wp-element-button)):hover){background-color: black;color: yellow;}'; $expected = static::$base_styles . $element_styles; @@ -1101,7 +1101,7 @@ public function test_get_stylesheet_with_deprecated_feature_level_selectors() { ); $base_styles = ':root{--wp--preset--color--green: green;}' . static::$base_styles; - $block_styles = '.wp-block-test, .wp-block-test__wrapper{color: green;}.wp-block-test .inner, .wp-block-test__wrapper .inner{border-radius: 9999px;padding: 20px;}.wp-block-test .sub-heading, .wp-block-test__wrapper .sub-heading{font-size: 3em;}'; + $block_styles = ':where(.wp-block-test, .wp-block-test__wrapper){color: green;}:where(.wp-block-test .inner, .wp-block-test__wrapper .inner){border-radius: 9999px;padding: 20px;}:where(.wp-block-test .sub-heading, .wp-block-test__wrapper .sub-heading){font-size: 3em;}'; $preset_styles = '.has-green-color{color: var(--wp--preset--color--green) !important;}.has-green-background-color{background-color: var(--wp--preset--color--green) !important;}.has-green-border-color{border-color: var(--wp--preset--color--green) !important;}'; $expected = $base_styles . $block_styles . $preset_styles; @@ -1163,7 +1163,7 @@ public function test_get_stylesheet_with_block_json_selectors() { ); $base_styles = ':root{--wp--preset--color--green: green;}' . static::$base_styles; - $block_styles = '.custom-root-selector{background-color: grey;padding: 20px;}.custom-root-selector img{border-radius: 9999px;}.custom-root-selector > figcaption{color: navy;font-size: 3em;}'; + $block_styles = ':where(.custom-root-selector){background-color: grey;padding: 20px;}:where(.custom-root-selector img){border-radius: 9999px;}:where(.custom-root-selector > figcaption){color: navy;font-size: 3em;}'; $preset_styles = '.has-green-color{color: var(--wp--preset--color--green) !important;}.has-green-background-color{background-color: var(--wp--preset--color--green) !important;}.has-green-border-color{border-color: var(--wp--preset--color--green) !important;}'; $expected = $base_styles . $block_styles . $preset_styles; @@ -1332,7 +1332,7 @@ public function test_get_stylesheet_generates_valid_block_gap_values_and_skips_n $this->assertSame( 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: 1rem; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child:first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child:last-child { margin-block-end: 0; }:root { --wp--style--block-gap: 1rem; }:where(body .is-layout-flow) > :first-child:first-child{margin-block-start: 0;}:where(body .is-layout-flow) > :last-child:last-child{margin-block-end: 0;}:where(body .is-layout-flow) > *{margin-block-start: 1rem;margin-block-end: 0;}:where(body .is-layout-constrained) > :first-child:first-child{margin-block-start: 0;}:where(body .is-layout-constrained) > :last-child:last-child{margin-block-end: 0;}:where(body .is-layout-constrained) > *{margin-block-start: 1rem;margin-block-end: 0;}:where(body .is-layout-flex) {gap: 1rem;}:where(body .is-layout-grid) {gap: 1rem;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}' . - '.wp-block-post-content{color: gray;}.wp-block-social-links-is-layout-flow > :first-child:first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-flow > :last-child:last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > :first-child:first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-constrained > :last-child:last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-flex{gap: 0;}.wp-block-social-links-is-layout-grid{gap: 0;}.wp-block-buttons-is-layout-flow > :first-child:first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-flow > :last-child:last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > :first-child:first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-constrained > :last-child:last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-flex{gap: 0;}.wp-block-buttons-is-layout-grid{gap: 0;}', + ':where(.wp-block-post-content){color: gray;}.wp-block-social-links-is-layout-flow > :first-child:first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-flow > :last-child:last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > :first-child:first-child{margin-block-start: 0;}.wp-block-social-links-is-layout-constrained > :last-child:last-child{margin-block-end: 0;}.wp-block-social-links-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-social-links-is-layout-flex{gap: 0;}.wp-block-social-links-is-layout-grid{gap: 0;}.wp-block-buttons-is-layout-flow > :first-child:first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-flow > :last-child:last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-flow > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > :first-child:first-child{margin-block-start: 0;}.wp-block-buttons-is-layout-constrained > :last-child:last-child{margin-block-end: 0;}.wp-block-buttons-is-layout-constrained > *{margin-block-start: 0;margin-block-end: 0;}.wp-block-buttons-is-layout-flex{gap: 0;}.wp-block-buttons-is-layout-grid{gap: 0;}', $theme_json->get_stylesheet() ); } @@ -1364,7 +1364,7 @@ public function test_get_stylesheet_returns_outline_styles() { ) ); - $element_styles = '.wp-element-button, .wp-block-button__link{outline-color: red;outline-offset: 3px;outline-style: dashed;outline-width: 3px;}.wp-element-button:hover, .wp-block-button__link:hover{outline-color: blue;outline-offset: 3px;outline-style: solid;outline-width: 3px;}'; + $element_styles = ':where(.wp-element-button, .wp-block-button__link){outline-color: red;outline-offset: 3px;outline-style: dashed;outline-width: 3px;}:where(.wp-element-button:hover, .wp-block-button__link:hover){outline-color: blue;outline-offset: 3px;outline-style: solid;outline-width: 3px;}'; $expected = static::$base_styles . $element_styles; $this->assertSame( $expected, $theme_json->get_stylesheet() ); @@ -1389,7 +1389,7 @@ public function test_get_stylesheet_custom_root_selector() { // Results also include root site blocks styles which hard code // `body { margin: 0; }`. $this->assertSame( - 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }.custom{color: teal;}', + 'body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.custom){color: teal;}', $actual ); } @@ -1448,7 +1448,7 @@ public function test_get_stylesheet_generates_fluid_typography_values() { // Results also include root site blocks styles. $this->assertSame( - ':root{--wp--preset--font-size--pickles: clamp(14px, 0.875rem + ((1vw - 3.2px) * 0.156), 16px);--wp--preset--font-size--toast: clamp(14.642px, 0.915rem + ((1vw - 3.2px) * 0.575), 22px);}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}body{font-size: clamp(0.875em, 0.875rem + ((1vw - 0.2em) * 0.156), 1em);}h1{font-size: clamp(50.171px, 3.136rem + ((1vw - 3.2px) * 3.893), 100px);}.wp-block-test-clamp-me{font-size: clamp(27.894px, 1.743rem + ((1vw - 3.2px) * 1.571), 48px);}.has-pickles-font-size{font-size: var(--wp--preset--font-size--pickles) !important;}.has-toast-font-size{font-size: var(--wp--preset--font-size--toast) !important;}', + ':root{--wp--preset--font-size--pickles: clamp(14px, 0.875rem + ((1vw - 3.2px) * 0.156), 16px);--wp--preset--font-size--toast: clamp(14.642px, 0.915rem + ((1vw - 3.2px) * 0.575), 22px);}body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(body){font-size: clamp(0.875em, 0.875rem + ((1vw - 0.2em) * 0.156), 1em);}:where(h1){font-size: clamp(50.171px, 3.136rem + ((1vw - 3.2px) * 3.893), 100px);}:where(.wp-block-test-clamp-me){font-size: clamp(27.894px, 1.743rem + ((1vw - 3.2px) * 1.571), 48px);}.has-pickles-font-size{font-size: var(--wp--preset--font-size--pickles) !important;}.has-toast-font-size{font-size: var(--wp--preset--font-size--toast) !important;}', $theme_json->get_stylesheet() ); } @@ -3517,7 +3517,7 @@ public function test_get_property_value_valid() { ) ); - $color_styles = 'body{background-color: #ffffff;color: #000000;}.wp-element-button, .wp-block-button__link{background-color: #000000;color: #ffffff;}'; + $color_styles = ':where(body){background-color: #ffffff;color: #000000;}:where(.wp-element-button, .wp-block-button__link){background-color: #000000;color: #ffffff;}'; $expected = static::$base_styles . $color_styles; $this->assertSame( $expected, $theme_json->get_stylesheet() ); } @@ -3595,7 +3595,7 @@ public function test_get_property_value_loop() { ) ); - $color_styles = 'body{background-color: #ffffff;}.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $color_styles = ':where(body){background-color: #ffffff;}:where(.wp-element-button, .wp-block-button__link){color: #ffffff;}'; $expected = static::$base_styles . $color_styles; $this->assertSame( $expected, $theme_json->get_stylesheet() ); } @@ -3628,7 +3628,7 @@ public function test_get_property_value_recursion() { ) ); - $color_styles = 'body{background-color: #ffffff;color: #ffffff;}.wp-element-button, .wp-block-button__link{color: #ffffff;}'; + $color_styles = ':where(body){background-color: #ffffff;color: #ffffff;}:where(.wp-element-button, .wp-block-button__link){color: #ffffff;}'; $expected = static::$base_styles . $color_styles; $this->assertSame( $expected, $theme_json->get_stylesheet() ); } @@ -3652,7 +3652,7 @@ public function test_get_property_value_self() { ) ); - $color_styles = 'body{background-color: #ffffff;}'; + $color_styles = ':where(body){background-color: #ffffff;}'; $expected = static::$base_styles . $color_styles; $this->assertSame( $expected, $theme_json->get_stylesheet() ); } @@ -3682,7 +3682,7 @@ public function test_get_styles_for_block_with_padding_aware_alignments() { 'selector' => 'body', ); - $expected = 'body { margin: 0; }.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}body{--wp--style--root--padding-top: 10px;--wp--style--root--padding-right: 12px;--wp--style--root--padding-bottom: 10px;--wp--style--root--padding-left: 12px;}'; + $expected = 'body { margin: 0; }.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding:not(.wp-block-block)) { padding-right: 0; padding-left: 0; }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(.has-global-padding:not(.wp-block-block)) > .alignfull { margin-right: 0; margin-left: 0; }.has-global-padding > .alignfull:where(:not(.has-global-padding):not(.is-layout-flex):not(.is-layout-grid)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding :where(.has-global-padding) > .alignfull:where(:not(.has-global-padding)) > :where([class*="wp-block-"]:not(.alignfull):not([class*="__"]),.wp-block:not(.alignfull),p,h1,h2,h3,h4,h5,h6,ul,ol) { padding-right: 0; padding-left: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(body){--wp--style--root--padding-top: 10px;--wp--style--root--padding-right: 12px;--wp--style--root--padding-bottom: 10px;--wp--style--root--padding-left: 12px;}'; $root_rules = $theme_json->get_root_layout_rules( WP_Theme_JSON_Gutenberg::ROOT_BLOCK_SELECTOR, $metadata ); $style_rules = $theme_json->get_styles_for_block( $metadata ); $this->assertSame( $expected, $root_rules . $style_rules ); @@ -3710,7 +3710,7 @@ public function test_get_styles_for_block_without_padding_aware_alignments() { 'selector' => 'body', ); - $expected = static::$base_styles . 'body{padding-top: 10px;padding-right: 12px;padding-bottom: 10px;padding-left: 12px;}'; + $expected = static::$base_styles . ':where(body){padding-top: 10px;padding-right: 12px;padding-bottom: 10px;padding-left: 12px;}'; $root_rules = $theme_json->get_root_layout_rules( WP_Theme_JSON_Gutenberg::ROOT_BLOCK_SELECTOR, $metadata ); $style_rules = $theme_json->get_styles_for_block( $metadata ); $this->assertSame( $expected, $root_rules . $style_rules ); @@ -4680,7 +4680,7 @@ public function data_update_separator_declarations() { 'background' => 'blue', ), ), - 'expected_output' => static::$base_styles . '.wp-block-separator{background-color: blue;color: blue;}', + 'expected_output' => static::$base_styles . ':where(.wp-block-separator){background-color: blue;color: blue;}', ), // If background and text are defined, do not include border-color, as text color is enough. 'background and text, no border-color' => array( @@ -4690,7 +4690,7 @@ public function data_update_separator_declarations() { 'text' => 'red', ), ), - 'expected_output' => static::$base_styles . '.wp-block-separator{background-color: blue;color: red;}', + 'expected_output' => static::$base_styles . ':where(.wp-block-separator){background-color: blue;color: red;}', ), // If only text is defined, do not include border-color, as by itself is enough. 'only text' => array( @@ -4699,7 +4699,7 @@ public function data_update_separator_declarations() { 'text' => 'red', ), ), - 'expected_output' => static::$base_styles . '.wp-block-separator{color: red;}', + 'expected_output' => static::$base_styles . ':where(.wp-block-separator){color: red;}', ), // If background, text, and border-color are defined, include everything, CSS specificity will decide which to apply. 'background, text, and border-color' => array( @@ -4712,7 +4712,7 @@ public function data_update_separator_declarations() { 'color' => 'pink', ), ), - 'expected_output' => static::$base_styles . '.wp-block-separator{background-color: blue;border-color: pink;color: red;}', + 'expected_output' => static::$base_styles . ':where(.wp-block-separator){background-color: blue;border-color: pink;color: red;}', ), // If background and border color are defined, include everything, CSS specificity will decide which to apply. 'background, and border-color' => array( @@ -4724,7 +4724,7 @@ public function data_update_separator_declarations() { 'color' => 'pink', ), ), - 'expected_output' => static::$base_styles . '.wp-block-separator{background-color: blue;border-color: pink;}', + 'expected_output' => static::$base_styles . ':where(.wp-block-separator){background-color: blue;border-color: pink;}', ), ); } @@ -4788,7 +4788,7 @@ public function test_get_shadow_styles_for_blocks() { ); $global_styles = ':root{--wp--preset--shadow--natural: 5px 5px 0 0 black;}' . static::$base_styles; - $element_styles = 'a:where(:not(.wp-element-button)){box-shadow: var(--wp--preset--shadow--natural);}.wp-element-button, .wp-block-button__link{box-shadow: var(--wp--preset--shadow--natural);}p{box-shadow: var(--wp--preset--shadow--natural);}'; + $element_styles = ':where(a:where(:not(.wp-element-button))){box-shadow: var(--wp--preset--shadow--natural);}:where(.wp-element-button, .wp-block-button__link){box-shadow: var(--wp--preset--shadow--natural);}:where(p){box-shadow: var(--wp--preset--shadow--natural);}'; $expected_styles = $global_styles . $element_styles; $this->assertSame( $expected_styles, $theme_json->get_stylesheet() ); } @@ -4835,7 +4835,7 @@ public function test_get_top_level_background_image_styles() { ) ); - $expected_styles = "body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; + $expected_styles = "body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}:where(body){background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; $this->assertSame( $expected_styles, $theme_json->get_stylesheet(), 'Styles returned from "::get_stylesheet()" with top-level background styles type does not match expectations' ); $theme_json = new WP_Theme_JSON_Gutenberg( @@ -4852,7 +4852,7 @@ public function test_get_top_level_background_image_styles() { ) ); - $expected_styles = "body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; + $expected_styles = "body { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}:where(body){background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;}"; $this->assertSame( $expected_styles, $theme_json->get_stylesheet(), 'Styles returned from "::get_stylesheet()" with top-level background image as string type does not match expectations' ); } diff --git a/test/e2e/specs/editor/blocks/columns.spec.js b/test/e2e/specs/editor/blocks/columns.spec.js index 635d45dd99ce2..8ddf7e9377ff2 100644 --- a/test/e2e/specs/editor/blocks/columns.spec.js +++ b/test/e2e/specs/editor/blocks/columns.spec.js @@ -380,4 +380,33 @@ test.describe( 'Columns', () => { ] ); } ); } ); + + test( 'should arrow up into empty columns', async ( { editor, page } ) => { + await editor.insertBlock( { + name: 'core/columns', + innerBlocks: [ { name: 'core/column' }, { name: 'core/column' } ], + } ); + await editor.insertBlock( { + name: 'core/paragraph', + } ); + + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'ArrowUp' ); + await page.keyboard.press( 'Delete' ); + + await expect.poll( editor.getBlocks ).toMatchObject( [ + { + name: 'core/columns', + innerBlocks: [ + { + name: 'core/column', + }, + ], + }, + { + name: 'core/paragraph', + attributes: { content: '' }, + }, + ] ); + } ); } ); diff --git a/test/e2e/specs/editor/plugins/post-type-locking.spec.js b/test/e2e/specs/editor/plugins/post-type-locking.spec.js index cdfce4c08dcc9..dc56b6a9b3b42 100644 --- a/test/e2e/specs/editor/plugins/post-type-locking.spec.js +++ b/test/e2e/specs/editor/plugins/post-type-locking.spec.js @@ -328,6 +328,47 @@ test.describe( 'Post-type locking', () => { }, ] ); } ); + + test( 'should allow blocks to be switched to other types', async ( { + editor, + page, + } ) => { + await editor.canvas + .getByRole( 'document', { + name: 'Empty block', + } ) + .first() + .fill( 'p1' ); + + const blockSwitcher = page + .getByRole( 'toolbar', { name: 'Block tools' } ) + .getByRole( 'button', { name: 'Paragraph' } ); + + // Verify the block switcher exists. + await expect( blockSwitcher ).toHaveAttribute( + 'aria-haspopup', + 'true' + ); + + // Verify the correct block transforms appear. + await blockSwitcher.click(); + await expect( + page + .getByRole( 'menu', { name: 'Paragraph' } ) + .getByRole( 'menuitem' ) + ).toHaveText( [ + 'Heading', + 'List', + 'Quote', + 'Buttons', + 'Code', + 'Columns', + 'Group', + 'Preformatted', + 'Pullquote', + 'Verse', + ] ); + } ); } ); test.describe( 'template_lock all locked group', () => { diff --git a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt index aef1c3c1731d1..45d99c7c27e6f 100644 --- a/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt +++ b/test/e2e/specs/editor/various/__snapshots__/Inserting-blocks-firefox-webkit-inserts-p-59603-ragging-and-dropping-from-the-global-inserter-1-chromium.txt @@ -2,7 +2,7 @@

Dummy text

- +