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": []
}