From def5f01f63b90d03e3bde48dc7b1a0794f9f9a05 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 13 Nov 2024 16:25:51 -0500 Subject: [PATCH 01/42] add the filter and test --- lib/interactivity-api.php | 21 + .../interactivity/instant-search.spec.ts | 469 ++++++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 test/e2e/specs/interactivity/instant-search.spec.ts diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index ff68936f054a7e..9483c4894b8782 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -31,3 +31,24 @@ function ( $data ) { } } add_action( 'after_setup_theme', 'gutenberg_register_interactivity_script_module_data_hooks', 20 ); + +function gutenberg_block_core_query_add_url_filtering( $context ) { + if ( empty( $context['queryId'] ) ) { + return $context; + } + + // if the parent block is NOT a Query Loop block, then we bail out + if ( empty( $context['query']['inherit'] ) ) { + return $context; + } + + $search_key = 'query-' . $context['queryId'] . '-s'; + if ( ! isset( $_GET[ $search_key ] ) ) { + return $context; + } + + $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); + + return $context; +} +add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering' ); diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts new file mode 100644 index 00000000000000..9a1f6cba317b1a --- /dev/null +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -0,0 +1,469 @@ +/** + * Internal dependencies + */ +import { test, expect } from './fixtures'; +/** + * External dependencies + */ +import type { Page } from '@playwright/test'; + +/** + * Go to the next page of the query. + * @param page - The page object. + * @param pageNumber - The page number to navigate to. + * @param args - Query arguments: ['default'] or ['custom', number] + */ +async function goToNextPage( + page: Page, + pageNumber: number, + ...args: [ 'default' ] | [ 'custom', number ] +) { + const [ queryType, queryId ] = args; + await page + .getByTestId( `${ queryType }-query` ) + .getByRole( 'link', { name: 'Next Page' } ) + .click(); + + // Wait for the response + return page.waitForResponse( ( response ) => + queryType === 'default' + ? response.url().includes( `paged=${ pageNumber }` ) || + response.url().includes( `/page/${ pageNumber }/` ) + : response + .url() + .includes( `query-${ queryId }-page=${ pageNumber }` ) + ); +} + +test.describe( 'Instant Search', () => { + test.beforeAll( async ( { requestUtils } ) => { + await requestUtils.activateTheme( 'emptytheme' ); + await requestUtils.setGutenbergExperiments( [ + 'gutenberg-search-query-block', + ] ); + await requestUtils.deleteAllPosts(); + + // Create test posts + // Make sure to create them last-to-first to avoid flakiness + await requestUtils.createPost( { + title: 'Unique Post', + content: 'This post has unique content.', + status: 'publish', + date_gmt: new Date( + new Date().getTime() - 1000 * 60 * 60 * 24 * 5 + ).toISOString(), + } ); + await requestUtils.createPost( { + title: 'Fourth Test Post', + content: 'This is the fourth test post content.', + status: 'publish', + date_gmt: new Date( + new Date().getTime() - 1000 * 60 * 60 * 24 * 4 + ).toISOString(), + } ); + await requestUtils.createPost( { + title: 'Third Test Post', + content: 'This is the third test post content.', + status: 'publish', + date_gmt: new Date( + new Date().getTime() - 1000 * 60 * 60 * 24 * 3 + ).toISOString(), + } ); + await requestUtils.createPost( { + title: 'Second Test Post', + content: 'This is the second test post content.', + status: 'publish', + date_gmt: new Date( + new Date().getTime() - 1000 * 60 * 60 * 24 * 2 + ).toISOString(), + } ); + await requestUtils.createPost( { + title: 'First Test Post', + content: 'This is the first test post content.', + status: 'publish', + date_gmt: new Date( + new Date().getTime() - 1000 * 60 * 60 * 24 * 1 + ).toISOString(), + } ); + + // Set the Blog pages show at most 2 posts + await requestUtils.updateSiteSettings( { + posts_per_page: 2, + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + await requestUtils.deleteAllPages(); + await requestUtils.deleteAllTemplates( 'wp_template' ); + + // Reset the Blog pages show at most 10 posts + await requestUtils.updateSiteSettings( { + posts_per_page: 10, + } ); + } ); + + test.describe( 'Custom Query', () => { + let pageId: number; + + const queryId = 123; + + test.beforeAll( async ( { requestUtils } ) => { + // Create page with custom query + const { id } = await requestUtils.createPage( { + status: 'publish', + date_gmt: new Date().toISOString(), + title: 'Custom Query', + content: ` + +
+ + + + + + + + + + + + +

No results found.

+ + +
+`, + } ); + + pageId = id; + } ); + + test.beforeEach( async ( { page } ) => { + await page.goto( `/?p=${ pageId }` ); + } ); + + test( 'should update search results without page reload', async ( { + page, + } ) => { + // Check that the first post is shown initially + await expect( + page.getByText( 'First Test Post', { exact: true } ) + ).toBeVisible(); + + // Type in search input and verify results update + await page.locator( 'input[type="search"]' ).fill( 'Unique' ); + await page.waitForResponse( ( response ) => + response.url().includes( `instant-search-${ queryId }=Unique` ) + ); + + // Verify only the unique post is shown + await expect( + page.getByText( 'Unique Post', { exact: true } ) + ).toBeVisible(); + + // Check that there is only one post + const posts = page + .getByTestId( 'custom-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( posts ).toHaveCount( 1 ); + + // Verify that the other posts are hidden + await expect( + page.getByText( 'First Test Post', { exact: true } ) + ).toBeHidden(); + } ); + + test( 'should update URL with search parameter', async ( { page } ) => { + // Test global query search parameter + await page.locator( 'input[type="search"]' ).fill( 'Test' ); + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ queryId }=Test` ) + ); + + // Clear search and verify parameter is removed + await page.locator( 'input[type="search"]' ).fill( '' ); + await expect( page ).not.toHaveURL( + new RegExp( `instant-search-${ queryId }=` ) + ); + } ); + + test( 'should handle search debouncing', async ( { page } ) => { + let responseCount = 0; + + // Monitor the number of requests + page.on( 'response', ( res ) => { + if ( res.url().includes( `instant-search-${ queryId }=` ) ) { + responseCount++; + } + } ); + + // Type quickly and wait for the response + let responsePromise = page.waitForResponse( ( response ) => { + return ( + response + .url() + .includes( `instant-search-${ queryId }=Test` ) && + response.status() === 200 + ); + } ); + await page + .locator( 'input[type="search"]' ) + .pressSequentially( 'Test', { delay: 100 } ); + await responsePromise; + + // Check that only one request was made + expect( responseCount ).toBe( 1 ); + + // Verify URL is updated after debounce + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ queryId }=Test` ) + ); + + responsePromise = page.waitForResponse( ( response ) => { + return response + .url() + .includes( `instant-search-${ queryId }=Test1234` ); + } ); + // Type again with a large delay and verify that a request is made + // for each character + await page + .locator( 'input[type="search"]' ) + .pressSequentially( '1234', { delay: 500 } ); + await responsePromise; + + // Check that five requests were made (Test, Test1, Test12, Test123, Test1234) + expect( responseCount ).toBe( 5 ); + } ); + + test( 'should reset pagination when searching', async ( { page } ) => { + // Navigate to second page + await page.click( 'a.wp-block-query-pagination-next' ); + + await expect( page ).toHaveURL( + new RegExp( `query-${ queryId }-page=2` ) + ); + + // Search and verify we're back to first page + await page.locator( 'input[type="search"]' ).fill( 'Test' ); + await expect( page ).not.toHaveURL( + new RegExp( `query-${ queryId }-page=2` ) + ); + + // The url should now contain `?paged=1` because we're on the first page + // We cannot remove the `paged` param completely because the pathname + // might contain the `/page/2` suffix so we need to set `paged` to `1` to + // override it. + await expect( page ).toHaveURL( + new RegExp( `query-${ queryId }-page=1` ) + ); + } ); + + test( 'should show no-results block when search has no matches', async ( { + page, + } ) => { + await page + .locator( 'input[type="search"]' ) + .fill( 'NonexistentContent' ); + await page.waitForResponse( ( response ) => + response + .url() + .includes( + `instant-search-${ queryId }=NonexistentContent` + ) + ); + + // Verify no-results block is shown + await expect( page.getByText( 'No results found.' ) ).toBeVisible(); + } ); + + test( 'should update pagination numbers based on search results', async ( { + page, + } ) => { + // Initially should show pagination numbers for 3 pages + await expect( + page.locator( '.wp-block-query-pagination-numbers' ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: '2' } ) + ).toBeVisible(); + await expect( + page.getByRole( 'link', { name: '3' } ) + ).toBeVisible(); + + // Search for unique post + await page.locator( 'input[type="search"]' ).fill( 'Unique' ); + await page.waitForResponse( ( response ) => + response.url().includes( `instant-search-${ queryId }=Unique` ) + ); + + // Pagination numbers should not be visible with single result + await expect( + page.locator( '.wp-block-query-pagination-numbers' ) + ).toBeHidden(); + } ); + } ); + + test.describe( 'Multiple Queries', () => { + const customQueryId = 1234; + + test.beforeAll( async ( { requestUtils } ) => { + // Edit the Home template to include both query types + await requestUtils.deleteAllTemplates( 'wp_template' ); + await requestUtils.createTemplate( 'wp_template', { + slug: 'home', + title: 'Home', + content: ` + +
+ +

Default Query

+ + + + + + + + + + + + + +

No results found.

+ + +
+ + + +
+ +

Custom Query

+ + + + + + + + + + + + + +

No results found.

