Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
73.33% covered (warning)
73.33%
66 / 90
n/a
0 / 0
CRAP
n/a
0 / 0
1<?php
2/**
3 * Search Input block render.
4 *
5 * WordPress passes $attributes, $content, $block to render.php at runtime;
6 * VariableAnalysis can't see that, so the sniff is disabled here.
7 *
8 * @package automattic/jetpack-search
9 */
10
11namespace Automattic\Jetpack\Search;
12
13if ( ! defined( 'ABSPATH' ) ) {
14    exit( 0 );
15}
16
17// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
18
19// Trim so a whitespace-only placeholder still falls back to the translated
20// default — block.json's static default can't be localized, so the empty/
21// whitespace case resolves here.
22$placeholder = trim( (string) ( $attributes['placeholder'] ?? '' ) );
23if ( '' === $placeholder ) {
24    $placeholder = __( 'Search…', 'jetpack-search-pkg' );
25}
26$show_icon   = (bool) ( $attributes['showIcon'] ?? true );
27$submit_only = ! empty( $attributes['submitOnly'] );
28// Sanitize the saved `suggestionTypes` array down to a known-good subset of
29// the enum, in the canonical render order, deduplicated. Mirrors the
30// enum in `block.json::attributes.suggestionTypes.items.enum` and the
31// `TYPE_ORDER` constant in `suggestion-rows.js`. Authors can save anything
32// the editor lets them — and a future block-version migration could leave
33// stale strings here — so the front end stays defensive.
34$allowed_suggestion_types = array( 'query', 'taxonomy', 'post' );
35$raw_suggestion_types     = $attributes['suggestionTypes'] ?? $allowed_suggestion_types;
36if ( ! is_array( $raw_suggestion_types ) ) {
37    $raw_suggestion_types = $allowed_suggestion_types;
38}
39$suggestion_types = array_values(
40    array_intersect( $allowed_suggestion_types, array_map( 'strval', $raw_suggestion_types ) )
41);
42// `enableSuggestions` is the author's master kill switch; an empty
43// `suggestionTypes` array collapses to the same outcome (no dropdown). The
44// combined gate keeps the rest of the render path branch-free — no listbox,
45// no combobox attributes, no `data-wp-context` seeded when either says off.
46$enable_suggestions = ! empty( $attributes['enableSuggestions'] ) && ! empty( $suggestion_types );
47// Read the URL-derived query through the shared helper so the SSR
48// `value=` matches the Interactivity store's seeded `searchQuery`.
49// The helper picks `s` vs `q` based on `is_search()` and applies the
50// same sanitize_text_field + trim WP would have applied to `s`.
51$initial_query = Search_Blocks::parse_url_search_query();
52$input_id      = wp_unique_id( 'jetpack-search-input-' );
53// A separate id pair is generated for the listbox + status region so the
54// input's `aria-controls` / `aria-activedescendant` can resolve to stable
55// DOM ids. Only generated (and only emitted) when suggestions are on, to
56// keep the default DOM untouched for authors who haven't opted in.
57$listbox_id = $enable_suggestions ? wp_unique_id( 'jetpack-search-suggestions-' ) : '';
58// `data-wp-context` seeds the per-instance Interactivity state for the
59// suggestions dropdown. Keeping it per-block (rather than on the shared
60// `jetpack-search` store) means a header + sidebar Search Input on the
61// same page never share a dropdown's `showSuggestions` / `activeIndex` —
62// each instance owns its own UI state while still pulling the query from
63// the shared `state.searchQuery`. Post-type scope lives on the parent
64// `search-results` block, not here — inputs are entry points, not
65// boundaries.
66$context = $enable_suggestions
67    ? array(
68        'showSuggestions' => false,
69        'activeIndex'     => -1,
70        'activeOptionId'  => '',
71        'rows'            => array(),
72        'listboxId'       => $listbox_id,
73        // `suggestionTypes` is seeded so the view bundle can filter
74        // rows client-side without re-reading the block attribute via
75        // a roundtrip. Per-instance because two Search Input blocks
76        // on the same page could pick different shapes.
77        'suggestionTypes' => $suggestion_types,
78    )
79    : array();
80$emit_context = ! empty( $context );
81$context_json = $emit_context
82    ? wp_json_encode( $context, JSON_HEX_AMP | JSON_UNESCAPED_SLASHES )
83    : '';
84
85// Mirrors `render_block_core_search()`'s width handling: both halves of the
86// (value, unit) pair must be present, then emit `width: <n><unit>;` on the
87// wrapper. The inside-wrapper's `display:flex` + `min-width:0` on the field
88// shrinks the visible text area in step; icon + clear stay at natural width.
89// `widthUnit` is allowlisted against the same units the editor offers —
90// `block.json` only types it as a free-form string, so a REST write or a
91// future migration can't smuggle an arbitrary unit into the inline style.
92$wrapper_extra_attrs = array();
93$allowed_width_units = array( 'px', '%' );
94$raw_width_unit      = (string) ( $attributes['widthUnit'] ?? '' );
95$width_unit          = in_array( $raw_width_unit, $allowed_width_units, true ) ? $raw_width_unit : '';
96$has_width           = isset( $attributes['width'] ) && '' !== $attributes['width'] && '' !== $width_unit;
97if ( $has_width ) {
98    $wrapper_extra_attrs['style'] = sprintf(
99        'width:%d%s;',
100        (int) $attributes['width'],
101        $width_unit
102    );
103}
104?>
105<div
106    <?php echo wp_kses_data( get_block_wrapper_attributes( $wrapper_extra_attrs ) ); ?>
107    data-wp-interactive="jetpack-search"
108    <?php if ( $emit_context ) : ?>
109    data-wp-context='<?php echo esc_attr( $context_json ); ?>'
110    <?php endif; ?>
111>
112    <label class="jetpack-search-input__label screen-reader-text" for="<?php echo esc_attr( $input_id ); ?>">
113        <?php esc_html_e( 'Search', 'jetpack-search-pkg' ); ?>
114    </label>
115    <div class="jetpack-search-input__inside-wrapper">
116        <?php if ( $show_icon ) : ?>
117        <svg class="jetpack-search-input__icon" aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
118            <path d="M13 5c-3.3 0-6 2.7-6 6 0 1.4.5 2.7 1.3 3.7l-3.8 3.8 1.1 1.1 3.8-3.8c1 .8 2.3 1.3 3.7 1.3 3.3 0 6-2.7 6-6s-2.7-6-6-6zm0 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5z" />
119        </svg>
120        <?php endif; ?>
121        <input
122            id="<?php echo esc_attr( $input_id ); ?>"
123            type="search"
124            class="jetpack-search-input__field"
125            placeholder="<?php echo esc_attr( $placeholder ); ?>"
126            value="<?php echo esc_attr( $initial_query ); ?>"
127            <?php if ( $submit_only ) : ?>
128            data-submit-only="true"
129            <?php endif; ?>
130            <?php if ( $enable_suggestions ) : ?>
131            role="combobox"
132            aria-autocomplete="list"
133            aria-haspopup="listbox"
134            aria-controls="<?php echo esc_attr( $listbox_id ); ?>"
135            data-suggestions-enabled="true"
136            data-wp-bind--aria-expanded="context.showSuggestions"
137            data-wp-bind--aria-activedescendant="context.activeOptionId"
138            data-wp-on--focus="actions.onSearchFocus"
139            data-wp-on--blur="actions.onSearchBlur"
140            <?php endif; ?>
141            data-wp-bind--value="state.searchQuery"
142            data-wp-on--input="actions.onSearchInput"
143            data-wp-on--keydown="actions.onSearchKeydown"
144            data-wp-init="callbacks.initFocusInputIfHasQuery"
145        />
146        <button
147            type="button"
148            class="jetpack-search-input__clear"
149            data-wp-bind--hidden="!state.searchQuery"
150            data-wp-on--click="actions.clearSearch"
151        >
152            <?php
153            /* translators: Button is used to clear the search input query. */
154            echo esc_html__( 'clear', 'jetpack-search-pkg' );
155            ?>
156        </button>
157        <?php if ( $enable_suggestions ) : ?>
158        <ul
159            id="<?php echo esc_attr( $listbox_id ); ?>"
160            class="jetpack-search-input__suggestions"
161            role="listbox"
162            aria-label="<?php echo esc_attr__( 'Search suggestions', 'jetpack-search-pkg' ); ?>"
163            data-wp-bind--hidden="!context.showSuggestions"
164            hidden
165        >
166            <template
167                data-wp-each--row="context.rows"
168                data-wp-each-key="context.row.key"
169            >
170                <li
171                    class="jetpack-search-input__suggestions-label"
172                    role="presentation"
173                    data-wp-bind--hidden="!context.row.isHeader"
174                    data-wp-text="context.row.label"
175                ></li>
176                <li
177                    class="jetpack-search-input__suggestions-option"
178                    role="option"
179                    data-wp-bind--hidden="context.row.isHeader"
180                    data-wp-bind--id="context.row.optionId"
181                    data-wp-bind--aria-selected="state.isRowActive"
182                    data-wp-class--is-active="state.isRowActive"
183                    data-wp-on--mousedown="actions.onSuggestionMousedown"
184                    data-wp-on--click="actions.onSuggestionClick"
185                    tabindex="-1"
186                >
187                    <span data-wp-text="context.row.text"></span>
188                </li>
189            </template>
190        </ul>
191        <?php endif; ?>
192    </div>
193</div>