Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Query and Search blocks: support for Instant Search via render_block_context filter #67013

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3869e85
add the filter and test
michalczaplinski Nov 13, 2024
4d4cd20
remove
michalczaplinski Nov 14, 2024
a09e0dd
Create instant search using `render_block_context` filter.
michalczaplinski Nov 14, 2024
55a41d8
Remove handling of inheritd query from `search/index.php`
michalczaplinski Nov 14, 2024
4baafc4
Handle case when query is defined in block context in DB.
michalczaplinski Nov 14, 2024
949e8c2
Guard against queryId being undefined in block context
michalczaplinski Nov 18, 2024
c3fc24a
Merge branch 'trunk' into feature/search-query-take-2
michalczaplinski Nov 19, 2024
f4e2a93
Fix the e2e test suite
michalczaplinski Nov 19, 2024
5931b6d
Move the filter to `/experimental` folder.
michalczaplinski Nov 20, 2024
997f8c0
Add an e2e test case if query.search attribute is present
michalczaplinski Nov 20, 2024
0f57df0
Remove the search button when instant search is enabled
michalczaplinski Nov 20, 2024
c9a907c
Merge branch 'trunk' into feature/search-query-take-2
michalczaplinski Nov 21, 2024
157a395
Do not delete pages and templates in e2e tests
michalczaplinski Nov 21, 2024
f6d39ad
Set the pageId for Multiple Queries tests
michalczaplinski Nov 21, 2024
3f29890
Fix the block name via metadata when Seach is instant.
michalczaplinski Nov 25, 2024
db5f965
Remove stuff related to Default queries from `view.js`
michalczaplinski Nov 26, 2024
295e7ab
Merge remote-tracking branch 'origin/trunk' into feature/search-query…
michalczaplinski Nov 27, 2024
4b20168
Add `attributes.metadata` to useEffect dependency array
michalczaplinski Nov 27, 2024
2204ed8
Remove `attributes.metadata` & label from dependency array
michalczaplinski Nov 29, 2024
40ea84d
Don't use `Promise.withResolvers()` in search block's view.js
michalczaplinski Dec 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions lib/experimental/blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
12 changes: 12 additions & 0 deletions lib/experiments-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,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',
)
);

add_settings_field(
'gutenberg-editor-write-mode',
__( 'Editor write mode', 'gutenberg' ),
Expand Down
1 change: 1 addition & 0 deletions packages/block-library/src/search/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"default": false
}
},
"usesContext": [ "enhancedPagination", "query", "queryId" ],
"supports": {
"align": [ "left", "center", "right" ],
"color": {
Expand Down
74 changes: 49 additions & 25 deletions packages/block-library/src/search/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export default function SearchEdit( {
toggleSelection,
isSelected,
clientId,
context,
} ) {
const {
label,
Expand All @@ -82,6 +83,19 @@ 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 } );
}
}, [ isEnhancedPagination, setAttributes ] );

const wasJustInsertedIntoNavigationBlock = useSelect(
( select ) => {
const { getBlockParentsByBlockName, wasBlockJustInserted } =
Expand Down Expand Up @@ -385,24 +399,28 @@ export default function SearchEdit( {
} }
className={ showLabel ? 'is-pressed' : undefined }
/>
<ToolbarDropdownMenu
icon={ getButtonPositionIcon() }
label={ __( 'Change button position' ) }
controls={ buttonPositionControls }
/>
{ ! hasNoButton && (
<ToolbarButton
title={ __( 'Use button with icon' ) }
icon={ buttonWithIcon }
onClick={ () => {
setAttributes( {
buttonUseIcon: ! buttonUseIcon,
} );
} }
className={
buttonUseIcon ? 'is-pressed' : undefined
}
/>
{ ! isEnhancedPagination && (
<>
<ToolbarDropdownMenu
icon={ getButtonPositionIcon() }
label={ __( 'Change button position' ) }
controls={ buttonPositionControls }
/>
{ ! hasNoButton && (
<ToolbarButton
title={ __( 'Use button with icon' ) }
icon={ buttonWithIcon }
onClick={ () => {
setAttributes( {
buttonUseIcon: ! buttonUseIcon,
} );
} }
className={
buttonUseIcon ? 'is-pressed' : undefined
}
/>
) }
</>
) }
</ToolbarGroup>
</BlockControls>
Expand Down Expand Up @@ -596,16 +614,22 @@ export default function SearchEdit( {
} }
showHandle={ isSelected }
>
{ ( isButtonPositionInside ||
isButtonPositionOutside ||
hasOnlyButton ) && (
{ isEnhancedPagination ? (
renderTextField()
) : (
<>
{ renderTextField() }
{ renderButton() }
{ ( isButtonPositionInside ||
isButtonPositionOutside ||
hasOnlyButton ) && (
<>
{ renderTextField() }
{ renderButton() }
</>
) }

{ hasNoButton && renderTextField() }
</>
) }

{ hasNoButton && renderTextField() }
</ResizableBox>
</div>
);
Expand Down
81 changes: 63 additions & 18 deletions packages/block-library/src/search/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<!--
// wp:search /-->`. Support these by defaulting an undefined label and
Expand All @@ -29,11 +29,16 @@ 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_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 = '';
Expand All @@ -48,6 +53,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( '<label %1$s>%2$s</label>', $inline_styles['label'], $label_inner_html ) );
Expand Down Expand Up @@ -90,6 +101,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 ) {
Expand Down Expand Up @@ -163,28 +184,52 @@ 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(
$form_context = array(
'isSearchInputInitiallyVisible' => $open_by_default,
'inputId' => $input_id,
'ariaLabelExpanded' => $aria_label_expanded,
'ariaLabelCollapsed' => $aria_label_collapsed,
);
$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"
';
}

if ( $enhanced_pagination && $instant_search_enabled && isset( $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,
array(
'isSearchInputVisible' => $open_by_default,
'inputId' => $input_id,
'ariaLabelExpanded' => $aria_label_expanded,
'ariaLabelCollapsed' => $aria_label_collapsed,
'search' => $search,
'queryId' => $block->context['queryId'],
)
);
$form_directives = '
data-wp-interactive="core/search"'
. $form_context .
'data-wp-class--wp-block-search__searchfield-hidden="!context.isSearchInputVisible"
data-wp-on-async--keydown="actions.handleSearchKeydown"
data-wp-on-async--focusout="actions.handleSearchFocusout"
';
}

$form_directives .= wp_interactivity_data_wp_context( $form_context );

return sprintf(
'<form role="search" method="get" action="%1s" %2s %3s>%4s</form>',
esc_url( home_url( '/' ) ),
Expand Down
78 changes: 74 additions & 4 deletions packages/block-library/src/search/view.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand All @@ -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();
}
},
Expand Down Expand Up @@ -66,6 +80,62 @@ 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?.();
let resolve, reject;
const promise = new Promise( ( res, rej ) => {
resolve = res;
reject = rej;
} );
const timeout = setTimeout( resolve, 300 );
supersedePreviousSearch = () => {
clearTimeout( timeout );
reject();
};
try {
yield promise;
} catch {
return;
}

const url = new URL( window.location.href );

if ( 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 {
// 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 }
Expand Down
Loading
Loading