diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php index fe22410c246573..768fde56ff06f3 100644 --- a/packages/block-library/src/query-pagination-next/index.php +++ b/packages/block-library/src/query-pagination-next/index.php @@ -63,7 +63,7 @@ function render_block_core_query_pagination_next( $attributes, $content, $block wp_reset_postdata(); // Restore original Post Data. } - if ( $enhanced_pagination ) { + if ( $enhanced_pagination && isset( $content ) ) { $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag( array( diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index dc61f5d38b2828..fc1fee08e82148 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -51,7 +51,7 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl ); } - if ( $enhanced_pagination ) { + if ( $enhanced_pagination && isset( $content ) ) { $p = new WP_HTML_Tag_Processor( $content ); if ( $p->next_tag( array( diff --git a/packages/block-library/src/query/edit/enhanced-pagination-modal.js b/packages/block-library/src/query/edit/enhanced-pagination-modal.js index 94c4b13b091439..d6a7b75e0aec9a 100644 --- a/packages/block-library/src/query/edit/enhanced-pagination-modal.js +++ b/packages/block-library/src/query/edit/enhanced-pagination-modal.js @@ -12,11 +12,7 @@ import { useState, useEffect } from '@wordpress/element'; /** * Internal dependencies */ -import { useUnsupportedBlockList } from '../utils'; - -const disableEnhancedPaginationDescription = __( - 'You have added unsupported blocks. For the enhanced pagination to work, remove them, then re-enable "Enhanced pagination" in the Query Block settings.' -); +import { useUnsupportedBlocks } from '../utils'; const modalDescriptionId = 'wp-block-query-enhanced-pagination-modal__description'; @@ -27,35 +23,53 @@ export default function EnhancedPaginationModal( { setAttributes, } ) { const [ isOpen, setOpen ] = useState( false ); - - const unsupported = useUnsupportedBlockList( clientId ); + const { hasBlocksFromPlugins, hasPostContentBlock, hasUnsupportedBlocks } = + useUnsupportedBlocks( clientId ); useEffect( () => { - setOpen( !! unsupported.length && enhancedPagination ); - }, [ unsupported.length, enhancedPagination, setOpen ] ); + if ( enhancedPagination && hasUnsupportedBlocks ) { + setAttributes( { enhancedPagination: false } ); + setOpen( true ); + } + }, [ enhancedPagination, hasUnsupportedBlocks, setAttributes ] ); + + const closeModal = () => { + setOpen( false ); + }; + + let notice = __( + 'If you still want to prevent full page reloads, remove that block, then disable "Force page reload" again in the Query Block settings.' + ); + if ( hasBlocksFromPlugins ) { + notice = + __( + 'Currently, avoiding full page reloads is not possible when blocks from plugins are present inside the Query block.' + ) + + ' ' + + notice; + } else if ( hasPostContentBlock ) { + notice = + __( + 'Currently, avoiding full page reloads is not possible when a Content block is present inside the Query block.' + ) + + ' ' + + notice; + } return ( isOpen && ( - - { disableEnhancedPaginationDescription } - - diff --git a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js index 64b4f5d96eb38b..de889c0715c07a 100644 --- a/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js +++ b/packages/block-library/src/query/edit/inspector-controls/enhanced-pagination-control.js @@ -1,58 +1,45 @@ /** * WordPress dependencies */ -import { ToggleControl, Notice } from '@wordpress/components'; +import { ToggleControl } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; -import { BlockTitle } from '@wordpress/block-editor'; /** * Internal dependencies */ -import { useUnsupportedBlockList } from '../../utils'; +import { useUnsupportedBlocks } from '../../utils'; export default function EnhancedPaginationControl( { enhancedPagination, setAttributes, clientId, } ) { - const unsupported = useUnsupportedBlockList( clientId ); + const { hasUnsupportedBlocks } = useUnsupportedBlocks( clientId ); + + let help = __( 'Browsing between pages requires a full page reload.' ); + if ( enhancedPagination ) { + help = __( + "Browsing between pages won't require a full page reload, unless non-compatible blocks are detected." + ); + } else if ( hasUnsupportedBlocks ) { + help = __( + "Force page reload can't be disabled because there are non-compatible blocks inside the Query block." + ); + } return ( <> { setAttributes( { - enhancedPagination: !! value, + enhancedPagination: ! value, } ); } } /> - { !! unsupported.length && ( - - { __( - "Enhanced pagination doesn't support the following blocks:" - ) } -
    - { unsupported.map( ( id ) => ( -
  • - -
  • - ) ) } -
- { __( - 'If you want to enable it, you have to remove all unsupported blocks first.' - ) } -
- ) } ); } diff --git a/packages/block-library/src/query/index.php b/packages/block-library/src/query/index.php index 7fe27b1400ac18..fed28137e70857 100644 --- a/packages/block-library/src/query/index.php +++ b/packages/block-library/src/query/index.php @@ -129,3 +129,86 @@ function register_block_core_query() { ); } add_action( 'init', 'register_block_core_query' ); + +/** + * Traverse the tree of blocks looking for any plugin block (i.e., a block from + * an installed plugin) inside a Query block with the enhanced pagination + * enabled. If at least one is found, the enhanced pagination is effectively + * disabled to prevent any potential incompatibilities. + * + * @since 6.4.0 + * + * @param array $parsed_block The block being rendered. + * @return string Returns the parsed block, unmodified. + */ +function block_core_query_disable_enhanced_pagination( $parsed_block ) { + static $enhanced_query_stack = array(); + static $dirty_enhanced_queries = array(); + static $render_query_callback = null; + + $block_name = $parsed_block['blockName']; + + if ( + 'core/query' === $block_name && + isset( $parsed_block['attrs']['enhancedPagination'] ) && + true === $parsed_block['attrs']['enhancedPagination'] && + isset( $parsed_block['attrs']['queryId'] ) + ) { + $enhanced_query_stack[] = $parsed_block['attrs']['queryId']; + + if ( ! isset( $render_query_callback ) ) { + /** + * Filter that disables the enhanced pagination feature during block + * rendering when a plugin block has been found inside. It does so + * by adding an attribute called `data-wp-navigation-disabled` which + * is later handled by the front-end logic. + * + * @param string $content The block content. + * @param array $block The full block, including name and attributes. + * @return string Returns the modified output of the query block. + */ + $render_query_callback = static function ( $content, $block ) use ( &$enhanced_query_stack, &$dirty_enhanced_queries, &$render_query_callback ) { + $has_enhanced_pagination = + isset( $block['attrs']['enhancedPagination'] ) && + true === $block['attrs']['enhancedPagination'] && + isset( $block['attrs']['queryId'] ); + + if ( ! $has_enhanced_pagination ) { + return $content; + } + + if ( isset( $dirty_enhanced_queries[ $block['attrs']['queryId'] ] ) ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag() ) { + $p->set_attribute( 'data-wp-navigation-disabled', 'true' ); + } + $content = $p->get_updated_html(); + $dirty_enhanced_queries[ $block['attrs']['queryId'] ] = null; + } + + array_pop( $enhanced_query_stack ); + + if ( empty( $enhanced_query_stack ) ) { + remove_filter( 'render_block_core/query', $render_query_callback ); + $render_query_callback = null; + } + + return $content; + }; + + add_filter( 'render_block_core/query', $render_query_callback, 10, 2 ); + } + } elseif ( + ! empty( $enhanced_query_stack ) && + isset( $block_name ) && + ( ! str_starts_with( $block_name, 'core/' ) || 'core/post-content' === $block_name ) + ) { + foreach ( $enhanced_query_stack as $query_id ) { + $dirty_enhanced_queries[ $query_id ] = true; + } + } + + return $parsed_block; +} + +add_filter( 'render_block_data', 'block_core_query_disable_enhanced_pagination', 10, 1 ); diff --git a/packages/block-library/src/query/utils.js b/packages/block-library/src/query/utils.js index 322c8f4c1453fb..2ce4acd4a782cf 100644 --- a/packages/block-library/src/query/utils.js +++ b/packages/block-library/src/query/utils.js @@ -346,29 +346,43 @@ export const usePatterns = ( clientId, name ) => { }; /** - * Hook that returns a list of unsupported blocks inside the Query Loop with the - * given `clientId`. + * The object returned by useUnsupportedBlocks with info about the type of + * unsupported blocks present inside the Query block. + * + * @typedef {Object} UnsupportedBlocksInfo + * @property {boolean} hasBlocksFromPlugins True if blocks from plugins are present. + * @property {boolean} hasPostContentBlock True if a 'core/post-content' block is present. + * @property {boolean} hasUnsupportedBlocks True if there are any unsupported blocks. + */ + +/** + * Hook that returns an object with information about the unsupported blocks + * present inside a Query Loop with the given `clientId`. The returned object + * contains props that are true when a certain type of unsupported block is + * present. * * @param {string} clientId The block's client ID. - * @return {string[]} List of block titles. + * @return {UnsupportedBlocksInfo} The object containing the information. */ -export const useUnsupportedBlockList = ( clientId ) => { +export const useUnsupportedBlocks = ( clientId ) => { return useSelect( ( select ) => { const { getClientIdsOfDescendants, getBlockName } = select( blockEditorStore ); - - return getClientIdsOfDescendants( clientId ).filter( + const blocks = {}; + getClientIdsOfDescendants( clientId ).forEach( ( descendantClientId ) => { const blockName = getBlockName( descendantClientId ); - return ( - ! blockName.startsWith( 'core/' ) || - blockName === 'core/post-content' || - blockName === 'core/template-part' || - blockName === 'core/block' - ); + if ( ! blockName.startsWith( 'core/' ) ) { + blocks.hasBlocksFromPlugins = true; + } else if ( blockName === 'core/post-content' ) { + blocks.hasPostContentBlock = true; + } } ); + blocks.hasUnsupportedBlocks = + blocks.hasBlocksFromPlugins || blocks.hasPostContentBlock; + return blocks; }, [ clientId ] ); diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js index 3e50d521ea6ef7..983cfc50aba989 100644 --- a/packages/block-library/src/query/view.js +++ b/packages/block-library/src/query/view.js @@ -33,7 +33,14 @@ store( { core: { query: { navigate: async ( { event, ref, context } ) => { - if ( isValidLink( ref ) && isValidEvent( event ) ) { + const isDisabled = ref.closest( '[data-wp-navigation-id]' ) + ?.dataset.wpNavigationDisabled; + + if ( + isValidLink( ref ) && + isValidEvent( event ) && + ! isDisabled + ) { event.preventDefault(); const id = ref.closest( '[data-wp-navigation-id]' ) @@ -70,7 +77,9 @@ store( { } }, prefetch: async ( { ref } ) => { - if ( isValidLink( ref ) ) { + const isDisabled = ref.closest( '[data-wp-navigation-id]' ) + ?.dataset.wpNavigationDisabled; + if ( isValidLink( ref ) && ! isDisabled ) { await prefetch( ref.href ); } }, diff --git a/phpunit/blocks/render-query-test.php b/phpunit/blocks/render-query-test.php new file mode 100644 index 00000000000000..d98d697ebc976b --- /dev/null +++ b/phpunit/blocks/render-query-test.php @@ -0,0 +1,263 @@ +post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_name' => 'post-1', + 'post_title' => 'Post 1', + 'post_content' => 'Post 1 content', + 'post_excerpt' => 'Post 1', + ) + ); + + self::$post_2 = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_name' => 'post-2', + 'post_title' => 'Post 2', + 'post_content' => 'Post 2 content', + 'post_excerpt' => 'Post 2', + ) + ); + + self::$post_3 = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_name' => 'post-2', + 'post_title' => 'Post 2', + 'post_content' => 'Post 2 content', + 'post_excerpt' => 'Post 2', + ) + ); + + register_block_type( + 'test/plugin-block', + array( + 'render_callback' => static function () { + return '
Test
'; + }, + ) + ); + } + + public function tear_down() { + unregister_block_type( 'test/plugin-block' ); + parent::tear_down(); + } + + /** + * Tests that the `core/query` block adds the corresponding directives when + * the `enhancedPagination` attribute is set. + */ + public function test_rendering_query_with_enhanced_pagination() { + global $wp_query, $wp_the_query, $paged; + + $content = << +
+ + + + + + +
+ +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + 'paged' => 2, + ) + ); + + $wp_the_query = $wp_query; + $prev_paged = $paged; + $paged = 2; + + $output = do_blocks( $content ); + + $paged = $prev_paged; + + $p = new WP_HTML_Tag_Processor( $output ); + + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( '{"core":{"query":{"loadingText":"Loading page, please wait.","loadedText":"Page Loaded."}}}', $p->get_attribute( 'data-wp-context' ) ); + $this->assertSame( 'query-0', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( true, $p->get_attribute( 'data-wp-interactive' ) ); + + $p->next_tag( array( 'class_name' => 'wp-block-post' ) ); + $this->assertSame( 'post-template-item-' . self::$post_2->ID, $p->get_attribute( 'data-wp-key' ) ); + + $p->next_tag( array( 'class_name' => 'wp-block-query-pagination-previous' ) ); + $this->assertSame( 'query-pagination-previous', $p->get_attribute( 'data-wp-key' ) ); + $this->assertSame( 'actions.core.query.navigate', $p->get_attribute( 'data-wp-on--click' ) ); + $this->assertSame( 'actions.core.query.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); + $this->assertSame( 'effects.core.query.prefetch', $p->get_attribute( 'data-wp-effect' ) ); + + $p->next_tag( array( 'class_name' => 'wp-block-query-pagination-next' ) ); + $this->assertSame( 'query-pagination-next', $p->get_attribute( 'data-wp-key' ) ); + $this->assertSame( 'actions.core.query.navigate', $p->get_attribute( 'data-wp-on--click' ) ); + $this->assertSame( 'actions.core.query.prefetch', $p->get_attribute( 'data-wp-on--mouseenter' ) ); + $this->assertSame( 'effects.core.query.prefetch', $p->get_attribute( 'data-wp-effect' ) ); + + $p->next_tag( array( 'class_name' => 'screen-reader-text' ) ); + $this->assertSame( 'polite', $p->get_attribute( 'aria-live' ) ); + $this->assertSame( 'context.core.query.message', $p->get_attribute( 'data-wp-text' ) ); + + $p->next_tag( array( 'class_name' => 'wp-block-query__enhanced-pagination-animation' ) ); + $this->assertSame( 'selectors.core.query.startAnimation', $p->get_attribute( 'data-wp-class--start-animation' ) ); + $this->assertSame( 'selectors.core.query.finishAnimation', $p->get_attribute( 'data-wp-class--finish-animation' ) ); + } + + /** + * Tests that the `core/query` block adds an extra attribute to disable the + * enhanced pagination in the browser when a plugin block is found inside. + */ + public function test_rendering_query_with_enhanced_pagination_auto_disabled_when_plugins_blocks_are_found() { + global $wp_query, $wp_the_query; + + $content = << +
+ + + +
+ +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + ) + ); + + $wp_the_query = $wp_query; + + $output = do_blocks( $content ); + + $p = new WP_HTML_Tag_Processor( $output ); + + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( 'query-0', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); + } + + /** + * Tests that the `core/query` block adds an extra attribute to disable the + * enhanced pagination in the browser when a post content block is found inside. + */ + public function test_rendering_query_with_enhanced_pagination_auto_disabled_when_post_content_block_is_found() { + global $wp_query, $wp_the_query; + + $content = << +
+ + + +
+ +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + ) + ); + + $wp_the_query = $wp_query; + + $output = do_blocks( $content ); + + $p = new WP_HTML_Tag_Processor( $output ); + + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( 'query-0', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); + } + + /** + * Tests that the correct `core/query` blocks get the attribute that + * disables enhanced pagination only if they contain a descendant that is + * not supported (i.e., a plugin block). + */ + public function test_rendering_nested_queries_with_enhanced_pagination_auto_disabled() { + global $wp_query, $wp_the_query; + + $content = << +
+ + +
+ + +
+ + +
+ + + +
+ + +
+ +HTML; + + // Set main query to single post. + $wp_query = new WP_Query( + array( + 'posts_per_page' => 1, + ) + ); + + $wp_the_query = $wp_query; + + $output = do_blocks( $content ); + + $p = new WP_HTML_Tag_Processor( $output ); + + // Query 0 contains a plugin block inside query-2 -> disabled. + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( 'query-0', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); + + // Query 1 does not contain a plugin block -> enabled. + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( 'query-1', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( null, $p->get_attribute( 'data-wp-navigation-disabled' ) ); + + // Query 2 contains a plugin block -> disabled. + $p->next_tag( array( 'class_name' => 'wp-block-query' ) ); + $this->assertSame( 'query-2', $p->get_attribute( 'data-wp-navigation-id' ) ); + $this->assertSame( 'true', $p->get_attribute( 'data-wp-navigation-disabled' ) ); + } +}