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

Search Block: add button only with expandable input #50487

Merged
merged 22 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
6baab10
Build docs.
jffng May 9, 2023
eccace1
Fix lint errors.
jffng May 10, 2023
b976118
Fix php lint.
jffng May 10, 2023
73176c1
Remove duplicate call to view script.
jffng May 11, 2023
0cb5ef2
List all dependencies in useEffect calls.
jffng May 12, 2023
7f2869a
Add isSearchFieldHidden and buttonBehavior attributes to fixtures.
jffng May 12, 2023
223c2d2
Allow input to expand on button focus and clean up CSS.
jffng May 12, 2023
48b7ba3
Add hidden class on initial block rendering.
jffng May 15, 2023
cc7bf1b
Remove unneeded CSS.
jffng May 15, 2023
05ba9a5
Allow focus and blur to toggle search field, and simplify logic.
jffng May 15, 2023
b028cbb
Handle keyboard navigation, event handling, and refactor.
jffng May 17, 2023
a769701
Escape to close input, and do not allow focus to expand invisible ele…
jffng May 22, 2023
3a5c81a
Add aria attributes to describe hidden / expanded states.
jffng May 22, 2023
9282f83
Fix lint warnings.
jffng May 22, 2023
41946f5
Fix php warnings.
jffng May 22, 2023
919117f
Add and remove relevant ARIA attributes when input state changes.
jffng May 22, 2023
1860360
Make strict comparison.
jffng May 22, 2023
c3b84f5
Clean up aria and take input out of taborder by default.
jffng May 23, 2023
7665720
Fix label clicking behavior, remove unneeded event listeners.
jffng May 25, 2023
13e8dd7
Fix comparison bug.
jffng Jun 1, 2023
fe7ff74
Add and remove type and aria-label attributes to reflect form state.
jffng Jun 1, 2023
b77edab
Fix conditional on useEffect to show input on width change.
jffng Jun 2, 2023
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
2 changes: 1 addition & 1 deletion docs/reference-guides/core-blocks.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,7 @@ Help visitors find your content. ([Source](https://github.com/WordPress/gutenber
- **Name:** core/search
- **Category:** widgets
- **Supports:** align (center, left, right), anchor, color (background, gradients, text), typography (fontSize, lineHeight), ~~html~~
- **Attributes:** buttonPosition, buttonText, buttonUseIcon, label, placeholder, query, showLabel, width, widthUnit
- **Attributes:** buttonBehavior, buttonPosition, buttonText, buttonUseIcon, isSearchFieldHidden, label, placeholder, query, showLabel, width, widthUnit

## Separator

Expand Down
8 changes: 8 additions & 0 deletions packages/block-library/src/search/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
"query": {
"type": "object",
"default": {}
},
"buttonBehavior": {
"type": "string",
"default": "expand-searchfield"
},
"isSearchFieldHidden": {
"type": "boolean",
"default": false
}
},
"supports": {
Expand Down
63 changes: 60 additions & 3 deletions packages/block-library/src/search/edit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
useSetting,
} from '@wordpress/block-editor';
import { useDispatch, useSelect } from '@wordpress/data';
import { useEffect } from '@wordpress/element';
import { useEffect, useRef } from '@wordpress/element';
import {
ToolbarDropdownMenu,
ToolbarGroup,
Expand Down Expand Up @@ -59,6 +59,8 @@ import {
// button is placed inside wrapper.
const DEFAULT_INNER_PADDING = '4px';

const BUTTON_BEHAVIOR_EXPAND = 'expand-searchfield';

export default function SearchEdit( {
className,
attributes,
Expand All @@ -77,6 +79,8 @@ export default function SearchEdit( {
buttonText,
buttonPosition,
buttonUseIcon,
buttonBehavior,
isSearchFieldHidden,
style,
} = attributes;

Expand Down Expand Up @@ -130,12 +134,32 @@ export default function SearchEdit( {
const isButtonPositionOutside = 'button-outside' === buttonPosition;
const hasNoButton = 'no-button' === buttonPosition;
const hasOnlyButton = 'button-only' === buttonPosition;
const searchFieldRef = useRef();
const buttonRef = useRef();

const units = useCustomUnits( {
availableUnits: [ '%', 'px' ],
defaultValues: { '%': PC_WIDTH_DEFAULT, px: PX_WIDTH_DEFAULT },
} );

useEffect( () => {
if ( hasOnlyButton && ! isSelected ) {
setAttributes( {
isSearchFieldHidden: true,
} );
}
}, [ hasOnlyButton, isSelected, setAttributes ] );

useEffect( () => {
if ( hasOnlyButton || ! isSelected ) {
return;
}

setAttributes( {
isSearchFieldHidden: false,
} );
}, [ hasOnlyButton, isSelected, setAttributes, width ] );

const getBlockClassNames = () => {
return classnames(
className,
Expand All @@ -152,6 +176,12 @@ export default function SearchEdit( {
: undefined,
buttonUseIcon && ! hasNoButton
? 'wp-block-search__icon-button'
: undefined,
hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior
? 'wp-block-search__button-behavior-expand'
: undefined,
hasOnlyButton && isSearchFieldHidden
? 'wp-block-search__searchfield-hidden'
: undefined
);
};
Expand All @@ -165,6 +195,7 @@ export default function SearchEdit( {
onClick: () => {
setAttributes( {
buttonPosition: 'button-outside',
isSearchFieldHidden: false,
} );
},
},
Expand All @@ -176,6 +207,7 @@ export default function SearchEdit( {
onClick: () => {
setAttributes( {
buttonPosition: 'button-inside',
isSearchFieldHidden: false,
} );
},
},
Expand All @@ -187,6 +219,19 @@ export default function SearchEdit( {
onClick: () => {
setAttributes( {
buttonPosition: 'no-button',
isSearchFieldHidden: false,
} );
},
},
{
role: 'menuitemradio',
title: __( 'Button Only' ),
isActive: buttonPosition === 'button-only',
icon: buttonOnly,
onClick: () => {
setAttributes( {
buttonPosition: 'button-only',
isSearchFieldHidden: true,
} );
},
},
Expand Down Expand Up @@ -247,6 +292,7 @@ export default function SearchEdit( {
onChange={ ( event ) =>
setAttributes( { placeholder: event.target.value } )
}
ref={ searchFieldRef }
/>
);
};
Expand All @@ -268,6 +314,13 @@ export default function SearchEdit( {
? { borderRadius }
: borderProps.style ),
};
const handleButtonClick = () => {
if ( hasOnlyButton && BUTTON_BEHAVIOR_EXPAND === buttonBehavior ) {
setAttributes( {
isSearchFieldHidden: ! isSearchFieldHidden,
} );
}
};

return (
<>
Expand All @@ -281,6 +334,8 @@ export default function SearchEdit( {
? stripHTML( buttonText )
: __( 'Search' )
}
onClick={ handleButtonClick }
ref={ buttonRef }
>
<Icon icon={ search } />
</button>
Expand All @@ -297,6 +352,7 @@ export default function SearchEdit( {
onChange={ ( html ) =>
setAttributes( { buttonText: html } )
}
onClick={ handleButtonClick }
/>
) }
</>
Expand Down Expand Up @@ -516,14 +572,15 @@ export default function SearchEdit( {
} }
showHandle={ isSelected }
>
{ ( isButtonPositionInside || isButtonPositionOutside ) && (
{ ( isButtonPositionInside ||
isButtonPositionOutside ||
hasOnlyButton ) && (
<>
{ renderTextField() }
{ renderButton() }
</>
) }

{ hasOnlyButton && renderButton() }
{ hasNoButton && renderTextField() }
</ResizableBox>
</div>
Expand Down
51 changes: 31 additions & 20 deletions packages/block-library/src/search/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ function render_block_core_search( $attributes ) {
$classnames = classnames_for_block_core_search( $attributes );
$show_label = ( ! empty( $attributes['showLabel'] ) ) ? true : false;
$use_icon_button = ( ! empty( $attributes['buttonUseIcon'] ) ) ? true : false;
$show_input = ( ! empty( $attributes['buttonPosition'] ) && 'button-only' === $attributes['buttonPosition'] ) ? false : true;
$show_button = ( ! empty( $attributes['buttonPosition'] ) && 'no-button' === $attributes['buttonPosition'] ) ? false : true;
$query_params = ( ! empty( $attributes['query'] ) ) ? $attributes['query'] : array();
$input_markup = '';
Expand Down Expand Up @@ -64,24 +63,33 @@ function render_block_core_search( $attributes ) {
);
}

if ( $show_input ) {
$input_classes = array( 'wp-block-search__input' );
if ( ! $is_button_inside && ! empty( $border_color_classes ) ) {
$input_classes[] = $border_color_classes;
}
if ( ! empty( $typography_classes ) ) {
$input_classes[] = $typography_classes;
$aria_hidden = '';
$aria_expanded = '';
if ( ! empty( $attributes['buttonPosition'] ) && ! empty( $attributes['buttonBehavior'] ) ) {
if ( 'button-only' === $attributes['buttonPosition'] && 'expand-searchfield' === $attributes['buttonBehavior'] ) {
$aria_hidden = 'aria-hidden="true"';
$aria_expanded = sprintf( ' aria-expanded="false" aria-controls="wp-block-search__input-%s"', esc_attr( $input_id ) );
wp_enqueue_script( 'wp-block--search-view', plugins_url( 'search/view.min.js', __FILE__ ) );
}
$input_markup = sprintf(
'<input type="search" id="%s" class="%s" name="s" value="%s" placeholder="%s" %s required />',
$input_id,
esc_attr( implode( ' ', $input_classes ) ),
get_search_query(),
esc_attr( $attributes['placeholder'] ),
$inline_styles['input']
);
}

$input_classes = array( 'wp-block-search__input' );
if ( ! $is_button_inside && ! empty( $border_color_classes ) ) {
$input_classes[] = $border_color_classes;
}
if ( ! empty( $typography_classes ) ) {
$input_classes[] = $typography_classes;
}
$input_markup = sprintf(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to do this using the WP_HTML_Tag_Processor. There's an example of this in https://github.com/WordPress/gutenberg/pull/49212/files. Not necessary to get his merged, but a nice improvement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, happy to try that in a follow up.

'<input type="search" id="%s" class="%s" name="s" value="%s" placeholder="%s" %s required %s/>',
$input_id,
esc_attr( implode( ' ', $input_classes ) ),
get_search_query(),
esc_attr( $attributes['placeholder'] ),
$inline_styles['input'],
$aria_hidden
);

if ( count( $query_params ) > 0 ) {
foreach ( $query_params as $param => $value ) {
$query_params_markup .= sprintf(
Expand Down Expand Up @@ -122,11 +130,12 @@ function render_block_core_search( $attributes ) {

// Include the button element class.
$button_classes[] = wp_theme_get_element_class_name( 'button' );
$aria_attributes = $aria_label .= $aria_expanded;
$button_markup = sprintf(
'<button type="submit" class="%s" %s %s>%s</button>',
esc_attr( implode( ' ', $button_classes ) ),
$inline_styles['button'],
$aria_label,
$aria_attributes,
$button_internal_markup
);
}
Expand Down Expand Up @@ -188,6 +197,9 @@ function classnames_for_block_core_search( $attributes ) {

if ( 'button-only' === $attributes['buttonPosition'] ) {
$classnames[] = 'wp-block-search__button-only';
if ( ! empty( $attributes['buttonBehavior'] ) && 'expand-searchfield' === $attributes['buttonBehavior'] ) {
$classnames[] = 'wp-block-search__button-behavior-expand wp-block-search__searchfield-hidden';
}
}
}

Expand Down Expand Up @@ -294,10 +306,9 @@ function styles_for_block_core_search( $attributes ) {
$show_label = ( isset( $attributes['showLabel'] ) ) && false !== $attributes['showLabel'];

// Add width styles.
$has_width = ! empty( $attributes['width'] ) && ! empty( $attributes['widthUnit'] );
$button_only = ! empty( $attributes['buttonPosition'] ) && 'button-only' === $attributes['buttonPosition'];
$has_width = ! empty( $attributes['width'] ) && ! empty( $attributes['widthUnit'] );

if ( $has_width && ! $button_only ) {
if ( $has_width ) {
$wrapper_styles[] = sprintf(
'width: %d%s;',
esc_attr( $attributes['width'] ),
Expand Down
39 changes: 39 additions & 0 deletions packages/block-library/src/search/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,42 @@ $button-spacing-y: math.div($grid-unit-15, 2); // 6px
.wp-block-search.aligncenter .wp-block-search__inside-wrapper {
margin: auto;
}

.wp-block-search__button-behavior-expand {
.wp-block-search__inside-wrapper {
transition-property: width;
min-width: 0 !important;
}

.wp-block-search__input {
transition-duration: 300ms;
flex-basis: 100%;
}

// !important here to override inline styles on button only deselected view.
&.wp-block-search__searchfield-hidden {
overflow: hidden;

.wp-block-search__inside-wrapper {
overflow: hidden;
}

.wp-block-search__input {
width: 0 !important;
min-width: 0 !important;
padding-left: 0 !important;
padding-right: 0 !important;
border-left-width: 0 !important;
border-right-width: 0 !important;
flex-grow: 0;
margin: 0;
flex-basis: 0;
}
}
}

.wp-block[data-align="right"] .wp-block-search__button-behavior-expand {
.wp-block-search__inside-wrapper {
float: right;
}
}
56 changes: 56 additions & 0 deletions packages/block-library/src/search/view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
window.addEventListener( 'DOMContentLoaded', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this could use the new interactivity API cc @gziolo

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it definitely could. We will run the experiment for Interactivity API until everything is properly tested so it's fine to have two concurrent solutions for 2-3 Gutenberg plugin releases 👍🏻

const hiddenClass = 'wp-block-search__searchfield-hidden';

Array.from(
document.getElementsByClassName(
'wp-block-search__button-behavior-expand'
)
).forEach( ( block ) => {
const searchField = block.querySelector( '.wp-block-search__input' );
const searchButton = block.querySelector( '.wp-block-search__button' );
const activeElement = block.ownerDocument.activeElement;

const toggleSearchField = ( showSearchField ) => {
if ( showSearchField ) {
searchField.setAttribute( 'aria-hidden', 'false' );
searchButton.setAttribute( 'aria-expanded', 'true' );

return block.classList.remove( hiddenClass );
}

searchField.setAttribute( 'aria-hidden', 'true' );
searchButton.setAttribute( 'aria-expanded', 'false' );
return block.classList.add( hiddenClass );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also toggle a few additional attributes for accessibility purposes here.

When the search text input is hidden, we could add:

  • aria-hidden="true" to the text input. It's worth noting that this prevents focus, but the element is hidden. I'm not sure what the best practice would be here.
  • tabindex="-1" also to the text input, so that the hidden input cannot be tabbed to
  • aria-expanded="false" and aria-controls="wp-block-search__input-[element-ID]" to the search button (the element-ID would be the ID assigned when the search block is inserted). More info about aria-expanded.
  • Possibly also consider adding an aria-label="Expand/show search input", so that it's clear what the button will do

When the text input is visible:

  • aria-hidden="false" on the text input. This allows focus again too, as well as being an accurate state.
  • Remove tabindex="-1" from the text input
  • aria-expanded="true" on the search button
  • Remove aria-label, as the button text should suffice when it's acting as a search button

I think this would help make the experience of using a mouse vs using a keyboard more consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice suggestions! I applied them here: 3a5c81a

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the closed state, the button is responsible for displaying the search form. In the open state, the button is responsible for submitting the form. This is an important distinction, as we're changing the behavior of the button, and need to communicate that. So, I think we need to modify a couple of the properties in the open state:

  • Remove aria-expanded: It's no longer controlling the hidden input, and having aria-expanded set makes screen readers announce it as Search, expanded, button, which makes it seem like pressing it again should collapse it instead of perform a search.
  • Remove aria-controls for the same reason. It's no longer controlling the element.

When the search input is hidden, I agree with @mikachan that the search button should have a label to communicate that it will open the search form.

Thanks for working on this @jffng!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jeryj — should we keep aria-expanded and aria-controls when the search input is hidden, to accurately describe the button behavior? Or is that taken care of by adding an aria-label to the button?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied this feedback in 919117f

};

const hideSearchField = ( e ) => {
if (
! e.target.closest( '.wp-block-search__inside-wrapper' ) &&
activeElement !== searchButton &&
activeElement !== searchField
) {
return toggleSearchField( false );
}

if ( e.key === 'Escape' ) {
searchButton.focus();
return toggleSearchField( false );
}
};

const handleButtonClick = ( e ) => {
if ( block.classList.contains( hiddenClass ) ) {
e.preventDefault();
searchField.focus();
toggleSearchField( true );
}
};

searchField.addEventListener( 'blur', hideSearchField );
jffng marked this conversation as resolved.
Show resolved Hide resolved
searchField.addEventListener( 'keydown', ( e ) => {
hideSearchField( e );
} );
searchButton.addEventListener( 'click', handleButtonClick );
document.body.addEventListener( 'click', hideSearchField );
} );
} );
4 changes: 3 additions & 1 deletion test/integration/fixtures/blocks/core__search.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"placeholder": "",
"buttonPosition": "button-outside",
"buttonUseIcon": false,
"query": {}
"query": {},
"buttonBehavior": "expand-searchfield",
"isSearchFieldHidden": false
},
"innerBlocks": []
}
Expand Down
Loading