Skip to content

Commit 540ac7a

Browse files
General: Add opt-in <search> element support to the core search form.
The HTML `<search>` element is in Baseline and carries an implicit ARIA landmark role of `search`, expressing natively what `get_search_form()` currently does with a manual `role="search"` attribute. Adopting it unconditionally is not back-compatible: wrapping the `<form>` in a new element breaks direct-child CSS selectors (e.g. `.search-container > form`) and reparents the form for flex/grid layouts, dropping `role="search"` breaks `form[role="search"]` selectors, and keeping the role alongside the wrapper produces nested, double-announced search landmarks. Make the wrapper opt in instead: - Add a `search-element` theme support feature. Themes that have audited their CSS declare `add_theme_support( 'search-element' )` to enable it. - Add a `wrap_in_search` argument to `get_search_form()`, defaulting to `current_theme_supports( 'search-element' )`. The `search_form_args` filter can toggle it site-wide, and a per-call value overrides both. When enabled (html5 format only), the form is wrapped in `<search>`, `role="search"` is dropped to avoid a nested landmark, and any `aria_label` names the `<search>` element. The default output, the xhtml fallback, and the bundled classic themes are left unchanged. Add unit tests covering the default and wrapped markup, aria-label placement, the xhtml fallback, the filter, and the theme-support default. See #65288.
1 parent ac5e1ac commit 540ac7a

3 files changed

Lines changed: 239 additions & 7 deletions

File tree

