diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md index fde7f405558cc..0f25cead8b07a 100644 --- a/docs/reference-guides/core-blocks.md +++ b/docs/reference-guides/core-blocks.md @@ -665,7 +665,7 @@ An advanced block that allows displaying post types based on different query par - **Name:** core/query - **Category:** theme - **Supports:** align (full, wide), layout, ~~html~~ -- **Attributes:** namespace, query, queryId, tagName +- **Attributes:** enhancedPagination, namespace, query, queryId, tagName ## No results diff --git a/packages/block-library/src/post-template/block.json b/packages/block-library/src/post-template/block.json index 1f0d4677e6727..48804de75d2ca 100644 --- a/packages/block-library/src/post-template/block.json +++ b/packages/block-library/src/post-template/block.json @@ -13,7 +13,8 @@ "queryContext", "displayLayout", "templateSlug", - "previewPostType" + "previewPostType", + "enhancedPagination" ], "supports": { "reusable": false, diff --git a/packages/block-library/src/post-template/index.php b/packages/block-library/src/post-template/index.php index d72e66ce65350..e616939514a68 100644 --- a/packages/block-library/src/post-template/index.php +++ b/packages/block-library/src/post-template/index.php @@ -43,8 +43,9 @@ function block_core_post_template_uses_featured_image( $inner_blocks ) { * @return string Returns the output of the query, structured using the layout defined by the block's inner blocks. */ function render_block_core_post_template( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; // Use global query if needed. $use_global_query = ( isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] ); @@ -120,7 +121,10 @@ function render_block_core_post_template( $attributes, $content, $block ) { // Wrap the render inner blocks in a `li` element with the appropriate post classes. $post_classes = implode( ' ', get_post_class( 'wp-block-post' ) ); - $content .= '
  • ' . $block_content . '
  • '; + + $inner_block_directives = $enhanced_pagination ? ' data-wp-key="post-template-item-' . $post_id . '"' : ''; + + $content .= '' . $block_content . ''; } /* diff --git a/packages/block-library/src/query-pagination-next/block.json b/packages/block-library/src/query-pagination-next/block.json index 60d44d7ca17b5..95b1169dc992f 100644 --- a/packages/block-library/src/query-pagination-next/block.json +++ b/packages/block-library/src/query-pagination-next/block.json @@ -12,7 +12,13 @@ "type": "string" } }, - "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ], + "usesContext": [ + "queryId", + "query", + "paginationArrow", + "showLabel", + "enhancedPagination" + ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-next/index.php b/packages/block-library/src/query-pagination-next/index.php index f0ded727ee8a9..83c177c6fb0a9 100644 --- a/packages/block-library/src/query-pagination-next/index.php +++ b/packages/block-library/src/query-pagination-next/index.php @@ -15,9 +15,10 @@ * @return string Returns the next posts link for the query pagination. */ function render_block_core_query_pagination_next( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; $wrapper_attributes = get_block_wrapper_attributes(); $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; @@ -61,6 +62,22 @@ function render_block_core_query_pagination_next( $attributes, $content, $block } wp_reset_postdata(); // Restore original Post Data. } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wp-block-query-pagination-next', + ) + ) ) { + $p->set_attribute( 'data-wp-key', 'query-pagination-next' ); + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' ); + $content = $p->get_updated_html(); + } + } + return $content; } diff --git a/packages/block-library/src/query-pagination-numbers/block.json b/packages/block-library/src/query-pagination-numbers/block.json index 09b001c94dbf5..f05e269d2ece2 100644 --- a/packages/block-library/src/query-pagination-numbers/block.json +++ b/packages/block-library/src/query-pagination-numbers/block.json @@ -13,7 +13,7 @@ "default": 2 } }, - "usesContext": [ "queryId", "query" ], + "usesContext": [ "queryId", "query", "enhancedPagination" ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-numbers/index.php b/packages/block-library/src/query-pagination-numbers/index.php index c37a4ae9fac7e..98098533adac7 100644 --- a/packages/block-library/src/query-pagination-numbers/index.php +++ b/packages/block-library/src/query-pagination-numbers/index.php @@ -15,9 +15,10 @@ * @return string Returns the pagination numbers for the Query. */ function render_block_core_query_pagination_numbers( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; - $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $max_page = isset( $block->context['query']['pages'] ) ? (int) $block->context['query']['pages'] : 0; $wrapper_attributes = get_block_wrapper_attributes(); $content = ''; @@ -84,9 +85,24 @@ function render_block_core_query_pagination_numbers( $attributes, $content, $blo wp_reset_postdata(); // Restore original Post Data. $wp_query = $prev_wp_query; } + if ( empty( $content ) ) { return ''; } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + while ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'page-numbers', + ) + ) ) { + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + } + $content = $p->get_updated_html(); + } + return sprintf( '
    %2$s
    ', $wrapper_attributes, diff --git a/packages/block-library/src/query-pagination-previous/block.json b/packages/block-library/src/query-pagination-previous/block.json index d13442f831c97..fbaac543c1da3 100644 --- a/packages/block-library/src/query-pagination-previous/block.json +++ b/packages/block-library/src/query-pagination-previous/block.json @@ -12,7 +12,13 @@ "type": "string" } }, - "usesContext": [ "queryId", "query", "paginationArrow", "showLabel" ], + "usesContext": [ + "queryId", + "query", + "paginationArrow", + "showLabel", + "enhancedPagination" + ], "supports": { "reusable": false, "html": false, diff --git a/packages/block-library/src/query-pagination-previous/index.php b/packages/block-library/src/query-pagination-previous/index.php index 5665506598f81..a580880f0f04c 100644 --- a/packages/block-library/src/query-pagination-previous/index.php +++ b/packages/block-library/src/query-pagination-previous/index.php @@ -15,8 +15,9 @@ * @return string Returns the previous posts link for the query. */ function render_block_core_query_pagination_previous( $attributes, $content, $block ) { - $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; - $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; + $page_key = isset( $block->context['queryId'] ) ? 'query-' . $block->context['queryId'] . '-page' : 'query-page'; + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + $page = empty( $_GET[ $page_key ] ) ? 1 : (int) $_GET[ $page_key ]; $wrapper_attributes = get_block_wrapper_attributes(); $show_label = isset( $block->context['showLabel'] ) ? (bool) $block->context['showLabel'] : true; @@ -49,6 +50,22 @@ function render_block_core_query_pagination_previous( $attributes, $content, $bl $label ); } + + if ( $enhanced_pagination ) { + $p = new WP_HTML_Tag_Processor( $content ); + if ( $p->next_tag( + array( + 'tag_name' => 'a', + 'class_name' => 'wp-block-query-pagination-previous', + ) + ) ) { + $p->set_attribute( 'data-wp-key', 'query-pagination-previous' ); + $p->set_attribute( 'data-wp-on--click', 'actions.core.query.navigate' ); + $p->set_attribute( 'data-wp-on--mouseenter', 'actions.core.query.prefetch' ); + $content = $p->get_updated_html(); + } + } + return $content; } diff --git a/packages/block-library/src/query/block.json b/packages/block-library/src/query/block.json index e4b78b585be0e..d30eccf376579 100644 --- a/packages/block-library/src/query/block.json +++ b/packages/block-library/src/query/block.json @@ -34,17 +34,24 @@ }, "namespace": { "type": "string" + }, + "enhancedPagination": { + "type": "boolean", + "default": false } }, "providesContext": { "queryId": "queryId", "query": "query", - "displayLayout": "displayLayout" + "displayLayout": "displayLayout", + "enhancedPagination": "enhancedPagination" }, "supports": { "align": [ "wide", "full" ], "html": false, "layout": true }, - "editorStyle": "wp-block-query-editor" + "editorStyle": "wp-block-query-editor", + "style": "wp-block-query", + "viewScript": "file:./view.min.js" } diff --git a/packages/block-library/src/query/edit/inspector-controls/index.js b/packages/block-library/src/query/edit/inspector-controls/index.js index 5244a88831255..492f276ccf615 100644 --- a/packages/block-library/src/query/edit/inspector-controls/index.js +++ b/packages/block-library/src/query/edit/inspector-controls/index.js @@ -17,7 +17,8 @@ import { privateApis as blockEditorPrivateApis, } from '@wordpress/block-editor'; import { debounce } from '@wordpress/compose'; -import { useEffect, useState, useCallback } from '@wordpress/element'; +import { useEffect, useState, useCallback, useRef } from '@wordpress/element'; +import { speak } from '@wordpress/a11y'; /** * Internal dependencies @@ -40,8 +41,8 @@ import { const { BlockInfo } = unlock( blockEditorPrivateApis ); export default function QueryInspectorControls( props ) { - const { attributes, setQuery, setDisplayLayout } = props; - const { query, displayLayout } = attributes; + const { attributes, setQuery, setDisplayLayout, setAttributes } = props; + const { query, displayLayout, enhancedPagination } = attributes; const { order, orderBy, @@ -123,6 +124,18 @@ export default function QueryInspectorControls( props ) { isControlAllowed( allowedControls, 'parents' ) && isPostTypeHierarchical; + const enhancedPaginationNotice = __( + 'Enhanced Pagination might cause interactive blocks within the Post Template to stop working. Disable it if you experience any issues.' + ); + + const isFirstRender = useRef( true ); // Don't speak on first render. + useEffect( () => { + if ( ! isFirstRender.current && enhancedPagination ) { + speak( enhancedPaginationNotice ); + } + isFirstRender.current = false; + }, [ enhancedPagination, enhancedPaginationNotice ] ); + const showFiltersPanel = showTaxControl || showAuthorControl || @@ -280,6 +293,36 @@ export default function QueryInspectorControls( props ) { ) } + + + + setAttributes( { + enhancedPagination: !! value, + } ) + } + /> + { enhancedPagination && ( +
    + + { enhancedPaginationNotice } + +
    + ) } +
    +
    ); } diff --git a/packages/block-library/src/query/edit/query-content.js b/packages/block-library/src/query/edit/query-content.js index 1d795dd646d48..89c6efa280979 100644 --- a/packages/block-library/src/query/edit/query-content.js +++ b/packages/block-library/src/query/edit/query-content.js @@ -109,6 +109,7 @@ export default function QueryContent( { attributes={ attributes } setQuery={ updateQuery } setDisplayLayout={ updateDisplayLayout } + setAttributes={ setAttributes } /> next_tag() ) { + // Add the necessary directives. + $p->set_attribute( 'data-wp-interactive', true ); + $p->set_attribute( 'data-wp-navigation-id', 'query-' . $attributes['queryId'] ); + $p->set_attribute( + 'data-wp-context', + wp_json_encode( array( 'core' => array( 'query' => (object) array() ) ) ) + ); + $content = $p->get_updated_html(); + + // Mark the block as interactive. + $block->block_type->supports['interactivity'] = true; + + // Add a div to announce messages using `aria-live`. + $last_div_position = strripos( $content, '' ); + $content = substr_replace( + $content, + '
    +
    ', + $last_div_position, + 0 + ); + + // Use state to send translated strings. + wp_store( + array( + 'state' => array( + 'core' => array( + 'query' => array( + 'loadingText' => __( 'Loading page, please wait.' ), + 'loadedText' => __( 'Page Loaded.' ), + ), + ), + ), + ) + ); + } + } + + $view_asset = 'wp-block-query-view'; + if ( ! wp_script_is( $view_asset ) ) { + $script_handles = $block->block_type->view_script_handles; + // If the script is not needed, and it is still in the `view_script_handles`, remove it. + if ( ! $attributes['enhancedPagination'] && in_array( $view_asset, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_diff( $script_handles, array( $view_asset ) ); + } + // If the script is needed, but it was previously removed, add it again. + if ( $attributes['enhancedPagination'] && ! in_array( $view_asset, $script_handles, true ) ) { + $block->block_type->view_script_handles = array_merge( $script_handles, array( $view_asset ) ); + } + } + + $style_asset = 'wp-block-query'; + if ( ! wp_style_is( $style_asset ) ) { + $style_handles = $block->block_type->style_handles; + // If the styles are not needed, and they are still in the `style_handles`, remove them. + if ( ! $attributes['enhancedPagination'] && in_array( $style_asset, $style_handles, true ) ) { + $block->block_type->style_handles = array_diff( $style_handles, array( $style_asset ) ); + } + // If the styles are needed, but they were previously removed, add them again. + if ( $attributes['enhancedPagination'] && ! in_array( $style_asset, $style_handles, true ) ) { + $block->block_type->style_handles = array_merge( $style_handles, array( $style_asset ) ); + } + } + + return $content; +} + /** * Registers the `core/query` block on the server. */ function register_block_core_query() { register_block_type_from_metadata( - __DIR__ . '/query' + __DIR__ . '/query', + array( + 'render_callback' => 'render_block_core_query', + ) ); } add_action( 'init', 'register_block_core_query' ); diff --git a/packages/block-library/src/query/style.scss b/packages/block-library/src/query/style.scss new file mode 100644 index 0000000000000..c560018056d7f --- /dev/null +++ b/packages/block-library/src/query/style.scss @@ -0,0 +1,63 @@ +.wp-block-query__enhanced-pagination-animation { + position: fixed; + top: 0; + left: 0; + margin: 0; + padding: 0; + width: 100vw; + max-width: 100vw !important; + height: 4px; + background-color: var(--wp--preset--color--primary, #000); + opacity: 0; + + &.start-animation { + animation: + wp-block-query__enhanced-pagination-start-animation + 30s + cubic-bezier(0, 1, 0, 1) + infinite; + } + + &.finish-animation { + animation: + wp-block-query__enhanced-pagination-finish-animation + 300ms + ease-in; + } +} + +@keyframes wp-block-query__enhanced-pagination-start-animation { + 0% { + transform: scaleX(0); + transform-origin: 0% 0%; + opacity: 1; + } + 100% { + transform: scaleX(1); + transform-origin: 0% 0%; + opacity: 1; + } +} + +@keyframes wp-block-query__enhanced-pagination-finish-animation { + 0% { + opacity: 1; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +.wp-block-query__enhanced-pagination-navigation-announce { + position: absolute; + clip: rect(0, 0, 0, 0); + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + border: 0; +} diff --git a/packages/block-library/src/query/view.js b/packages/block-library/src/query/view.js new file mode 100644 index 0000000000000..cbd5573e05c6f --- /dev/null +++ b/packages/block-library/src/query/view.js @@ -0,0 +1,82 @@ +/** + * WordPress dependencies + */ +import { store, navigate, prefetch } from '@wordpress/interactivity'; + +const isValidLink = ( ref ) => + ref && + ref instanceof window.HTMLAnchorElement && + ref.href && + ( ! ref.target || ref.target === '_self' ) && + ref.origin === window.location.origin; + +const isValidEvent = ( event ) => + event.button === 0 && // left clicks only + ! event.metaKey && // open in new tab (mac) + ! event.ctrlKey && // open in new tab (windows) + ! event.altKey && // download + ! event.shiftKey && + ! event.defaultPrevented; + +store( { + selectors: { + core: { + query: { + startAnimation: ( { context } ) => + context.core.query.animation === 'start', + finishAnimation: ( { context } ) => + context.core.query.animation === 'finish', + }, + }, + }, + actions: { + core: { + query: { + navigate: async ( { event, ref, context, state } ) => { + if ( isValidLink( ref ) && isValidEvent( event ) ) { + event.preventDefault(); + + const id = ref.closest( '[data-wp-navigation-id]' ) + .dataset.wpNavigationId; + + // Don't announce the navigation immediately, wait 300 ms. + const timeout = setTimeout( () => { + context.core.query.message = + state.core.query.loadingText; + context.core.query.animation = 'start'; + }, 300 ); + + await navigate( ref.href ); + + // Dismiss loading message if it hasn't been added yet. + clearTimeout( timeout ); + + // Announce that the page has been loaded. If the message is the + // same, we use a no-break space similar to the @wordpress/a11y + // package: https://github.com/WordPress/gutenberg/blob/c395242b8e6ee20f8b06c199e4fc2920d7018af1/packages/a11y/src/filter-message.js#L20-L26 + context.core.query.message = + state.core.query.loadedText + + ( context.core.query.message === + state.core.query.loadedText + ? '\u00A0' + : '' ); + + context.core.query.animation = 'finish'; + + // Focus the first anchor of the Query block. + document + .querySelector( + `[data-wp-navigation-id=${ id }] a[href]` + ) + ?.focus(); + } + }, + prefetch: async ( { ref } ) => { + if ( isValidLink( ref ) ) { + await prefetch( ref.href ); + } + }, + }, + }, + }, +} ); diff --git a/test/integration/fixtures/blocks/core__query.json b/test/integration/fixtures/blocks/core__query.json index fb545c16ea695..b050aaa2b5b1f 100644 --- a/test/integration/fixtures/blocks/core__query.json +++ b/test/integration/fixtures/blocks/core__query.json @@ -18,7 +18,8 @@ "taxQuery": null, "parents": [] }, - "tagName": "div" + "tagName": "div", + "enhancedPagination": false }, "innerBlocks": [] }