diff --git a/src/wp-content/themes/twentyseventeen/searchform.php b/src/wp-content/themes/twentyseventeen/searchform.php
index 7dd83b579ed88..6196de0fa4539 100644
--- a/src/wp-content/themes/twentyseventeen/searchform.php
+++ b/src/wp-content/themes/twentyseventeen/searchform.php
@@ -12,7 +12,11 @@
-
+
+
+
+
+method="get" class="search-form" action="">
+
+
+
diff --git a/src/wp-content/themes/twentytwenty/searchform.php b/src/wp-content/themes/twentytwenty/searchform.php
index 331a366198f00..958b58b39b71c 100644
--- a/src/wp-content/themes/twentytwenty/searchform.php
+++ b/src/wp-content/themes/twentytwenty/searchform.php
@@ -22,8 +22,13 @@
if ( empty( $twentytwenty_aria_label ) && ! empty( $args['label'] ) ) {
$twentytwenty_aria_label = 'aria-label="' . esc_attr( $args['label'] ) . '"';
}
+
+$twentytwenty_wrap_in_search = ! empty( $args['wrap_in_search'] );
?>
- method="get" class="search-form" action="">
+
+>
+
+method="get" class="search-form" action="">
+
+
+
diff --git a/src/wp-content/themes/twentytwentyone/searchform.php b/src/wp-content/themes/twentytwentyone/searchform.php
index 999fc8478cae1..ed47753e7508b 100644
--- a/src/wp-content/themes/twentytwentyone/searchform.php
+++ b/src/wp-content/themes/twentytwentyone/searchform.php
@@ -19,9 +19,17 @@
$twentytwentyone_unique_id = wp_unique_id( 'search-form-' );
$twentytwentyone_aria_label = ! empty( $args['aria_label'] ) ? 'aria-label="' . esc_attr( $args['aria_label'] ) . '"' : '';
+
+$twentytwentyone_wrap_in_search = ! empty( $args['wrap_in_search'] );
?>
- method="get" class="search-form" action="">
+
+>
+
+method="get" class="search-form" action="">
+
+
+
diff --git a/src/wp-includes/general-template.php b/src/wp-includes/general-template.php
index 34233c35b0cc3..a3fafb2e11161 100644
--- a/src/wp-includes/general-template.php
+++ b/src/wp-includes/general-template.php
@@ -227,14 +227,21 @@ function get_template_part( $slug, $name = null, $args = array() ) {
*
* @since 2.7.0
* @since 5.2.0 The `$args` array parameter was added in place of an `$echo` boolean flag.
+ * @since 7.1.0 Added the `$wrap_in_search` argument and the `search-element` theme support
+ * feature to wrap the form in a `` element.
*
* @param array $args {
* Optional. Array of display arguments.
*
- * @type bool $echo Whether to echo or return the form. Default true.
- * @type string $aria_label ARIA label for the search form. Useful to distinguish
- * multiple search forms on the same page and improve
- * accessibility. Default empty.
+ * @type bool $echo Whether to echo or return the form. Default true.
+ * @type string $aria_label ARIA label for the search form. Useful to distinguish
+ * multiple search forms on the same page and improve
+ * accessibility. Default empty.
+ * @type bool $wrap_in_search Whether to wrap the form in a semantic HTML ``
+ * landmark element and drop the now-redundant `role="search"`
+ * attribute on the form. Only applies to the 'html5' format.
+ * Defaults to true when the theme declares support for the
+ * 'search-element' feature, false otherwise.
* }
* @return void|string Void if 'echo' argument is true, search form HTML if 'echo' is false.
*/
@@ -269,8 +276,9 @@ function get_search_form( $args = array() ) {
// Defaults are to echo and to output no custom label on the form.
$defaults = array(
- 'echo' => $echo,
- 'aria_label' => '',
+ 'echo' => $echo,
+ 'aria_label' => '',
+ 'wrap_in_search' => current_theme_supports( 'search-element' ),
);
$args = wp_parse_args( $args, $defaults );
@@ -321,7 +329,26 @@ function get_search_form( $args = array() ) {
$aria_label = '';
}
- if ( 'html5' === $format ) {
+ if ( 'html5' === $format && $args['wrap_in_search'] ) {
+ /*
+ * Wrap the form in a landmark element. The implicit ARIA role
+ * of provides the search landmark, so role="search" is omitted
+ * from the form to avoid nesting two identical landmarks. Any aria-label
+ * names the landmark instead of the form.
+ */
+ $search_label = $args['aria_label'] ? ' aria-label="' . esc_attr( $args['aria_label'] ) . '"' : '';
+
+ $form = '
+
+ ' .
+ /* translators: Hidden accessibility text. */
+ _x( 'Search for:', 'label' ) .
+ '
+
+
+
+ ';
+ } elseif ( 'html5' === $format ) {
$form = '
' .
diff --git a/src/wp-includes/theme.php b/src/wp-includes/theme.php
index 07869ae61d0ff..0cf8d04b306db 100644
--- a/src/wp-includes/theme.php
+++ b/src/wp-includes/theme.php
@@ -2647,6 +2647,8 @@ function get_theme_starter_content() {
* see `WP_Theme_JSON::APPEARANCE_TOOLS_OPT_INS` for a complete list.
* @since 6.6.0 The `editor-spacing-sizes` feature was added.
* @since 7.0.0 The `html5` feature's 'script' and 'style' arguments are deprecated and unused.
+ * @since 7.1.0 The `search-element` feature wraps the core search form markup in the
+ * HTML `` landmark element.
*
* @global array $_wp_theme_features
*
@@ -2683,6 +2685,7 @@ function get_theme_starter_content() {
* - 'post-formats'
* - 'post-thumbnails'
* - 'responsive-embeds'
+ * - 'search-element'
* - 'starter-content'
* - 'title-tag'
* - 'widgets'
diff --git a/tests/phpunit/tests/general/getSearchForm.php b/tests/phpunit/tests/general/getSearchForm.php
new file mode 100644
index 0000000000000..059d7df54e816
--- /dev/null
+++ b/tests/phpunit/tests/general/getSearchForm.php
@@ -0,0 +1,202 @@
+ by default.
+ *
+ * @ticket 65288
+ */
+ public function test_html5_default_uses_form_role_search() {
+ $this->enable_html5_search_form();
+
+ $form = get_search_form( array( 'echo' => false ) );
+
+ $this->assertStringContainsString( 'assertStringNotContainsString( ' element.' );
+ }
+
+ /**
+ * Opting in should wrap the html5 form in a element and drop role="search".
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_wraps_form_and_drops_role() {
+ $this->enable_html5_search_form();
+
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'wrap_in_search' => true,
+ )
+ );
+
+ $this->assertStringContainsString( '', $form, 'The opted-in form should open a element.' );
+ $this->assertStringContainsString( ' ', $form, 'The opted-in form should close the element.' );
+ $this->assertStringContainsString( 'assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped to avoid a nested duplicate landmark.' );
+ }
+
+ /**
+ * A custom aria_label should name the landmark, not the inner form.
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_applies_aria_label_to_search_element() {
+ $this->enable_html5_search_form();
+
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'wrap_in_search' => true,
+ 'aria_label' => 'Search products',
+ )
+ );
+
+ $this->assertStringContainsString( '', $form, 'The aria-label should be applied to the element.' );
+ $this->assertStringContainsString( 'assertStringNotContainsString( ' element should have no attributes.
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_without_aria_label_has_no_attributes() {
+ $this->enable_html5_search_form();
+
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'wrap_in_search' => true,
+ )
+ );
+
+ $this->assertStringContainsString( '', $form, 'The element should have no attributes when no aria-label is set.' );
+ $this->assertStringNotContainsString( '', $form, 'The element should not contain a stray space.' );
+ }
+
+ /**
+ * The default html5 form should still apply a custom aria_label to the form.
+ *
+ * @ticket 65288
+ */
+ public function test_html5_default_applies_aria_label_to_form() {
+ $this->enable_html5_search_form();
+
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'aria_label' => 'Search the site',
+ )
+ );
+
+ $this->assertStringContainsString( 'assertStringNotContainsString( ' element.' );
+ }
+
+ /**
+ * The wrap_in_search argument should not affect the xhtml format.
+ *
+ * The element does not exist in XHTML 1.x, so themes without html5
+ * 'search-form' support should continue to receive the unchanged xhtml markup.
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_is_ignored_for_xhtml_format() {
+ // No html5 theme support added: the format defaults to xhtml.
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'wrap_in_search' => true,
+ )
+ );
+
+ $this->assertStringNotContainsString( ' element.' );
+ $this->assertStringContainsString( 'assertStringContainsString( 'id="searchform"', $form, 'The xhtml markup should be unchanged.' );
+ }
+
+ /**
+ * The wrapping should be enableable globally via the search_form_args filter.
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_can_be_enabled_via_filter() {
+ $this->enable_html5_search_form();
+
+ add_filter(
+ 'search_form_args',
+ static function ( $args ) {
+ $args['wrap_in_search'] = true;
+ return $args;
+ }
+ );
+
+ $form = get_search_form( array( 'echo' => false ) );
+
+ $this->assertStringContainsString( '', $form, 'The search_form_args filter should be able to enable wrapping.' );
+ $this->assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped when wrapping is enabled via filter.' );
+ }
+
+ /**
+ * Declaring 'search-element' theme support should wrap the form by default.
+ *
+ * @ticket 65288
+ */
+ public function test_search_element_theme_support_enables_wrap_by_default() {
+ $this->enable_html5_search_form();
+ add_theme_support( 'search-element' );
+
+ $form = get_search_form( array( 'echo' => false ) );
+
+ $this->assertStringContainsString( '', $form, 'Declaring search-element support should wrap the form by default.' );
+ $this->assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped when search-element support is declared.' );
+ }
+
+ /**
+ * An explicit wrap_in_search => false should override 'search-element' theme support.
+ *
+ * @ticket 65288
+ */
+ public function test_wrap_in_search_false_overrides_theme_support() {
+ $this->enable_html5_search_form();
+ add_theme_support( 'search-element' );
+
+ $form = get_search_form(
+ array(
+ 'echo' => false,
+ 'wrap_in_search' => false,
+ )
+ );
+
+ $this->assertStringNotContainsString( 'assertStringContainsString( '