src/wp-includes/general-template.php

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -227,14 +227,21 @@ function get_template_part( $slug, $name = null, $args = array() ) {
227227
*
228228
* @since 2.7.0
229229
* @since 5.2.0 The `$args` array parameter was added in place of an `$echo` boolean flag.
230+
* @since 7.1.0 Added the `$wrap_in_search` argument and the `search-element` theme support
231+
* feature to wrap the form in a `<search>` element.
230232
*
231233
* @param array $args {
232234
* Optional. Array of display arguments.
233235
*
234-
* @type bool $echo Whether to echo or return the form. Default true.
235-
* @type string $aria_label ARIA label for the search form. Useful to distinguish
236-
* multiple search forms on the same page and improve
237-
* accessibility. Default empty.
236+
* @type bool $echo Whether to echo or return the form. Default true.
237+
* @type string $aria_label ARIA label for the search form. Useful to distinguish
238+
* multiple search forms on the same page and improve
239+
* accessibility. Default empty.
240+
* @type bool $wrap_in_search Whether to wrap the form in a semantic HTML `<search>`
241+
* landmark element and drop the now-redundant `role="search"`
242+
* attribute on the form. Only applies to the 'html5' format.
243+
* Defaults to true when the theme declares support for the
244+
* 'search-element' feature, false otherwise.
238245
* }
239246
* @return void|string Void if 'echo' argument is true, search form HTML if 'echo' is false.
240247
*/
@@ -269,8 +276,9 @@ function get_search_form( $args = array() ) {
269276

270277
// Defaults are to echo and to output no custom label on the form.
271278
$defaults = array(
272-
'echo' => $echo,
273-
'aria_label' => '',
279+
'echo' => $echo,
280+
'aria_label' => '',
281+
'wrap_in_search' => current_theme_supports( 'search-element' ),
274282
);
275283

276284
$args = wp_parse_args( $args, $defaults );
@@ -321,7 +329,26 @@ function get_search_form( $args = array() ) {
321329
$aria_label = '';
322330
}
323331

324-
if ( 'html5' === $format ) {
332+
if ( 'html5' === $format && $args['wrap_in_search'] ) {
333+
/*
334+
* Wrap the form in a <search> landmark element. The implicit ARIA role
335+
* of <search> provides the search landmark, so role="search" is omitted
336+
* from the form to avoid nesting two identical landmarks. Any aria-label
337+
* names the <search> landmark instead of the form.
338+
*/
339+
$search_label = $args['aria_label'] ? ' aria-label="' . esc_attr( $args['aria_label'] ) . '"' : '';
340+
341+
$form = '<search' . $search_label . '><form method="get" class="search-form" action="' . esc_url( home_url( '/' ) ) . '">
342+
<label>
343+
<span class="screen-reader-text">' .
344+
/* translators: Hidden accessibility text. */
345+
_x( 'Search for:', 'label' ) .
346+
'</span>
347+
<input type="search" class="search-field" placeholder="' . esc_attr_x( 'Search &hellip;', 'placeholder' ) . '" value="' . get_search_query() . '" name="s" />
348+
</label>
349+
<input type="submit" class="search-submit" value="' . esc_attr_x( 'Search', 'submit button' ) . '" />
350+
</form></search>';
351+
} elseif ( 'html5' === $format ) {
325352
$form = '<form role="search" ' . $aria_label . 'method="get" class="search-form" action="' . esc_url( home_url( '/' ) ) . '">
326353
<label>
327354
<span class="screen-reader-text">' .

src/wp-includes/theme.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2647,6 +2647,8 @@ function get_theme_starter_content() {
26472647
* see `WP_Theme_JSON::APPEARANCE_TOOLS_OPT_INS` for a complete list.
26482648
* @since 6.6.0 The `editor-spacing-sizes` feature was added.
26492649
* @since 7.0.0 The `html5` feature's 'script' and 'style' arguments are deprecated and unused.
2650+
* @since 7.1.0 The `search-element` feature wraps the core search form markup in the
2651+
* HTML `<search>` landmark element.
26502652
*
26512653
* @global array $_wp_theme_features
26522654
*
@@ -2683,6 +2685,7 @@ function get_theme_starter_content() {
26832685
* - 'post-formats'
26842686
* - 'post-thumbnails'
26852687
* - 'responsive-embeds'
2688+
* - 'search-element'
26862689
* - 'starter-content'
26872690
* - 'title-tag'
26882691
* - 'widgets'
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
<?php
2+
/**
3+
* Tests for the get_search_form() function.
4+
*
5+
* @since 7.1.0
6+
*
7+
* @group general
8+
* @group template
9+
*
10+
* @covers ::get_search_form
11+
*/
12+
class Tests_General_GetSearchForm extends WP_UnitTestCase {
13+
14+
/**
15+
* Removes any theme support added during a test.
16+
*/
17+
public function tear_down() {
18+
remove_theme_support( 'html5' );
19+
remove_theme_support( 'search-element' );
20+
parent::tear_down();
21+
}
22+
23+
/**
24+
* Enables the html5 'search-form' theme support so the html5 markup is used.
25+
*/
26+
private function enable_html5_search_form() {
27+
add_theme_support( 'html5', array( 'search-form' ) );
28+
}
29+
30+
/**
31+
* The html5 form should keep role="search" on the <form> by default.
32+
*
33+
* @ticket 65288
34+
*/
35+
public function test_html5_default_uses_form_role_search() {
36+
$this->enable_html5_search_form();
37+
38+
$form = get_search_form( array( 'echo' => false ) );
39+
40+
$this->assertStringContainsString( '<form role="search"', $form, 'The default html5 form should keep role="search" on the form.' );
41+
$this->assertStringNotContainsString( '<search', $form, 'The default html5 form should not be wrapped in a <search> element.' );
42+
}
43+
44+
/**
45+
* Opting in should wrap the html5 form in a <search> element and drop role="search".
46+
*
47+
* @ticket 65288
48+
*/
49+
public function test_wrap_in_search_wraps_form_and_drops_role() {
50+
$this->enable_html5_search_form();
51+
52+
$form = get_search_form(
53+
array(
54+
'echo' => false,
55+
'wrap_in_search' => true,
56+
)
57+
);
58+
59+
$this->assertStringContainsString( '<search>', $form, 'The opted-in form should open a <search> element.' );
60+
$this->assertStringContainsString( '</search>', $form, 'The opted-in form should close the <search> element.' );
61+
$this->assertStringContainsString( '<form method="get" class="search-form"', $form, 'The inner form markup should be preserved.' );
62+
$this->assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped to avoid a nested duplicate landmark.' );
63+
}
64+
65+
/**
66+
* A custom aria_label should name the <search> landmark, not the inner form.
67+
*
68+
* @ticket 65288
69+
*/
70+
public function test_wrap_in_search_applies_aria_label_to_search_element() {
71+
$this->enable_html5_search_form();
72+
73+
$form = get_search_form(
74+
array(
75+
'echo' => false,
76+
'wrap_in_search' => true,
77+
'aria_label' => 'Search products',
78+
)
79+
);
80+
81+
$this->assertStringContainsString( '<search aria-label="Search products">', $form, 'The aria-label should be applied to the <search> element.' );
82+
$this->assertStringContainsString( '<form method="get"', $form, 'The inner form should not carry the aria-label.' );
83+
$this->assertStringNotContainsString( '<form method="get" aria-label', $form, 'The aria-label should not appear on the inner form.' );
84+
}
85+
86+
/**
87+
* Without a custom aria_label, the <search> element should have no attributes.
88+
*
89+
* @ticket 65288
90+
*/
91+
public function test_wrap_in_search_without_aria_label_has_no_attributes() {
92+
$this->enable_html5_search_form();
93+
94+
$form = get_search_form(
95+
array(
96+
'echo' => false,
97+
'wrap_in_search' => true,
98+
)
99+
);
100+
101+
$this->assertStringContainsString( '<search>', $form, 'The <search> element should have no attributes when no aria-label is set.' );
102+
$this->assertStringNotContainsString( '<search >', $form, 'The <search> element should not contain a stray space.' );
103+
}
104+
105+
/**
106+
* The default html5 form should still apply a custom aria_label to the form.
107+
*
108+
* @ticket 65288
109+
*/
110+
public function test_html5_default_applies_aria_label_to_form() {
111+
$this->enable_html5_search_form();
112+
113+
$form = get_search_form(
114+
array(
115+
'echo' => false,
116+
'aria_label' => 'Search the site',
117+
)
118+
);
119+
120+
$this->assertStringContainsString( '<form role="search" aria-label="Search the site"', $form, 'The aria-label should be applied to the form by default.' );
121+
$this->assertStringNotContainsString( '<search', $form, 'The default form should not be wrapped in a <search> element.' );
122+
}
123+
124+
/**
125+
* The wrap_in_search argument should not affect the xhtml format.
126+
*
127+
* The <search> element does not exist in XHTML 1.x, so themes without html5
128+
* 'search-form' support should continue to receive the unchanged xhtml markup.
129+
*
130+
* @ticket 65288
131+
*/
132+
public function test_wrap_in_search_is_ignored_for_xhtml_format() {
133+
// No html5 theme support added: the format defaults to xhtml.
134+
$form = get_search_form(
135+
array(
136+
'echo' => false,
137+
'wrap_in_search' => true,
138+
)
139+
);
140+
141+
$this->assertStringNotContainsString( '<search', $form, 'The xhtml format should not use the <search> element.' );
142+
$this->assertStringContainsString( '<form role="search"', $form, 'The xhtml format should keep role="search" on the form.' );
143+
$this->assertStringContainsString( 'id="searchform"', $form, 'The xhtml markup should be unchanged.' );
144+
}
145+
146+
/**
147+
* The wrapping should be enableable globally via the search_form_args filter.
148+
*
149+
* @ticket 65288
150+
*/
151+
public function test_wrap_in_search_can_be_enabled_via_filter() {
152+
$this->enable_html5_search_form();
153+
154+
add_filter(
155+
'search_form_args',
156+
static function ( $args ) {
157+
$args['wrap_in_search'] = true;
158+
return $args;
159+
}
160+
);
161+
162+
$form = get_search_form( array( 'echo' => false ) );
163+
164+
$this->assertStringContainsString( '<search>', $form, 'The search_form_args filter should be able to enable wrapping.' );
165+
$this->assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped when wrapping is enabled via filter.' );
166+
}
167+
168+
/**
169+
* Declaring 'search-element' theme support should wrap the form by default.
170+
*
171+
* @ticket 65288
172+
*/
173+
public function test_search_element_theme_support_enables_wrap_by_default() {
174+
$this->enable_html5_search_form();
175+
add_theme_support( 'search-element' );
176+
177+
$form = get_search_form( array( 'echo' => false ) );
178+
179+
$this->assertStringContainsString( '<search>', $form, 'Declaring search-element support should wrap the form by default.' );
180+
$this->assertStringNotContainsString( 'role="search"', $form, 'role="search" should be dropped when search-element support is declared.' );
181+
}
182+
183+
/**
184+
* An explicit wrap_in_search => false should override 'search-element' theme support.
185+
*
186+
* @ticket 65288
187+
*/
188+
public function test_wrap_in_search_false_overrides_theme_support() {
189+
$this->enable_html5_search_form();
190+
add_theme_support( 'search-element' );
191+
192+
$form = get_search_form(
193+
array(
194+
'echo' => false,
195+
'wrap_in_search' => false,
196+
)
197+
);
198+
199+
$this->assertStringNotContainsString( '<search', $form, 'An explicit false should override search-element theme support.' );
200+
$this->assertStringContainsString( '<form role="search"', $form, 'The form should keep role="search" when wrapping is explicitly disabled.' );
201+
}
202+
}

0 commit comments

Comments
 (0)