+ + +
+`, + } ); + } ); + + test.beforeEach( async ( { page } ) => { + await page.goto( '/' ); + } ); + + test( 'should handle searches independently', async ( { page } ) => { + // Get search inputs + const defaultQuerySearch = page.getByLabel( + 'default-instant-search' + ); + + const customQuerySearch = page.getByLabel( + 'custom-instant-search' + ); + + // Search in default query + await defaultQuerySearch.fill( 'Unique' ); + await page.waitForResponse( ( response ) => + response.url().includes( 'instant-search=Unique' ) + ); + + // Verify only default query ONLY shows the unique post + await expect( + page + .getByTestId( 'default-query' ) + .getByText( 'Unique Post', { exact: true } ) + ).toBeVisible(); + + // Verify that the custom query shows exactly 2 posts: First Test Post and Second Test Post + const customQuery = page.getByTestId( 'custom-query' ); + const posts = customQuery.getByRole( 'heading', { level: 3 } ); + await expect( posts ).toHaveCount( 2 ); + await expect( posts ).toContainText( [ + 'First Test Post', + 'Second Test Post', + ] ); + + // Search in custom query + await customQuerySearch.fill( 'Third' ); + await page.waitForResponse( ( response ) => + response + .url() + .includes( `instant-search-${ customQueryId }=Third` ) + ); + + // Verify URL contains both search parameters + await expect( page ).toHaveURL( /instant-search=Unique/ ); + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ customQueryId }=Third` ) + ); + + // Clear default query search + await defaultQuerySearch.fill( '' ); + await expect( page ).not.toHaveURL( /instant-search=/ ); + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ customQueryId }=Third` ) + ); + + // Clear custom query search + await customQuerySearch.fill( '' ); + await expect( page ).not.toHaveURL( + new RegExp( `instant-search-${ customQueryId }=` ) + ); + } ); + + test( 'should handle pagination independently', async ( { page } ) => { + const defaultQuerySearch = page.getByLabel( + 'default-instant-search' + ); + const customQuerySearch = page.getByLabel( + 'custom-instant-search' + ); + + // Navigate to second page in default query + await goToNextPage( page, 2, 'default' ); + + // Navigate to second page in custom query + await goToNextPage( page, 2, 'custom', customQueryId ); + + // Navigate to third page in custom query + await goToNextPage( page, 3, 'custom', customQueryId ); + + // Verify URL contains both pagination parameters + await expect( page ).toHaveURL( /(?:paged=2|\/page\/2\/)/ ); + await expect( page ).toHaveURL( + new RegExp( `query-${ customQueryId }-page=3` ) + ); + + // Search in default query and verify only its pagination resets + await defaultQuerySearch.fill( 'Test' ); + await expect( page ).toHaveURL( /paged=1/ ); + await expect( page ).toHaveURL( + new RegExp( `query-${ customQueryId }-page=3` ) + ); + + // Verify that the + + // Search in custom query and verify only its pagination resets + await customQuerySearch.fill( 'Test' ); + await expect( page ).toHaveURL( /paged=1/ ); + await expect( page ).toHaveURL( + new RegExp( `query-${ customQueryId }-page=1` ) + ); + } ); + } ); +} ); From 2886139615d60caba843cc9c08d3e5d8a8b01ade Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 14 Nov 2024 09:09:38 -0500 Subject: [PATCH 02/42] remove --- lib/interactivity-api.php | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/lib/interactivity-api.php b/lib/interactivity-api.php index 9483c4894b8782..ff68936f054a7e 100644 --- a/lib/interactivity-api.php +++ b/lib/interactivity-api.php @@ -31,24 +31,3 @@ function ( $data ) { } } add_action( 'after_setup_theme', 'gutenberg_register_interactivity_script_module_data_hooks', 20 ); - -function gutenberg_block_core_query_add_url_filtering( $context ) { - if ( empty( $context['queryId'] ) ) { - return $context; - } - - // if the parent block is NOT a Query Loop block, then we bail out - if ( empty( $context['query']['inherit'] ) ) { - return $context; - } - - $search_key = 'query-' . $context['queryId'] . '-s'; - if ( ! isset( $_GET[ $search_key ] ) ) { - return $context; - } - - $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); - - return $context; -} -add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering' ); From c8b390b047918c6e48febdaa116991bd130ab976 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 14 Nov 2024 11:47:57 -0500 Subject: [PATCH 03/42] Create instant search using `render_block_context` filter. --- lib/blocks.php | 29 +++++++ lib/experiments-page.php | 12 +++ packages/block-library/src/search/block.json | 1 + packages/block-library/src/search/index.php | 61 +++++++++++--- packages/block-library/src/search/view.js | 85 +++++++++++++++++++- 5 files changed, 174 insertions(+), 14 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index c3fdb26700c58c..0d485b74c1503e 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -509,3 +509,32 @@ function _gutenberg_footnotes_force_filtered_html_on_import_filter( $arg ) { add_action( 'init', '_gutenberg_footnotes_kses_init' ); add_action( 'set_current_user', '_gutenberg_footnotes_kses_init' ); add_filter( 'force_filtered_html_on_import', '_gutenberg_footnotes_force_filtered_html_on_import_filter', 999 ); + + +function gutenberg_block_core_query_add_url_filtering( $context ) { + + // Make sure it only runs for blocks with a queryId + if ( empty( $context['queryId'] ) ) { + return $context; + } + + // Check if the instant search gutenberg experiment is enabled + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + if ( ! $instant_search_enabled ) { + return $context; + } + + // Get the search key from the URL + $search_key = 'instant-search-' . $context['queryId']; + if ( ! isset( $_GET[ $search_key ] ) ) { + return $context; + } + + // Add the search query to the context, it will be picked up by all the blocks that + // use the `query` context like `post-template` or `query-pagination`. + $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); + + return $context; +} +add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); diff --git a/lib/experiments-page.php b/lib/experiments-page.php index 256a185a3af692..3c78a9246295af 100644 --- a/lib/experiments-page.php +++ b/lib/experiments-page.php @@ -199,6 +199,18 @@ function gutenberg_initialize_experiments_settings() { ) ); + add_settings_field( + 'gutenberg-search-query-block', + __( 'Instant Search and Query Block', 'gutenberg' ), + 'gutenberg_display_experiment_field', + 'gutenberg-experiments', + 'gutenberg_experiments_section', + array( + 'label' => __( 'Enable instant search functionality of the Search + Query blocks.', 'gutenberg' ), + 'id' => 'gutenberg-search-query-block', + ) + ); + register_setting( 'gutenberg-experiments', 'gutenberg-experiments' diff --git a/packages/block-library/src/search/block.json b/packages/block-library/src/search/block.json index c5af5a29d21beb..e7207498125d6e 100644 --- a/packages/block-library/src/search/block.json +++ b/packages/block-library/src/search/block.json @@ -48,6 +48,7 @@ "default": false } }, + "usesContext": [ "enhancedPagination", "query", "queryId" ], "supports": { "align": [ "left", "center", "right" ], "color": { diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 87e12f5d33d079..efb6e1bac4a37d 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -16,7 +16,7 @@ * * @return string The search block markup. */ -function render_block_core_search( $attributes ) { +function render_block_core_search( $attributes, $content, $block ) { // Older versions of the Search block defaulted the label and buttonText // attributes to `__( 'Search' )` meaning that many posts contain ``. Support these by defaulting an undefined label and @@ -31,8 +31,8 @@ function render_block_core_search( $attributes ) { $input_id = wp_unique_id( 'wp-block-search__input-' ); $classnames = classnames_for_block_core_search( $attributes ); - $show_label = ! empty( $attributes['showLabel'] ); - $use_icon_button = ! empty( $attributes['buttonUseIcon'] ); + $show_label = ( ! empty( $attributes['showLabel'] ) ) ? true : false; + $use_icon_button = ( ! empty( $attributes['buttonUseIcon'] ) ) ? true : false; $show_button = ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) ? false : true; $button_position = $show_button ? $attributes['buttonPosition'] : null; $query_params = ( ! empty( $attributes['query'] ) ) ? $attributes['query'] : array(); @@ -48,6 +48,12 @@ function render_block_core_search( $attributes ) { // This variable is a constant and its value is always false at this moment. // It is defined this way because some values depend on it, in case it changes in the future. $open_by_default = false; + // Check if the block is using the enhanced pagination. + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + + // Check if the block is using the instant search experiment. + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); $label_inner_html = empty( $attributes['label'] ) ? __( 'Search' ) : wp_kses_post( $attributes['label'] ); $label = new WP_HTML_Tag_Processor( sprintf( '', $inline_styles['label'], $label_inner_html ) ); @@ -90,6 +96,16 @@ function render_block_core_search( $attributes ) { $input->set_attribute( 'aria-hidden', 'true' ); $input->set_attribute( 'tabindex', '-1' ); } + + // Instant search is only available when using the enhanced pagination. + if ( $enhanced_pagination ) { + wp_enqueue_script_module( '@wordpress/block-library/search/view' ); + + if ( $instant_search_enabled ) { + $input->set_attribute( 'data-wp-bind--value', 'context.search' ); + $input->set_attribute( 'data-wp-on-async--input', 'actions.updateSearch' ); + } + } } if ( count( $query_params ) > 0 ) { @@ -163,18 +179,21 @@ function render_block_core_search( $attributes ) { array( 'class' => $classnames ) ); $form_directives = ''; + $form_context = array(); // If it's interactive, add the directives. + if ( $is_expandable_searchfield || ( $enhanced_pagination && $instant_search_enabled ) ) { + $form_directives = 'data-wp-interactive="core/search"'; + } + if ( $is_expandable_searchfield ) { $aria_label_expanded = __( 'Submit Search' ); $aria_label_collapsed = __( 'Expand search field' ); - $form_context = wp_interactivity_data_wp_context( - array( - 'isSearchInputVisible' => $open_by_default, - 'inputId' => $input_id, - 'ariaLabelExpanded' => $aria_label_expanded, - 'ariaLabelCollapsed' => $aria_label_collapsed, - ) + $form_context = array( + 'isSearchInputInitiallyVisible' => $open_by_default, + 'inputId' => $input_id, + 'ariaLabelExpanded' => $aria_label_expanded, + 'ariaLabelCollapsed' => $aria_label_collapsed, ); $form_directives = ' data-wp-interactive="core/search" @@ -185,6 +204,28 @@ function render_block_core_search( $attributes ) { '; } + if ( $enhanced_pagination && $instant_search_enabled ) { + $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); + $search = ''; + + if ( $is_inherited ) { + $search = empty( $_GET['instant-search'] ) ? '' : sanitize_text_field( $_GET['instant-search'] ); + } else { + $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); + } + + $form_context = array_merge( + $form_context, + array( + 'search' => $search, + 'isInherited' => $is_inherited, + 'queryId' => $block->context['queryId'], + ) + ); + } + + $form_directives .= wp_interactivity_data_wp_context( $form_context ); + return sprintf( '
%4s
', esc_url( home_url( '/' ) ), diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 0e4c462a2e3213..b3494c6f1a8364 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -3,7 +3,10 @@ */ import { store, getContext, getElement } from '@wordpress/interactivity'; -const { actions } = store( +/** @type {( () => void ) | null} */ +let supersedePreviousSearch = null; + +const { state, actions } = store( 'core/search', { state: { @@ -29,14 +32,25 @@ const { actions } = store( const { isSearchInputVisible } = getContext(); return isSearchInputVisible ? '0' : '-1'; }, + get isSearchInputVisible() { + const ctx = getContext(); + + // `ctx.isSearchInputVisible` is a client-side-only context value, so + // if it's not set, it means that it's an initial page load, so we need + // to return the value of `ctx.isSearchInputInitiallyVisible`. + if ( typeof ctx.isSearchInputVisible === 'undefined' ) { + return ctx.isSearchInputInitiallyVisible; + } + return ctx.isSearchInputVisible; + }, }, actions: { openSearchInput( event ) { - const ctx = getContext(); - const { ref } = getElement(); - if ( ! ctx.isSearchInputVisible ) { + if ( ! state.isSearchInputVisible ) { event.preventDefault(); + const ctx = getContext(); ctx.isSearchInputVisible = true; + const { ref } = getElement(); ref.parentElement.querySelector( 'input' ).focus(); } }, @@ -66,6 +80,69 @@ const { actions } = store( actions.closeSearchInput(); } }, + *updateSearch( e ) { + const { value } = e.target; + + const ctx = getContext(); + + // Don't navigate if the search didn't really change. + if ( value === ctx.search ) { + return; + } + + ctx.search = value; + + // Debounce the search by 300ms to prevent multiple navigations. + supersedePreviousSearch?.(); + const { promise, resolve, reject } = Promise.withResolvers(); + const timeout = setTimeout( resolve, 300 ); + supersedePreviousSearch = () => { + clearTimeout( timeout ); + reject(); + }; + try { + yield promise; + } catch { + return; + } + + const url = new URL( window.location.href ); + + if ( value ) { + if ( ctx.isInherited ) { + url.searchParams.set( 'instant-search', value ); + + // Make sure we reset the pagination. + url.searchParams.set( 'paged', '1' ); + } else { + // Set the instant-search parameter using the query ID and search value + const queryId = ctx.queryId; + url.searchParams.set( + `instant-search-${ queryId }`, + value + ); + + // Make sure we reset the pagination. + url.searchParams.set( `query-${ queryId }-page`, '1' ); + } + } else if ( ctx.isInherited ) { + // Reset global search for inherited queries + url.searchParams.delete( 'instant-search' ); + url.searchParams.delete( 'paged' ); + } else { + // Reset specific search for non-inherited queries + url.searchParams.delete( + `instant-search-${ ctx.queryId }` + ); + url.searchParams.delete( `query-${ ctx.queryId }-page` ); + } + + const { actions: routerActions } = yield import( + '@wordpress/interactivity-router' + ); + + routerActions.navigate( url.href ); + }, }, }, { lock: true } From 00d90f32b30ae743bf35be83e3fb0c5ca0cb4dae Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 14 Nov 2024 18:02:29 -0500 Subject: [PATCH 04/42] Remove handling of inheritd query from `search/index.php` --- packages/block-library/src/search/index.php | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index efb6e1bac4a37d..468ff89b7ffc75 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -205,21 +205,14 @@ function render_block_core_search( $attributes, $content, $block ) { } if ( $enhanced_pagination && $instant_search_enabled ) { - $is_inherited = isset( $block->context['query']['inherit'] ) && $block->context['query']['inherit'] && ! empty( $block->context['queryId'] ); - $search = ''; - if ( $is_inherited ) { - $search = empty( $_GET['instant-search'] ) ? '' : sanitize_text_field( $_GET['instant-search'] ); - } else { - $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); - } + $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); $form_context = array_merge( $form_context, array( - 'search' => $search, - 'isInherited' => $is_inherited, - 'queryId' => $block->context['queryId'], + 'search' => $search, + 'queryId' => $block->context['queryId'], ) ); } From 6214a724f4116fb42281e8ddc4c31e3e1ff91aca Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 14 Nov 2024 18:49:21 -0500 Subject: [PATCH 05/42] Handle case when query is defined in block context in DB. --- packages/block-library/src/search/index.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 468ff89b7ffc75..83918cb68ae8ae 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -206,7 +206,15 @@ function render_block_core_search( $attributes, $content, $block ) { if ( $enhanced_pagination && $instant_search_enabled ) { - $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? '' : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); + $search = ''; + + // If the query is defined in the block context, use it + if ( isset( $block->context['query']['search'] ) && '' !== $block->context['query']['search'] ) { + $search = $block->context['query']['search']; + } + + // If the query is defined in the URL, it overrides the block context value if defined + $search = empty( $_GET[ 'instant-search-' . $block->context['queryId'] ] ) ? $search : sanitize_text_field( $_GET[ 'instant-search-' . $block->context['queryId'] ] ); $form_context = array_merge( $form_context, From cbb469c4bc45fc75b12d51164d6ba6651e67f7cd Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 18 Nov 2024 17:58:47 -0500 Subject: [PATCH 06/42] Guard against queryId being undefined in block context --- packages/block-library/src/search/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 83918cb68ae8ae..606a54455af54f 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -204,7 +204,7 @@ function render_block_core_search( $attributes, $content, $block ) { '; } - if ( $enhanced_pagination && $instant_search_enabled ) { + if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { $search = ''; From 571d85174a4235ff2c374cc21d01c45b6d451750 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 19 Nov 2024 18:28:20 -0500 Subject: [PATCH 07/42] Fix the e2e test suite --- .../interactivity/instant-search.spec.ts | 154 ++++++++++-------- 1 file changed, 83 insertions(+), 71 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 9a1f6cba317b1a..b766dae1b576ec 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -11,27 +11,23 @@ import type { Page } from '@playwright/test'; * Go to the next page of the query. * @param page - The page object. * @param pageNumber - The page number to navigate to. - * @param args - Query arguments: ['default'] or ['custom', number] + * @param testId - The test ID of the query. + * @param queryId - The query ID. */ async function goToNextPage( page: Page, pageNumber: number, - ...args: [ 'default' ] | [ 'custom', number ] + testId: string, + queryId: number ) { - const [ queryType, queryId ] = args; await page - .getByTestId( `${ queryType }-query` ) + .getByTestId( testId ) .getByRole( 'link', { name: 'Next Page' } ) .click(); // Wait for the response return page.waitForResponse( ( response ) => - queryType === 'default' - ? response.url().includes( `paged=${ pageNumber }` ) || - response.url().includes( `/page/${ pageNumber }/` ) - : response - .url() - .includes( `query-${ queryId }-page=${ pageNumber }` ) + response.url().includes( `query-${ queryId }-page=${ pageNumber }` ) ); } @@ -305,21 +301,22 @@ test.describe( 'Instant Search', () => { } ); test.describe( 'Multiple Queries', () => { - const customQueryId = 1234; + const firstQueryId = 1234; + const secondQueryId = 5678; test.beforeAll( async ( { requestUtils } ) => { - // Edit the Home template to include both query types + // Edit the Home template to include two custom queries await requestUtils.deleteAllTemplates( 'wp_template' ); await requestUtils.createTemplate( 'wp_template', { slug: 'home', title: 'Home', content: ` - -
+ +
-

Default Query

+

First Query

- + @@ -337,12 +334,12 @@ test.describe( 'Instant Search', () => {
- -
+ +
-

Custom Query

+

Second Query

- + @@ -368,101 +365,116 @@ test.describe( 'Instant Search', () => { test( 'should handle searches independently', async ( { page } ) => { // Get search inputs - const defaultQuerySearch = page.getByLabel( - 'default-instant-search' - ); - - const customQuerySearch = page.getByLabel( - 'custom-instant-search' - ); + const firstQuerySearch = page.getByLabel( '1st-instant-search' ); + const secondQuerySearch = page.getByLabel( '2nd-instant-search' ); - // Search in default query - await defaultQuerySearch.fill( 'Unique' ); + // Search in first query + await firstQuerySearch.fill( 'Unique' ); await page.waitForResponse( ( response ) => - response.url().includes( 'instant-search=Unique' ) + response + .url() + .includes( `instant-search-${ firstQueryId }=Unique` ) ); - // Verify only default query ONLY shows the unique post + // Verify first query ONLY shows the unique post await expect( page - .getByTestId( 'default-query' ) + .getByTestId( 'first-query' ) .getByText( 'Unique Post', { exact: true } ) ).toBeVisible(); - // Verify that the custom query shows exactly 2 posts: First Test Post and Second Test Post - const customQuery = page.getByTestId( 'custom-query' ); - const posts = customQuery.getByRole( 'heading', { level: 3 } ); + // Verify that the second query shows exactly 2 posts: First Test Post and Second Test Post + const secondQuery = page.getByTestId( 'second-query' ); + const posts = secondQuery.getByRole( 'heading', { level: 3 } ); await expect( posts ).toHaveCount( 2 ); await expect( posts ).toContainText( [ 'First Test Post', 'Second Test Post', ] ); - // Search in custom query - await customQuerySearch.fill( 'Third' ); + // Search in second query + await secondQuerySearch.fill( 'Third' ); await page.waitForResponse( ( response ) => response .url() - .includes( `instant-search-${ customQueryId }=Third` ) + .includes( `instant-search-${ secondQueryId }=Third` ) ); // Verify URL contains both search parameters - await expect( page ).toHaveURL( /instant-search=Unique/ ); await expect( page ).toHaveURL( - new RegExp( `instant-search-${ customQueryId }=Third` ) + new RegExp( `instant-search-${ firstQueryId }=Unique` ) ); + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ secondQueryId }=Third` ) + ); + + // Verify that the first query has only one post which is the "Unique" post + const firstQueryPosts = page + .getByTestId( 'first-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( firstQueryPosts ).toHaveCount( 1 ); + await expect( firstQueryPosts ).toContainText( 'Unique Post' ); + + // Verify that the second query has only one post which is the "Third Test Post" + const secondQueryPosts = page + .getByTestId( 'second-query' ) + .getByRole( 'heading', { level: 3 } ); + await expect( secondQueryPosts ).toHaveCount( 1 ); + await expect( secondQueryPosts ).toContainText( 'Third Test Post' ); - // Clear default query search - await defaultQuerySearch.fill( '' ); - await expect( page ).not.toHaveURL( /instant-search=/ ); + // Clear first query search + await firstQuerySearch.fill( '' ); + await expect( page ).not.toHaveURL( + new RegExp( `instant-search-${ firstQueryId }=` ) + ); await expect( page ).toHaveURL( - new RegExp( `instant-search-${ customQueryId }=Third` ) + new RegExp( `instant-search-${ secondQueryId }=Third` ) ); - // Clear custom query search - await customQuerySearch.fill( '' ); + // Clear second query search + await secondQuerySearch.fill( '' ); await expect( page ).not.toHaveURL( - new RegExp( `instant-search-${ customQueryId }=` ) + new RegExp( `instant-search-${ secondQueryId }=` ) ); } ); test( 'should handle pagination independently', async ( { page } ) => { - const defaultQuerySearch = page.getByLabel( - 'default-instant-search' - ); - const customQuerySearch = page.getByLabel( - 'custom-instant-search' - ); + const firstQuerySearch = page.getByLabel( '1st-instant-search' ); + const secondQuerySearch = page.getByLabel( '2nd-instant-search' ); - // Navigate to second page in default query - await goToNextPage( page, 2, 'default' ); + // Navigate to second page in first query + await goToNextPage( page, 2, 'first-query', firstQueryId ); - // Navigate to second page in custom query - await goToNextPage( page, 2, 'custom', customQueryId ); + // Navigate to second page in second query + await goToNextPage( page, 2, 'second-query', secondQueryId ); - // Navigate to third page in custom query - await goToNextPage( page, 3, 'custom', customQueryId ); + // Navigate to third page in second query + await goToNextPage( page, 3, 'second-query', secondQueryId ); // Verify URL contains both pagination parameters - await expect( page ).toHaveURL( /(?:paged=2|\/page\/2\/)/ ); await expect( page ).toHaveURL( - new RegExp( `query-${ customQueryId }-page=3` ) + new RegExp( `query-${ firstQueryId }-page=2` ) ); - - // Search in default query and verify only its pagination resets - await defaultQuerySearch.fill( 'Test' ); - await expect( page ).toHaveURL( /paged=1/ ); await expect( page ).toHaveURL( - new RegExp( `query-${ customQueryId }-page=3` ) + new RegExp( `query-${ secondQueryId }-page=3` ) ); - // Verify that the + // Search in first query and verify only its pagination resets + await firstQuerySearch.fill( 'Test' ); + await expect( page ).toHaveURL( + new RegExp( `query-${ firstQueryId }-page=1` ) + ); + await expect( page ).toHaveURL( + new RegExp( `query-${ secondQueryId }-page=3` ) + ); - // Search in custom query and verify only its pagination resets - await customQuerySearch.fill( 'Test' ); - await expect( page ).toHaveURL( /paged=1/ ); + // Search in second query and verify only its pagination resets + await secondQuerySearch.fill( 'Test' ); + await expect( page ).toHaveURL( + new RegExp( `query-${ firstQueryId }-page=1` ) + ); await expect( page ).toHaveURL( - new RegExp( `query-${ customQueryId }-page=1` ) + new RegExp( `query-${ secondQueryId }-page=1` ) ); } ); } ); From f922695a87cbff4c0bd607330480904f76012569 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 20 Nov 2024 12:54:38 -0500 Subject: [PATCH 08/42] Move the filter to `/experimental` folder. --- lib/blocks.php | 29 ----------------------------- lib/experimental/blocks.php | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+), 29 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index 0d485b74c1503e..c3fdb26700c58c 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -509,32 +509,3 @@ function _gutenberg_footnotes_force_filtered_html_on_import_filter( $arg ) { add_action( 'init', '_gutenberg_footnotes_kses_init' ); add_action( 'set_current_user', '_gutenberg_footnotes_kses_init' ); add_filter( 'force_filtered_html_on_import', '_gutenberg_footnotes_force_filtered_html_on_import_filter', 999 ); - - -function gutenberg_block_core_query_add_url_filtering( $context ) { - - // Make sure it only runs for blocks with a queryId - if ( empty( $context['queryId'] ) ) { - return $context; - } - - // Check if the instant search gutenberg experiment is enabled - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); - if ( ! $instant_search_enabled ) { - return $context; - } - - // Get the search key from the URL - $search_key = 'instant-search-' . $context['queryId']; - if ( ! isset( $_GET[ $search_key ] ) ) { - return $context; - } - - // Add the search query to the context, it will be picked up by all the blocks that - // use the `query` context like `post-template` or `query-pagination`. - $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); - - return $context; -} -add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 68113276ec1c06..3bf7a53f1c2de7 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -117,3 +117,37 @@ function gutenberg_register_block_style( $block_name, $style_properties ) { return $result; } + +/** + * Adds the search query to the context if the instant search gutenberg experiment is enabled. + * + * @param array $context The block context. + * @return array The block context. + */ +function gutenberg_block_core_query_add_url_filtering( $context ) { + + // Make sure it only runs for blocks with a queryId + if ( empty( $context['queryId'] ) ) { + return $context; + } + + // Check if the instant search gutenberg experiment is enabled + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + if ( ! $instant_search_enabled ) { + return $context; + } + + // Get the search key from the URL + $search_key = 'instant-search-' . $context['queryId']; + if ( ! isset( $_GET[ $search_key ] ) ) { + return $context; + } + + // Add the search query to the context, it will be picked up by all the blocks that + // use the `query` context like `post-template` or `query-pagination`. + $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); + + return $context; +} +add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); From c1eb9dea68e4ab9d783f9ab4e071596fa597bea9 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 20 Nov 2024 16:30:56 -0500 Subject: [PATCH 09/42] Add an e2e test case if query.search attribute is present --- .../interactivity/instant-search.spec.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index b766dae1b576ec..8b83d4ca0c869c 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -298,6 +298,75 @@ test.describe( 'Instant Search', () => { page.locator( '.wp-block-query-pagination-numbers' ) ).toBeHidden(); } ); + + test( 'should handle pre-defined search from query attributes', async ( { + requestUtils, + page, + } ) => { + // Create page with custom query that includes a search parameter + const { id } = await requestUtils.createPage( { + status: 'publish', + title: 'Query with Search', + content: ` + +
+ + + + + + + + + + + + +

No results found.

+ + +
+`, + } ); + + // Navigate to the page + await page.goto( `/?p=${ id }` ); + + // Verify the search input has the initial value + await expect( page.locator( 'input[type="search"]' ) ).toHaveValue( + 'Unique' + ); + + // Verify only the unique post is shown + await expect( + page.getByText( 'Unique Post', { exact: true } ) + ).toBeVisible(); + const posts = page + .getByTestId( 'query-with-search' ) + .getByRole( 'heading', { level: 3 } ); + await expect( posts ).toHaveCount( 1 ); + + // Verify URL does not contain the instant-search parameter + await expect( page ).not.toHaveURL( + new RegExp( `instant-search-${ queryId }=` ) + ); + + // Type new search term and verify normal instant search behavior + await page.locator( 'input[type="search"]' ).fill( 'Test' ); + await page.waitForResponse( ( response ) => + response.url().includes( `instant-search-${ queryId }=Test` ) + ); + + // Verify URL now contains the instant-search parameter + await expect( page ).toHaveURL( + new RegExp( `instant-search-${ queryId }=Test` ) + ); + + // Verify search results update + await expect( + page.getByText( 'First Test Post', { exact: true } ) + ).toBeVisible(); + } ); } ); test.describe( 'Multiple Queries', () => { From 970908f9464d10d50a88a9164a6abadfa65e545a Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 20 Nov 2024 17:44:31 -0500 Subject: [PATCH 10/42] Remove the search button when instant search is enabled --- packages/block-library/src/search/edit.js | 71 +++++++++++++-------- packages/block-library/src/search/index.php | 15 +++-- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index f193c04e2493aa..fd039a4212b2d4 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -67,6 +67,7 @@ export default function SearchEdit( { toggleSelection, isSelected, clientId, + context, } ) { const { label, @@ -82,6 +83,16 @@ export default function SearchEdit( { style, } = attributes; + const isEnhancedPagination = context?.enhancedPagination; + + useEffect( () => { + if ( isEnhancedPagination ) { + setAttributes( { metadata: { name: 'Instant Search' } } ); + } else { + setAttributes( { metadata: { name: label } } ); + } + }, [ isEnhancedPagination, setAttributes, label ] ); + const wasJustInsertedIntoNavigationBlock = useSelect( ( select ) => { const { getBlockParentsByBlockName, wasBlockJustInserted } = @@ -385,24 +396,28 @@ export default function SearchEdit( { } } className={ showLabel ? 'is-pressed' : undefined } /> - - { ! hasNoButton && ( - { - setAttributes( { - buttonUseIcon: ! buttonUseIcon, - } ); - } } - className={ - buttonUseIcon ? 'is-pressed' : undefined - } - /> + { ! isEnhancedPagination && ( + <> + + { ! hasNoButton && ( + { + setAttributes( { + buttonUseIcon: ! buttonUseIcon, + } ); + } } + className={ + buttonUseIcon ? 'is-pressed' : undefined + } + /> + ) } + ) } @@ -596,16 +611,22 @@ export default function SearchEdit( { } } showHandle={ isSelected } > - { ( isButtonPositionInside || - isButtonPositionOutside || - hasOnlyButton ) && ( + { isEnhancedPagination ? ( + renderTextField() + ) : ( <> - { renderTextField() } - { renderButton() } + { ( isButtonPositionInside || + isButtonPositionOutside || + hasOnlyButton ) && ( + <> + { renderTextField() } + { renderButton() } + + ) } + + { hasNoButton && renderTextField() } ) } - - { hasNoButton && renderTextField() }
); diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 606a54455af54f..d2b889b133661f 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -29,11 +29,16 @@ function render_block_core_search( $attributes, $content, $block ) { ) ); - $input_id = wp_unique_id( 'wp-block-search__input-' ); - $classnames = classnames_for_block_core_search( $attributes ); - $show_label = ( ! empty( $attributes['showLabel'] ) ) ? true : false; - $use_icon_button = ( ! empty( $attributes['buttonUseIcon'] ) ) ? true : false; - $show_button = ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) ? false : true; + $input_id = wp_unique_id( 'wp-block-search__input-' ); + $classnames = classnames_for_block_core_search( $attributes ); + $show_label = ( ! empty( $attributes['showLabel'] ) ) ? true : false; + $use_icon_button = ( ! empty( $attributes['buttonUseIcon'] ) ) ? true : false; + $show_button = true; + if ( isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination'] ) { + $show_button = false; + } elseif ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) { + $show_button = false; + } $button_position = $show_button ? $attributes['buttonPosition'] : null; $query_params = ( ! empty( $attributes['query'] ) ) ? $attributes['query'] : array(); $button = ''; From 98a0e232e9c88b2b6b8fdcc9733408db77d40569 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 21 Nov 2024 12:03:15 -0500 Subject: [PATCH 11/42] Use the query_loop_block_query_vars filter --- lib/experimental/blocks.php | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index 3bf7a53f1c2de7..28203a2009850d 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -119,35 +119,34 @@ function gutenberg_register_block_style( $block_name, $style_properties ) { } /** - * Adds the search query to the context if the instant search gutenberg experiment is enabled. + * Adds the search query to Query Loop blocks if the instant search experiment is enabled. * - * @param array $context The block context. - * @return array The block context. + * @param array $query The query variables. + * @param WP_Block $block Block instance. + * @return array Modified query variables. */ -function gutenberg_block_core_query_add_url_filtering( $context ) { - - // Make sure it only runs for blocks with a queryId - if ( empty( $context['queryId'] ) ) { - return $context; - } - +function gutenberg_block_core_query_add_url_filtering( $query, $block ) { // Check if the instant search gutenberg experiment is enabled $gutenberg_experiments = get_option( 'gutenberg-experiments' ); $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); if ( ! $instant_search_enabled ) { - return $context; + return $query; + } + + // Make sure block has a queryId + if ( empty( $block->context['queryId'] ) ) { + return $query; } // Get the search key from the URL - $search_key = 'instant-search-' . $context['queryId']; + $search_key = 'instant-search-' . $block->context['queryId']; if ( ! isset( $_GET[ $search_key ] ) ) { - return $context; + return $query; } - // Add the search query to the context, it will be picked up by all the blocks that - // use the `query` context like `post-template` or `query-pagination`. - $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); + // Add the search parameter to the query + $query['s'] = sanitize_text_field( $_GET[ $search_key ] ); - return $context; + return $query; } -add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); +add_filter( 'query_loop_block_query_vars', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); From 64d68da50494a44c7b28fb9817d10128976e37af Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 21 Nov 2024 15:42:36 -0500 Subject: [PATCH 12/42] Do not delete pages and templates in e2e tests --- test/e2e/specs/interactivity/instant-search.spec.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 8b83d4ca0c869c..e1eaafd2eeb280 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -90,13 +90,7 @@ test.describe( 'Instant Search', () => { test.afterAll( async ( { requestUtils } ) => { await requestUtils.deleteAllPosts(); - await requestUtils.deleteAllPages(); - await requestUtils.deleteAllTemplates( 'wp_template' ); - - // Reset the Blog pages show at most 10 posts - await requestUtils.updateSiteSettings( { - posts_per_page: 10, - } ); + await requestUtils.activateTheme( 'twentytwentyone' ); } ); test.describe( 'Custom Query', () => { @@ -375,9 +369,8 @@ test.describe( 'Instant Search', () => { test.beforeAll( async ( { requestUtils } ) => { // Edit the Home template to include two custom queries - await requestUtils.deleteAllTemplates( 'wp_template' ); - await requestUtils.createTemplate( 'wp_template', { - slug: 'home', + await requestUtils.createPage( { + status: 'publish', title: 'Home', content: ` From 2a89b6a7349aa8c4f75fdd4408602f0fd21e8313 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 21 Nov 2024 15:45:36 -0500 Subject: [PATCH 13/42] Set the pageId for Multiple Queries tests --- test/e2e/specs/interactivity/instant-search.spec.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index e1eaafd2eeb280..19362ad27d41b6 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -364,12 +364,14 @@ test.describe( 'Instant Search', () => { } ); test.describe( 'Multiple Queries', () => { + let pageId: number; + const firstQueryId = 1234; const secondQueryId = 5678; test.beforeAll( async ( { requestUtils } ) => { // Edit the Home template to include two custom queries - await requestUtils.createPage( { + const { id } = await requestUtils.createPage( { status: 'publish', title: 'Home', content: ` @@ -419,10 +421,12 @@ test.describe( 'Instant Search', () => {
`, } ); + + pageId = id; } ); test.beforeEach( async ( { page } ) => { - await page.goto( '/' ); + await page.goto( `/?p=${ pageId }` ); } ); test( 'should handle searches independently', async ( { page } ) => { From 7cad0911cfeb070b2bf2ad32e287e0a6e33d7e53 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 25 Nov 2024 12:56:42 -0500 Subject: [PATCH 14/42] Fix the block name via metadata when Seach is instant. --- packages/block-library/src/search/edit.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index fd039a4212b2d4..69aba4416b7a93 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -87,9 +87,12 @@ export default function SearchEdit( { useEffect( () => { if ( isEnhancedPagination ) { + // Add the name to the metadata setAttributes( { metadata: { name: 'Instant Search' } } ); } else { - setAttributes( { metadata: { name: label } } ); + // Remove the name from the metadata + const { name, ...metadata } = attributes.metadata || {}; + setAttributes( { metadata } ); } }, [ isEnhancedPagination, setAttributes, label ] ); From 7e1e27b549806c8411bba9e7339eb06cc4d635df Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 26 Nov 2024 12:44:46 -0500 Subject: [PATCH 15/42] Remove stuff related to Default queries from `view.js` --- packages/block-library/src/search/view.js | 27 +++++++---------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index b3494c6f1a8364..763cabbf714efc 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -109,26 +109,15 @@ const { state, actions } = store( const url = new URL( window.location.href ); if ( value ) { - if ( ctx.isInherited ) { - url.searchParams.set( 'instant-search', value ); - - // Make sure we reset the pagination. - url.searchParams.set( 'paged', '1' ); - } else { - // Set the instant-search parameter using the query ID and search value - const queryId = ctx.queryId; - url.searchParams.set( - `instant-search-${ queryId }`, - value - ); + // Set the instant-search parameter using the query ID and search value + const queryId = ctx.queryId; + url.searchParams.set( + `instant-search-${ queryId }`, + value + ); - // Make sure we reset the pagination. - url.searchParams.set( `query-${ queryId }-page`, '1' ); - } - } else if ( ctx.isInherited ) { - // Reset global search for inherited queries - url.searchParams.delete( 'instant-search' ); - url.searchParams.delete( 'paged' ); + // Make sure we reset the pagination. + url.searchParams.set( `query-${ queryId }-page`, '1' ); } else { // Reset specific search for non-inherited queries url.searchParams.delete( From b4d16f7a3d86129e74d3559427c5bc2bbc8db0d6 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 26 Nov 2024 20:12:25 -0500 Subject: [PATCH 16/42] Add `attributes.metadata` to useEffect dependency array --- packages/block-library/src/search/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 69aba4416b7a93..cb9bcf2709484b 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -94,7 +94,7 @@ export default function SearchEdit( { const { name, ...metadata } = attributes.metadata || {}; setAttributes( { metadata } ); } - }, [ isEnhancedPagination, setAttributes, label ] ); + }, [ isEnhancedPagination, setAttributes, label, attributes.metadata ] ); const wasJustInsertedIntoNavigationBlock = useSelect( ( select ) => { From 9095031a899c57205fa530b5eae80d41facdaf0b Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Fri, 29 Nov 2024 18:36:48 +0000 Subject: [PATCH 17/42] Remove `attributes.metadata` & label from dependency array --- packages/block-library/src/search/edit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index cb9bcf2709484b..9da028f338fab3 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -94,7 +94,7 @@ export default function SearchEdit( { const { name, ...metadata } = attributes.metadata || {}; setAttributes( { metadata } ); } - }, [ isEnhancedPagination, setAttributes, label, attributes.metadata ] ); + }, [ isEnhancedPagination, setAttributes ] ); const wasJustInsertedIntoNavigationBlock = useSelect( ( select ) => { From ca253efd385b4b439dcfe2d0c8a4c40848114224 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Tue, 3 Dec 2024 17:39:50 +0000 Subject: [PATCH 18/42] Don't use `Promise.withResolvers()` in search block's view.js --- packages/block-library/src/search/view.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index 763cabbf714efc..d280d355322afa 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -94,7 +94,11 @@ const { state, actions } = store( // Debounce the search by 300ms to prevent multiple navigations. supersedePreviousSearch?.(); - const { promise, resolve, reject } = Promise.withResolvers(); + let resolve, reject; + const promise = new Promise( ( res, rej ) => { + resolve = res; + reject = rej; + } ); const timeout = setTimeout( resolve, 300 ); supersedePreviousSearch = () => { clearTimeout( timeout ); From cf27754ccf81a06071db41c580822004d33095f9 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Fri, 29 Nov 2024 19:56:34 +0000 Subject: [PATCH 19/42] Explain why we disable the react-hooks/exhaustive-deps lint --- packages/block-library/src/search/edit.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 9da028f338fab3..7165f818c61a49 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -94,6 +94,13 @@ export default function SearchEdit( { const { name, ...metadata } = attributes.metadata || {}; setAttributes( { metadata } ); } + + // We disable the exhaustive-deps warning because the effect should not depend + // on the attributes.metadata value. We only want to re-run the effect when the + // isEnhancedPagination value changes. + + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps }, [ isEnhancedPagination, setAttributes ] ); const wasJustInsertedIntoNavigationBlock = useSelect( From a5c2b17ba8ad009bc9818498c6810c3d6ed60282 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Fri, 29 Nov 2024 20:14:13 +0000 Subject: [PATCH 20/42] Undo formatting changes From 2fc61fdbcf9510befeaabd666c8396ae6938bf3a Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 14 Nov 2024 11:47:57 -0500 Subject: [PATCH 21/42] Create instant search using `render_block_context` filter. --- lib/blocks.php | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/lib/blocks.php b/lib/blocks.php index c3fdb26700c58c..0d485b74c1503e 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -509,3 +509,32 @@ function _gutenberg_footnotes_force_filtered_html_on_import_filter( $arg ) { add_action( 'init', '_gutenberg_footnotes_kses_init' ); add_action( 'set_current_user', '_gutenberg_footnotes_kses_init' ); add_filter( 'force_filtered_html_on_import', '_gutenberg_footnotes_force_filtered_html_on_import_filter', 999 ); + + +function gutenberg_block_core_query_add_url_filtering( $context ) { + + // Make sure it only runs for blocks with a queryId + if ( empty( $context['queryId'] ) ) { + return $context; + } + + // Check if the instant search gutenberg experiment is enabled + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + if ( ! $instant_search_enabled ) { + return $context; + } + + // Get the search key from the URL + $search_key = 'instant-search-' . $context['queryId']; + if ( ! isset( $_GET[ $search_key ] ) ) { + return $context; + } + + // Add the search query to the context, it will be picked up by all the blocks that + // use the `query` context like `post-template` or `query-pagination`. + $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); + + return $context; +} +add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); From 58469986d75b7712318671aaf1ecdc6e19110490 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Fri, 29 Nov 2024 20:14:13 +0000 Subject: [PATCH 22/42] Undo formatting changes From 4b2f66d2ed5f8f71c62d3d09632c95c9c8a6e4f6 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 5 Dec 2024 16:36:29 +0000 Subject: [PATCH 23/42] Remove the `render_block_context` filter from blocks.php --- lib/blocks.php | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/lib/blocks.php b/lib/blocks.php index 0d485b74c1503e..c3fdb26700c58c 100644 --- a/lib/blocks.php +++ b/lib/blocks.php @@ -509,32 +509,3 @@ function _gutenberg_footnotes_force_filtered_html_on_import_filter( $arg ) { add_action( 'init', '_gutenberg_footnotes_kses_init' ); add_action( 'set_current_user', '_gutenberg_footnotes_kses_init' ); add_filter( 'force_filtered_html_on_import', '_gutenberg_footnotes_force_filtered_html_on_import_filter', 999 ); - - -function gutenberg_block_core_query_add_url_filtering( $context ) { - - // Make sure it only runs for blocks with a queryId - if ( empty( $context['queryId'] ) ) { - return $context; - } - - // Check if the instant search gutenberg experiment is enabled - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); - if ( ! $instant_search_enabled ) { - return $context; - } - - // Get the search key from the URL - $search_key = 'instant-search-' . $context['queryId']; - if ( ! isset( $_GET[ $search_key ] ) ) { - return $context; - } - - // Add the search query to the context, it will be picked up by all the blocks that - // use the `query` context like `post-template` or `query-pagination`. - $context['query']['search'] = sanitize_text_field( $_GET[ $search_key ] ); - - return $context; -} -add_filter( 'render_block_context', 'gutenberg_block_core_query_add_url_filtering', 10, 2 ); From ee2739d41f8fa7426c21720ef57e23c600867d36 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 11 Dec 2024 13:21:53 +0000 Subject: [PATCH 24/42] Fix how directives are added to the form in the search block. --- packages/block-library/src/search/index.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index d2b889b133661f..41478cc37d3a3b 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -200,9 +200,7 @@ function render_block_core_search( $attributes, $content, $block ) { 'ariaLabelExpanded' => $aria_label_expanded, 'ariaLabelCollapsed' => $aria_label_collapsed, ); - $form_directives = ' - data-wp-interactive="core/search" - ' . $form_context . ' + $form_directives .= ' data-wp-class--wp-block-search__searchfield-hidden="!context.isSearchInputVisible" data-wp-on-async--keydown="actions.handleSearchKeydown" data-wp-on-async--focusout="actions.handleSearchFocusout" From 806c3303581d77c80ad9496a01004288bcf661f1 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 12 Dec 2024 12:17:25 +0000 Subject: [PATCH 25/42] Add e2e test for renaming Search block to "Instant Search" in the List View and Inspector Controls - Also adds a new helper function to open the List View in the e2e test utils. --- .../src/editor/index.ts | 3 + .../src/editor/open-list-view.ts | 30 +++++ .../interactivity/instant-search.spec.ts | 103 ++++++++++++++++++ 3 files changed, 136 insertions(+) create mode 100644 packages/e2e-test-utils-playwright/src/editor/open-list-view.ts diff --git a/packages/e2e-test-utils-playwright/src/editor/index.ts b/packages/e2e-test-utils-playwright/src/editor/index.ts index 4ed32134f0979a..af38bf6b2d464f 100644 --- a/packages/e2e-test-utils-playwright/src/editor/index.ts +++ b/packages/e2e-test-utils-playwright/src/editor/index.ts @@ -29,6 +29,7 @@ import { setIsFixedToolbar } from './set-is-fixed-toolbar'; import { switchToLegacyCanvas } from './switch-to-legacy-canvas'; import { transformBlockTo } from './transform-block-to'; import { switchEditorTool } from './switch-editor-tool'; +import { openListView } from './open-list-view'; type EditorConstructorProps = { page: Page; @@ -92,4 +93,6 @@ export class Editor { switchToLegacyCanvas.bind( this ); /** @borrows transformBlockTo as this.transformBlockTo */ transformBlockTo: typeof transformBlockTo = transformBlockTo.bind( this ); + /** @borrows openListView as this.openListView */ + openListView: typeof openListView = openListView.bind( this ); } diff --git a/packages/e2e-test-utils-playwright/src/editor/open-list-view.ts b/packages/e2e-test-utils-playwright/src/editor/open-list-view.ts new file mode 100644 index 00000000000000..e8605fdaa637be --- /dev/null +++ b/packages/e2e-test-utils-playwright/src/editor/open-list-view.ts @@ -0,0 +1,30 @@ +/** + * Internal dependencies + */ +import type { Editor } from './index'; + +/** + * Clicks on the button in the header which opens Document Settings sidebar when + * it is closed. + * + * @param this + */ +export async function openListView( this: Editor ) { + const toggleButton = this.page + .getByRole( 'region', { name: 'Editor top bar' } ) + .getByRole( 'button', { + name: 'Document Overview', + disabled: false, + } ); + + const isClosed = + ( await toggleButton.getAttribute( 'aria-expanded' ) ) === 'false'; + + if ( isClosed ) { + await toggleButton.click(); + await this.page + .getByRole( 'region', { name: 'Document Overview' } ) + .getByRole( 'button', { name: 'Close' } ) + .waitFor(); + } +} diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 19362ad27d41b6..6ce62e7291931c 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -91,6 +91,9 @@ test.describe( 'Instant Search', () => { test.afterAll( async ( { requestUtils } ) => { await requestUtils.deleteAllPosts(); await requestUtils.activateTheme( 'twentytwentyone' ); + + // disable the gutenberg-search-query-block experiment + await requestUtils.setGutenbergExperiments( [] ); } ); test.describe( 'Custom Query', () => { @@ -544,4 +547,104 @@ test.describe( 'Instant Search', () => { ); } ); } ); + + test.describe( 'Editor', () => { + test.beforeEach( async ( { admin } ) => { + await admin.createNewPost( { + postType: 'post', + title: 'Instant Search Test', + } ); + } ); + + test.afterAll( async ( { requestUtils } ) => { + await requestUtils.deleteAllPosts(); + } ); + + test( 'should rename Search block to "Instant Search" in the List View and Inspector Controls', async ( { + editor, + page, + } ) => { + // Insert Query block with enhanced pagination enabled + await editor.insertBlock( { + name: 'core/query', + attributes: { + enhancedPagination: true, + query: { + inherit: false, + perPage: 2, + order: 'desc', + orderBy: 'date', + offset: 0, + }, + }, + innerBlocks: [ + { name: 'core/search' }, + { + name: 'core/post-template', + innerBlocks: [ + { name: 'core/post-title' }, + { name: 'core/post-excerpt' }, + ], + }, + ], + } ); + + // Select the Search block + const searchBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Search', + } ); + await editor.selectBlocks( searchBlock ); + + // Make sure the List View is open + await editor.openListView(); + const listView = page.getByRole( 'region', { + name: 'Document Overview', + } ); + await expect( listView ).toBeVisible(); + + // Check that the Search block which is a child of the Query Loop block + // is renamed to "Instant Search" in the List View + await expect( + listView.getByText( 'Instant Search' ) + ).toBeVisible(); + + const editorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + + // Check that the Search block is renamed to "Instant Search" in the Inspector Controls title + await editor.canvas + .getByRole( 'document', { name: 'Block: Search' } ) + .click(); + await expect( editorSettings ).toContainText( + 'Instant Search (Search)' + ); + + // Select the Query Loop block and open the Advanced View and disable enhanced pagination + await editor.selectBlocks( + editor.canvas.getByRole( 'document', { + name: 'Block: Query Loop', + } ) + ); + await editor.openDocumentSettingsSidebar(); + await editorSettings + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await editorSettings + .getByRole( 'checkbox', { name: 'Reload full page' } ) + .click(); + + // Check that the block is renamed back to "Search" in the Inspector Controls title + await editor.canvas + .getByRole( 'document', { name: 'Block: Search' } ) + .click(); + await expect( editorSettings ).toContainText( 'Search' ); + await expect( editorSettings ).not.toContainText( + 'Instant Search (Search)' + ); + + // Check that the Search block is renamed back to "Search" in the List View + await expect( listView.getByText( 'Search' ) ).toBeVisible(); + } ); + } ); } ); From 2e7432d869834ed8dd4b586151410f3937402657 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 12 Dec 2024 12:49:47 +0000 Subject: [PATCH 26/42] Appease the PHP formatter --- packages/block-library/src/search/index.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 41478cc37d3a3b..a062be70439b57 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -200,7 +200,7 @@ function render_block_core_search( $attributes, $content, $block ) { 'ariaLabelExpanded' => $aria_label_expanded, 'ariaLabelCollapsed' => $aria_label_collapsed, ); - $form_directives .= ' + $form_directives .= ' data-wp-class--wp-block-search__searchfield-hidden="!context.isSearchInputVisible" data-wp-on-async--keydown="actions.handleSearchKeydown" data-wp-on-async--focusout="actions.handleSearchFocusout" From 95a96479ac7c1c5ca33c17a22e6c59546f751a76 Mon Sep 17 00:00:00 2001 From: Michal Date: Thu, 12 Dec 2024 13:04:28 +0000 Subject: [PATCH 27/42] Update lib/experimental/blocks.php Co-authored-by: Andrei Draganescu --- lib/experimental/blocks.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/experimental/blocks.php b/lib/experimental/blocks.php index d426afa5bc098a..6990f1dab9c2c6 100644 --- a/lib/experimental/blocks.php +++ b/lib/experimental/blocks.php @@ -119,7 +119,7 @@ function gutenberg_register_block_style( $block_name, $style_properties ) { } /** - * Adds the search query to Query Loop blocks if the instant search experiment is enabled. + * Passes the search query param to Query Loop blocks, if the instant search experiment is enabled. * * @param array $query The query variables. * @param WP_Block $block Block instance. From ac750ae2920ae4cafa07c9da9437512b34c20056 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 12 Dec 2024 13:15:56 +0000 Subject: [PATCH 28/42] Update the comment in Search block to clarify when Instant Search is enabled --- packages/block-library/src/search/index.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index a062be70439b57..e042f0cbcdc7a3 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -102,7 +102,12 @@ function render_block_core_search( $attributes, $content, $block ) { $input->set_attribute( 'tabindex', '-1' ); } - // Instant search is only available when using the enhanced pagination. + // Instant search is only enabled when both are true: + // 1. The block is a child of a Query Loop block. + // 2. The Query Loop block has the enhanced pagination feature enabled. + // + // Instant search functionality does not make sense without enhanced pagination + // because we might have to paginate the results of the search too! if ( $enhanced_pagination ) { wp_enqueue_script_module( '@wordpress/block-library/search/view' ); From b1c11f4c43fffaf314f80189816e2a7f612fcadb Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Wed, 18 Dec 2024 22:33:45 +0000 Subject: [PATCH 29/42] Update e2e tests for Instant Search to verify block renaming in Inspector Controls and List View --- .../e2e/specs/interactivity/instant-search.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 6ce62e7291931c..62826b3644389c 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -612,13 +612,15 @@ test.describe( 'Instant Search', () => { name: 'Editor settings', } ); + const blockCard = editorSettings.locator( + '.block-editor-block-card' + ); + // Check that the Search block is renamed to "Instant Search" in the Inspector Controls title await editor.canvas .getByRole( 'document', { name: 'Block: Search' } ) .click(); - await expect( editorSettings ).toContainText( - 'Instant Search (Search)' - ); + await expect( blockCard ).toContainText( 'Instant Search' ); // Select the Query Loop block and open the Advanced View and disable enhanced pagination await editor.selectBlocks( @@ -638,10 +640,8 @@ test.describe( 'Instant Search', () => { await editor.canvas .getByRole( 'document', { name: 'Block: Search' } ) .click(); - await expect( editorSettings ).toContainText( 'Search' ); - await expect( editorSettings ).not.toContainText( - 'Instant Search (Search)' - ); + await expect( blockCard ).toContainText( 'Search' ); + await expect( blockCard ).not.toContainText( 'Instant Search' ); // Check that the Search block is renamed back to "Search" in the List View await expect( listView.getByText( 'Search' ) ).toBeVisible(); From 3dd53b9fafe0ee2ddf76c5bcc3a96166528d15ff Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 19 Dec 2024 14:12:50 +0000 Subject: [PATCH 30/42] Simplify the logic for enqueuing `@wordpress/block-library/search/view` --- packages/block-library/src/search/index.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index e042f0cbcdc7a3..7ec234575e700b 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -108,13 +108,11 @@ function render_block_core_search( $attributes, $content, $block ) { // // Instant search functionality does not make sense without enhanced pagination // because we might have to paginate the results of the search too! - if ( $enhanced_pagination ) { + if ( $enhanced_pagination && $instant_search_enabled) { wp_enqueue_script_module( '@wordpress/block-library/search/view' ); - - if ( $instant_search_enabled ) { - $input->set_attribute( 'data-wp-bind--value', 'context.search' ); - $input->set_attribute( 'data-wp-on-async--input', 'actions.updateSearch' ); - } + + $input->set_attribute( 'data-wp-bind--value', 'context.search' ); + $input->set_attribute( 'data-wp-on-async--input', 'actions.updateSearch' ); } } From bdaaf2e3eaf604fd4747eaf5bf49c5d6d03d7a94 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 19 Dec 2024 15:27:28 +0000 Subject: [PATCH 31/42] Put back the empty() calls that were erroneouslyreplaced with ternaries --- packages/block-library/src/search/index.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 7ec234575e700b..4313a686e91346 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -31,8 +31,8 @@ function render_block_core_search( $attributes, $content, $block ) { $input_id = wp_unique_id( 'wp-block-search__input-' ); $classnames = classnames_for_block_core_search( $attributes ); - $show_label = ( ! empty( $attributes['showLabel'] ) ) ? true : false; - $use_icon_button = ( ! empty( $attributes['buttonUseIcon'] ) ) ? true : false; + $show_label = ! empty( $attributes['showLabel'] ); + $use_icon_button = ! empty( $attributes['buttonUseIcon'] ); $show_button = true; if ( isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination'] ) { $show_button = false; @@ -108,9 +108,9 @@ function render_block_core_search( $attributes, $content, $block ) { // // Instant search functionality does not make sense without enhanced pagination // because we might have to paginate the results of the search too! - if ( $enhanced_pagination && $instant_search_enabled) { + if ( $enhanced_pagination && $instant_search_enabled ) { wp_enqueue_script_module( '@wordpress/block-library/search/view' ); - + $input->set_attribute( 'data-wp-bind--value', 'context.search' ); $input->set_attribute( 'data-wp-on-async--input', 'actions.updateSearch' ); } From 97b3c845e11c0d1394949e55488a38a2d5eb1353 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 19 Dec 2024 15:44:31 +0000 Subject: [PATCH 32/42] Only hide the search button if BOTH enhanced pagination & instant search experiment are enabled. --- packages/block-library/src/search/index.php | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 4313a686e91346..a557d1fe2f6c2d 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -33,9 +33,20 @@ function render_block_core_search( $attributes, $content, $block ) { $classnames = classnames_for_block_core_search( $attributes ); $show_label = ! empty( $attributes['showLabel'] ); $use_icon_button = ! empty( $attributes['buttonUseIcon'] ); + + // Check if the block is using the enhanced pagination. + $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; + + // Check if the block is using the instant search experiment. + $gutenberg_experiments = get_option( 'gutenberg-experiments' ); + $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + $show_button = true; - if ( isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination'] ) { + + // If the block is using the instant search experiment and the enhanced pagination, hide the button. + if ( $instant_search_enabled && $enhanced_pagination ) { $show_button = false; + // If the button position is no-button, ALSO hide the button. } elseif ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) { $show_button = false; } @@ -53,12 +64,6 @@ function render_block_core_search( $attributes, $content, $block ) { // This variable is a constant and its value is always false at this moment. // It is defined this way because some values depend on it, in case it changes in the future. $open_by_default = false; - // Check if the block is using the enhanced pagination. - $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; - - // Check if the block is using the instant search experiment. - $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); $label_inner_html = empty( $attributes['label'] ) ? __( 'Search' ) : wp_kses_post( $attributes['label'] ); $label = new WP_HTML_Tag_Processor( sprintf( '', $inline_styles['label'], $label_inner_html ) ); From 20957067ff7ef568713b860a5c99366471f8bb5b Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 19 Dec 2024 15:54:21 +0000 Subject: [PATCH 33/42] Format PHP --- packages/block-library/src/search/index.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index a557d1fe2f6c2d..26275ea91c75b4 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -33,7 +33,7 @@ function render_block_core_search( $attributes, $content, $block ) { $classnames = classnames_for_block_core_search( $attributes ); $show_label = ! empty( $attributes['showLabel'] ); $use_icon_button = ! empty( $attributes['buttonUseIcon'] ); - + // Check if the block is using the enhanced pagination. $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; @@ -41,7 +41,7 @@ function render_block_core_search( $attributes, $content, $block ) { $gutenberg_experiments = get_option( 'gutenberg-experiments' ); $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); - $show_button = true; + $show_button = true; // If the block is using the instant search experiment and the enhanced pagination, hide the button. if ( $instant_search_enabled && $enhanced_pagination ) { From 77e86d21693d72c0da94a30ef78b1bfff3b911af Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Thu, 19 Dec 2024 17:08:07 +0000 Subject: [PATCH 34/42] Removed redundant getter for `isSearchInputVisible` and simplified the logic to directly access the context. --- packages/block-library/src/search/view.js | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/packages/block-library/src/search/view.js b/packages/block-library/src/search/view.js index d280d355322afa..7492faa884993b 100644 --- a/packages/block-library/src/search/view.js +++ b/packages/block-library/src/search/view.js @@ -6,7 +6,7 @@ import { store, getContext, getElement } from '@wordpress/interactivity'; /** @type {( () => void ) | null} */ let supersedePreviousSearch = null; -const { state, actions } = store( +const { actions } = store( 'core/search', { state: { @@ -32,25 +32,14 @@ const { state, actions } = store( const { isSearchInputVisible } = getContext(); return isSearchInputVisible ? '0' : '-1'; }, - get isSearchInputVisible() { - const ctx = getContext(); - - // `ctx.isSearchInputVisible` is a client-side-only context value, so - // if it's not set, it means that it's an initial page load, so we need - // to return the value of `ctx.isSearchInputInitiallyVisible`. - if ( typeof ctx.isSearchInputVisible === 'undefined' ) { - return ctx.isSearchInputInitiallyVisible; - } - return ctx.isSearchInputVisible; - }, }, actions: { openSearchInput( event ) { - if ( ! state.isSearchInputVisible ) { + const ctx = getContext(); + const { ref } = getElement(); + if ( ! ctx.isSearchInputVisible ) { event.preventDefault(); - const ctx = getContext(); ctx.isSearchInputVisible = true; - const { ref } = getElement(); ref.parentElement.querySelector( 'input' ).focus(); } }, From 15b80fe163c33cd457ed3f69a6f6eb9c4518c064 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 30 Dec 2024 19:20:10 +0000 Subject: [PATCH 35/42] Check presence of instant search experiment in addition to the checking if enhancedPagination is enabled --- lib/experimental/editor-settings.php | 3 +++ packages/block-library/src/search/edit.js | 29 +++++++---------------- 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/lib/experimental/editor-settings.php b/lib/experimental/editor-settings.php index 5b36c32b3c8296..c2f3c40289b4df 100644 --- a/lib/experimental/editor-settings.php +++ b/lib/experimental/editor-settings.php @@ -28,6 +28,9 @@ function gutenberg_enable_experiments() { if ( gutenberg_is_experiment_enabled( 'gutenberg-full-page-client-side-navigation' ) ) { wp_add_inline_script( 'wp-block-library', 'window.__experimentalFullPageClientSideNavigation = true', 'before' ); } + if ( $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ) ) { + wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableSearchQueryBlock = true', 'before' ); + } if ( $gutenberg_experiments && array_key_exists( 'gutenberg-block-comment', $gutenberg_experiments ) ) { wp_add_inline_script( 'wp-block-editor', 'window.__experimentalEnableBlockComment = true', 'before' ); } diff --git a/packages/block-library/src/search/edit.js b/packages/block-library/src/search/edit.js index 7165f818c61a49..88feae5a81a747 100644 --- a/packages/block-library/src/search/edit.js +++ b/packages/block-library/src/search/edit.js @@ -83,25 +83,12 @@ export default function SearchEdit( { style, } = attributes; - const isEnhancedPagination = context?.enhancedPagination; - - useEffect( () => { - if ( isEnhancedPagination ) { - // Add the name to the metadata - setAttributes( { metadata: { name: 'Instant Search' } } ); - } else { - // Remove the name from the metadata - const { name, ...metadata } = attributes.metadata || {}; - setAttributes( { metadata } ); - } - - // We disable the exhaustive-deps warning because the effect should not depend - // on the attributes.metadata value. We only want to re-run the effect when the - // isEnhancedPagination value changes. - - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ isEnhancedPagination, setAttributes ] ); + // Check if the block is inside a Query block with enhanced pagination enabled + // and if the `__experimentalEnableSearchQueryBlock` flag is enabled. + const hasInstantSearch = !! ( + context?.enhancedPagination && + window?.__experimentalEnableSearchQueryBlock + ); const wasJustInsertedIntoNavigationBlock = useSelect( ( select ) => { @@ -406,7 +393,7 @@ export default function SearchEdit( { } } className={ showLabel ? 'is-pressed' : undefined } /> - { ! isEnhancedPagination && ( + { ! hasInstantSearch && ( <> - { isEnhancedPagination ? ( + { hasInstantSearch ? ( renderTextField() ) : ( <> From dd42f507c9bc26c4544e35b1cdd9ee76ee919678 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 13:30:14 +0000 Subject: [PATCH 36/42] Simplify instant search and enhanced pagination checks. --- packages/block-library/src/search/index.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 26275ea91c75b4..5f3c4bc571cff2 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -39,12 +39,12 @@ function render_block_core_search( $attributes, $content, $block ) { // Check if the block is using the instant search experiment. $gutenberg_experiments = get_option( 'gutenberg-experiments' ); - $instant_search_enabled = $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); + $instant_search_enabled = $enhanced_pagination && $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); $show_button = true; // If the block is using the instant search experiment and the enhanced pagination, hide the button. - if ( $instant_search_enabled && $enhanced_pagination ) { + if ( $instant_search_enabled ) { $show_button = false; // If the button position is no-button, ALSO hide the button. } elseif ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) { @@ -113,7 +113,7 @@ function render_block_core_search( $attributes, $content, $block ) { // // Instant search functionality does not make sense without enhanced pagination // because we might have to paginate the results of the search too! - if ( $enhanced_pagination && $instant_search_enabled ) { + if ( $instant_search_enabled ) { wp_enqueue_script_module( '@wordpress/block-library/search/view' ); $input->set_attribute( 'data-wp-bind--value', 'context.search' ); @@ -195,7 +195,7 @@ function render_block_core_search( $attributes, $content, $block ) { $form_context = array(); // If it's interactive, add the directives. - if ( $is_expandable_searchfield || ( $enhanced_pagination && $instant_search_enabled ) ) { + if ( $is_expandable_searchfield || $instant_search_enabled ) { $form_directives = 'data-wp-interactive="core/search"'; } @@ -215,7 +215,7 @@ function render_block_core_search( $attributes, $content, $block ) { '; } - if ( $enhanced_pagination && $instant_search_enabled && isset( $block->context['queryId'] ) ) { + if ( $instant_search_enabled && isset( $block->context['queryId'] ) ) { $search = ''; From 1d70719117daae9044d39950c012ecf8b9e1ae3c Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 13:57:35 +0000 Subject: [PATCH 37/42] Update comments --- packages/block-library/src/search/index.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/block-library/src/search/index.php b/packages/block-library/src/search/index.php index 5f3c4bc571cff2..c6a0befe279bd2 100644 --- a/packages/block-library/src/search/index.php +++ b/packages/block-library/src/search/index.php @@ -37,13 +37,12 @@ function render_block_core_search( $attributes, $content, $block ) { // Check if the block is using the enhanced pagination. $enhanced_pagination = isset( $block->context['enhancedPagination'] ) && $block->context['enhancedPagination']; - // Check if the block is using the instant search experiment. + // Check if the block is using the instant search experiment, which requires the enhanced pagination. $gutenberg_experiments = get_option( 'gutenberg-experiments' ); $instant_search_enabled = $enhanced_pagination && $gutenberg_experiments && array_key_exists( 'gutenberg-search-query-block', $gutenberg_experiments ); $show_button = true; - // If the block is using the instant search experiment and the enhanced pagination, hide the button. if ( $instant_search_enabled ) { $show_button = false; // If the button position is no-button, ALSO hide the button. From e0dd93ba3c569ee744ff44111b8ea1cbe38eff15 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 14:27:46 +0000 Subject: [PATCH 38/42] Refactor Instant Search tests to verify toolbar button visibility when Search block is inside Query block with enhanced pagination. Removed checks for renaming Search block to "Instant Search" in List View and Inspector Controls, focusing on button visibility instead. --- .../interactivity/instant-search.spec.ts | 60 ++++++++----------- 1 file changed, 26 insertions(+), 34 deletions(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 62826b3644389c..ec33cf0fc3c49a 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -560,7 +560,7 @@ test.describe( 'Instant Search', () => { await requestUtils.deleteAllPosts(); } ); - test( 'should rename Search block to "Instant Search" in the List View and Inspector Controls', async ( { + test( 'should hide specific toolbar buttons when Search block is inside Query block with enhanced pagination', async ( { editor, page, } ) => { @@ -595,40 +595,29 @@ test.describe( 'Instant Search', () => { } ); await editor.selectBlocks( searchBlock ); - // Make sure the List View is open - await editor.openListView(); - const listView = page.getByRole( 'region', { - name: 'Document Overview', + // Verify that the specific toolbar buttons are hidden + const toolbar = page.getByRole( 'toolbar', { + name: 'Block tools', } ); - await expect( listView ).toBeVisible(); - - // Check that the Search block which is a child of the Query Loop block - // is renamed to "Instant Search" in the List View await expect( - listView.getByText( 'Instant Search' ) - ).toBeVisible(); - - const editorSettings = page.getByRole( 'region', { - name: 'Editor settings', - } ); - - const blockCard = editorSettings.locator( - '.block-editor-block-card' - ); - - // Check that the Search block is renamed to "Instant Search" in the Inspector Controls title - await editor.canvas - .getByRole( 'document', { name: 'Block: Search' } ) - .click(); - await expect( blockCard ).toContainText( 'Instant Search' ); + toolbar.getByRole( 'button', { + name: 'Change button position', + } ) + ).toBeHidden(); + await expect( + toolbar.getByRole( 'button', { name: 'Use button with icon' } ) + ).toBeHidden(); - // Select the Query Loop block and open the Advanced View and disable enhanced pagination + // Select the Query Loop block and disable enhanced pagination await editor.selectBlocks( editor.canvas.getByRole( 'document', { name: 'Block: Query Loop', } ) ); await editor.openDocumentSettingsSidebar(); + const editorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); await editorSettings .getByRole( 'button', { name: 'Advanced' } ) .click(); @@ -636,15 +625,18 @@ test.describe( 'Instant Search', () => { .getByRole( 'checkbox', { name: 'Reload full page' } ) .click(); - // Check that the block is renamed back to "Search" in the Inspector Controls title - await editor.canvas - .getByRole( 'document', { name: 'Block: Search' } ) - .click(); - await expect( blockCard ).toContainText( 'Search' ); - await expect( blockCard ).not.toContainText( 'Instant Search' ); + // Select the Search block again + await editor.selectBlocks( searchBlock ); - // Check that the Search block is renamed back to "Search" in the List View - await expect( listView.getByText( 'Search' ) ).toBeVisible(); + // Verify that the toolbar buttons are now visible + await expect( + toolbar.getByRole( 'button', { + name: 'Change button position', + } ) + ).toBeVisible(); + await expect( + toolbar.getByRole( 'button', { name: 'Use button with icon' } ) + ).toBeVisible(); } ); } ); } ); From 2db363aef52f776f1a405aab4d33013dc2747df9 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 15:18:28 +0000 Subject: [PATCH 39/42] Update Search block label in ListView when it's an Instant Search - Updated `getBlockLabel` to include `clientId` for improved label context. - Modified `useBlockDisplayTitle` to utilize the updated `getBlockLabel` function. - Introduced `__experimentalLabel` in the search settings to handle block labels based on query loop context and instant search status. --- .../block-title/use-block-display-title.js | 7 +++- packages/block-library/src/search/index.js | 39 ++++++++++++++++++- packages/blocks/src/api/utils.js | 10 ++++- 3 files changed, 52 insertions(+), 4 deletions(-) diff --git a/packages/block-editor/src/components/block-title/use-block-display-title.js b/packages/block-editor/src/components/block-title/use-block-display-title.js index a51b336554a2a7..137c4ec16039d8 100644 --- a/packages/block-editor/src/components/block-title/use-block-display-title.js +++ b/packages/block-editor/src/components/block-title/use-block-display-title.js @@ -51,7 +51,12 @@ export default function useBlockDisplayTitle( { } const attributes = getBlockAttributes( clientId ); - const label = getBlockLabel( blockType, attributes, context ); + const label = getBlockLabel( + blockType, + attributes, + context, + clientId + ); // If the label is defined we prioritize it over a possible block variation title match. if ( label !== blockType.title ) { return label; diff --git a/packages/block-library/src/search/index.js b/packages/block-library/src/search/index.js index 85770a23268cba..c81abebdf79800 100644 --- a/packages/block-library/src/search/index.js +++ b/packages/block-library/src/search/index.js @@ -1,8 +1,10 @@ /** * WordPress dependencies */ -import { __ } from '@wordpress/i18n'; +import { __, sprintf } from '@wordpress/i18n'; import { search as icon } from '@wordpress/icons'; +import { select } from '@wordpress/data'; +import { store as blockEditorStore } from '@wordpress/block-editor'; /** * Internal dependencies @@ -18,6 +20,41 @@ export { metadata, name }; export const settings = { icon, + __experimentalLabel( attributes, { clientId } ) { + const { label } = attributes; + const customName = attributes?.metadata?.name; + + // Check if the block is inside a Query Loop block. + const queryLoopBlockIds = select( + blockEditorStore + ).getBlockParentsByBlockName( clientId, 'core/query' ); + + // If the block is not inside a Query Loop block, return the block label. + if ( ! queryLoopBlockIds.length ) { + return customName || label; + } + + const queryLoopBlock = select( blockEditorStore ).getBlock( + queryLoopBlockIds[ 0 ] + ); + + // Check if the Query Loop block has enhanced pagination enabled and + // if the `__experimentalEnableSearchQueryBlock` flag is enabled. + const hasInstantSearch = !! ( + queryLoopBlock?.attributes?.enhancedPagination && + window?.__experimentalEnableSearchQueryBlock + ); + + if ( ! hasInstantSearch ) { + return customName || label; + } + + return sprintf( + /* translators: %s: The block label */ + __( '%s (Instant search enabled)' ), + customName || label + ); + }, example: { attributes: { buttonText: __( 'Search' ), label: __( 'Search' ) }, viewportWidth: 400, diff --git a/packages/blocks/src/api/utils.js b/packages/blocks/src/api/utils.js index 1a215036496559..5bba5863893973 100644 --- a/packages/blocks/src/api/utils.js +++ b/packages/blocks/src/api/utils.js @@ -153,13 +153,19 @@ export function normalizeBlockType( blockTypeOrName ) { * @param {Object} blockType The block type. * @param {Object} attributes The values of the block's attributes. * @param {Object} context The intended use for the label. + * @param {string} clientId The block's client ID. * * @return {string} The block label. */ -export function getBlockLabel( blockType, attributes, context = 'visual' ) { +export function getBlockLabel( + blockType, + attributes, + context = 'visual', + clientId +) { const { __experimentalLabel: getLabel, title } = blockType; - const label = getLabel && getLabel( attributes, { context } ); + const label = getLabel && getLabel( attributes, { context, clientId } ); if ( ! label ) { return title; From c20526fff7f5af97d4686c653a4d4a6726dbca36 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 15:37:56 +0000 Subject: [PATCH 40/42] Enhance Instant Search e2e tests to verify List View label updates - Added a test to ensure the List View label updates correctly when the Search block is part of a Query block with enhanced pagination enabled. - Verified that the label reflects "Instant search enabled" when applicable and reverts to "Search" when enhanced pagination is disabled. --- .../interactivity/instant-search.spec.ts | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index ec33cf0fc3c49a..4555dd0252129d 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -638,5 +638,79 @@ test.describe( 'Instant Search', () => { toolbar.getByRole( 'button', { name: 'Use button with icon' } ) ).toBeVisible(); } ); + + test( 'should update List View label when Search block becomes Instant Search', async ( { + editor, + page, + } ) => { + // Insert Query block with enhanced pagination enabled + await editor.insertBlock( { + name: 'core/query', + attributes: { + enhancedPagination: true, + query: { + inherit: false, + perPage: 2, + order: 'desc', + orderBy: 'date', + offset: 0, + }, + }, + innerBlocks: [ + { + name: 'core/search', + attributes: { label: 'Search' }, + }, + { + name: 'core/post-template', + innerBlocks: [ + { name: 'core/post-title' }, + { name: 'core/post-excerpt' }, + ], + }, + ], + } ); + + // Select the Search block + const searchBlock = editor.canvas.getByRole( 'document', { + name: 'Block: Search', + } ); + await editor.selectBlocks( searchBlock ); + + // Open List View + await editor.openListView(); + const listView = page.getByRole( 'region', { + name: 'Document Overview', + } ); + await expect( listView ).toBeVisible(); + + // Verify that the Search block label includes "Instant search enabled" + await expect( + listView.getByText( 'Search (Instant search enabled)' ) + ).toBeVisible(); + + // Select the Query Loop block and disable enhanced pagination + await editor.selectBlocks( + editor.canvas.getByRole( 'document', { + name: 'Block: Query Loop', + } ) + ); + await editor.openDocumentSettingsSidebar(); + const editorSettings = page.getByRole( 'region', { + name: 'Editor settings', + } ); + await editorSettings + .getByRole( 'button', { name: 'Advanced' } ) + .click(); + await editorSettings + .getByRole( 'checkbox', { name: 'Reload full page' } ) + .click(); + + // Verify that the Search block label is back to normal + await expect( listView.getByText( 'Search' ) ).toBeVisible(); + await expect( + listView.getByText( 'Search (Instant search enabled)' ) + ).toBeHidden(); + } ); } ); } ); From 6b975c4d413573e5e61f0d22b8b4ace4f0e7cdd9 Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 16:11:53 +0000 Subject: [PATCH 41/42] Update Search block label to default to 'Search' when no custom name is provided --- packages/block-library/src/search/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/block-library/src/search/index.js b/packages/block-library/src/search/index.js index c81abebdf79800..e823cf6177472b 100644 --- a/packages/block-library/src/search/index.js +++ b/packages/block-library/src/search/index.js @@ -52,7 +52,7 @@ export const settings = { return sprintf( /* translators: %s: The block label */ __( '%s (Instant search enabled)' ), - customName || label + customName || label || 'Search' ); }, example: { From 7ca6d27e67860fff4a385d92eabe859e2cb45ffd Mon Sep 17 00:00:00 2001 From: Michal Czaplinski Date: Mon, 6 Jan 2025 16:33:55 +0000 Subject: [PATCH 42/42] Remove default label attribute from Search block in Instant Search tests - It's not needed because we fall back to 'Search' as last resort in `_experimentalLabel()` (previous commit). --- test/e2e/specs/interactivity/instant-search.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/e2e/specs/interactivity/instant-search.spec.ts b/test/e2e/specs/interactivity/instant-search.spec.ts index 4555dd0252129d..fd37610aa85d35 100644 --- a/test/e2e/specs/interactivity/instant-search.spec.ts +++ b/test/e2e/specs/interactivity/instant-search.spec.ts @@ -659,7 +659,6 @@ test.describe( 'Instant Search', () => { innerBlocks: [ { name: 'core/search', - attributes: { label: 'Search' }, }, { name: 'core/post-template',