Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.65% covered (success)
99.65%
282 / 283
n/a
0 / 0
CRAP
n/a
0 / 0
1<?php
2/**
3 * Results List block render.
4 *
5 * Renders three sibling regions inside a single block wrapper:
6 *  - the results list (skeleton while loading, then live results),
7 *  - the legacy empty-state message (gated by `state.showLegacyNoResults`,
8 *    so it stands down for pages carrying a `no-results` block),
9 *  - the legacy error message (gated by `state.showLegacyError`).
10 *
11 * The store's existing visibility flags ensure exactly one message is
12 * visible at a time, so the regions can coexist without extra wiring.
13 *
14 * @package automattic/jetpack-search
15 */
16
17namespace Automattic\Jetpack\Search;
18
19if ( ! defined( 'ABSPATH' ) ) {
20    exit( 0 );
21}
22
23// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
24
25/**
26 * Per-layout feature flags driving the SSR template below. Edit.js renders
27 * each layout as its own explicit JSX template (no shared feature-flag
28 * map) — the JS preview prioritizes readability over DRY since each
29 * sample list is short. PHP keeps the flag-table form because the
30 * Interactivity-bound DOM is identical across layouts and only the
31 * `<?php if ?>` gates around image / path / price / rating / date
32 * sections differ.
33 *
34 * @param string $layout Layout key.
35 * @return array{modifier:string, show_author:bool, show_content:bool, show_date:bool, show_image:bool, show_path:bool, show_price:bool, show_rating:bool}
36 */
37$resolve_layout = static function ( $layout ) {
38    $map = array(
39        'compact'  => array(
40            'modifier'     => 'compact',
41            'show_image'   => false,
42            'show_path'    => false,
43            'show_content' => false,
44            'show_author'  => false,
45            'show_date'    => true,
46            'show_price'   => false,
47            'show_rating'  => false,
48        ),
49        'expanded' => array(
50            'modifier'     => 'expanded',
51            'show_image'   => true,
52            'show_path'    => true,
53            'show_content' => true,
54            'show_author'  => true,
55            'show_date'    => true,
56            'show_price'   => false,
57            'show_rating'  => false,
58        ),
59        'product'  => array(
60            'modifier'     => 'product',
61            'show_image'   => true,
62            'show_path'    => false,
63            'show_content' => false,
64            'show_author'  => false,
65            'show_date'    => false,
66            'show_price'   => true,
67            'show_rating'  => true,
68        ),
69    );
70    return $map[ $layout ] ?? $map['expanded'];
71};
72
73// @phan-suppress-next-line PhanUndeclaredGlobalVariable
74$attrs  = (array) $attributes;
75$layout = $attrs['layout'] ?? 'expanded';
76// Pre-rename block markup used `card` for what is now `expanded`. Promote
77// the legacy value so saved content keeps rendering correctly instead of
78// falling through `$resolve_layout`'s unknown-layout fallback. Mirrors the
79// JS-side normalization in edit.js.
80if ( 'card' === $layout ) {
81    $layout = 'expanded';
82}
83// The product layout reads WC-shaped fields (price, sale price, rating)
84// off each result. On a non-Woo site those fields don't exist, so
85// rendering would emit empty price/rating regions. Collapse to the
86// neutral `expanded` layout so an author who saved `product` on a Woo
87// site that later deactivates WC still sees a sensible result page.
88// Mirrors the inspector-side gate in edit.js.
89if ( 'product' === $layout && ! Search_Blocks::woocommerce_blocks_enabled() ) {
90    $layout = 'expanded';
91}
92$features      = $resolve_layout( $layout );
93$wrapper_class = 'jetpack-search-results--' . $features['modifier'];
94$wrapper_attrs = get_block_wrapper_attributes( array( 'class' => $wrapper_class ) );
95
96// Hand the resolved layout to the store so the TrainTracks `ui_algo` matches
97// what visitors actually see. Deep-merges onto the global seed.
98if ( function_exists( 'wp_interactivity_state' ) ) {
99    wp_interactivity_state( 'jetpack-search', array( 'resultsLayout' => $layout ) );
100}
101
102// Skeleton items always render so the IA runtime can re-show them on a
103// client-side search from a bare /search/ page. The literal `hidden` keeps
104// them out of the pre-hydration paint unless the URL already carries a
105// query/filter that fires an initial fetch on hydration; afterwards
106// `data-wp-bind--hidden="state.skeletonHidden"` drives visibility reactively.
107$is_initial_loading = Search_Blocks::is_initial_loading();
108$skeleton_count     = 'compact' === $layout ? 6 : 4;
109
110// All three messages are superseded by the `jetpack-search/no-results` block,
111// which accepts any inner blocks instead of a plain string. Kept rendering for
112// saved content that predates it — `No_Results::legacy_no_results_getter()`
113// and `legacy_error_getter()` pick the binding that hides these regions
114// wherever a block covers the case, or drop them outright when the surrounding
115// markup's coverage is already known, so the two never both show.
116//
117// `trim()` so a whitespace-only attribute (e.g. an author who saved spaces)
118// still falls back to the default copy instead of rendering a blank message.
119$no_results_defaults = No_Results::default_messages();
120$no_results_message  = trim( (string) ( $attrs['noResultsMessage'] ?? '' ) );
121if ( '' === $no_results_message ) {
122    $no_results_message = $no_results_defaults['unfiltered'];
123}
124
125// Filter-aware variant — shown when `state.hasActiveFilters` is true. Both
126// variants live in the markup so the store's existing reactive getter picks
127// which `<p>` is visible without a store-side message-resolution branch.
128$no_results_with_filters_message = trim( (string) ( $attrs['noResultsWithFiltersMessage'] ?? '' ) );
129if ( '' === $no_results_with_filters_message ) {
130    $no_results_with_filters_message = $no_results_defaults['filtered'];
131}
132
133$error_message = trim( (string) ( $attrs['errorMessage'] ?? '' ) );
134if ( '' === $error_message ) {
135    $error_message = $no_results_defaults['error'];
136}
137
138$legacy_no_results_getter = No_Results::legacy_no_results_getter();
139$legacy_error_getter      = No_Results::legacy_error_getter();
140?>
141<div
142    <?php echo wp_kses_data( $wrapper_attrs ); ?>
143    data-wp-interactive="jetpack-search"
144    data-wp-init="callbacks.initialize"
145    data-wp-bind--aria-busy="state.isLoading"
146>
147    <ul
148        class="jetpack-search-results__list"
149        aria-live="polite"
150    >
151        <?php for ( $i = 0; $i < $skeleton_count; $i++ ) : ?>
152            <li
153                class="jetpack-search-results__item jetpack-search-results__item--skeleton"
154                data-wp-bind--hidden="state.skeletonHidden"
155                aria-hidden="true"
156                <?php echo $is_initial_loading ? '' : 'hidden'; ?>
157            >
158                <?php if ( 'product' === $layout ) : ?>
159                    <div class="jetpack-search-skeleton jetpack-search-skeleton--product-image"></div>
160                    <div class="jetpack-search-results__copy">
161                        <div class="jetpack-search-skeleton jetpack-search-skeleton--title"></div>
162                        <div class="jetpack-search-skeleton jetpack-search-skeleton--title-secondary"></div>
163                    </div>
164                <?php elseif ( 'compact' === $layout ) : ?>
165                    <div class="jetpack-search-results__copy">
166                        <div class="jetpack-search-skeleton jetpack-search-skeleton--title"></div>
167                    </div>
168                <?php else : ?>
169                    <div class="jetpack-search-results__copy">
170                        <div class="jetpack-search-skeleton jetpack-search-skeleton--title"></div>
171                        <div class="jetpack-search-skeleton jetpack-search-skeleton--path"></div>
172                        <div class="jetpack-search-skeleton jetpack-search-skeleton--meta"></div>
173                    </div>
174                    <div class="jetpack-search-skeleton jetpack-search-skeleton--image"></div>
175                <?php endif; ?>
176            </li>
177        <?php endfor; ?>
178        <template
179            data-wp-each--result="state.results"
180            data-wp-each-key="context.result.id"
181        >
182            <li
183                class="jetpack-search-results__item"
184                data-wp-on--click="actions.recordResultInteract"
185            >
186                <?php if ( $features['show_image'] && 'product' === $layout ) : ?>
187                    <a
188                        class="jetpack-search-results__product-image-link"
189                        data-wp-bind--href="context.result.permalink"
190                        tabindex="-1"
191                        aria-hidden="true"
192                    >
193                        <span
194                            class="jetpack-search-results__product-image"
195                            data-wp-bind--hidden="!context.result.imageUrl"
196                            data-wp-style--background-image="context.result.imageBackgroundImage"
197                        ></span>
198                        <span
199                            class="jetpack-search-results__product-image-placeholder"
200                            data-wp-bind--hidden="context.result.imageUrl"
201                        ></span>
202                    </a>
203                <?php endif; ?>
204                <div class="jetpack-search-results__copy">
205                    <h3 class="jetpack-search-results__title">
206                        <a
207                            class="jetpack-search-results__title-link"
208                            data-wp-bind--href="context.result.permalink"
209                        >
210                            <span
211                                data-wp-bind--hidden="context.result.hasTitlePieces"
212                                data-wp-text="context.result.title"
213                            ></span>
214                            <template
215                                data-wp-each--piece="context.result.titlePieces"
216                                data-wp-each-key="context.piece.index"
217                            >
218                                <span
219                                    data-wp-text="context.piece.text"
220                                    data-wp-class--jetpack-search-results__highlight="context.piece.isHighlight"
221                                ></span>
222                            </template>
223                        </a>
224                    </h3>
225                    <?php if ( $features['show_content'] ) : ?>
226                        <div
227                            class="jetpack-search-results__content"
228                            data-wp-bind--hidden="!context.result.hasContentPieces"
229                        >
230                            <template
231                                data-wp-each--piece="context.result.contentPieces"
232                                data-wp-each-key="context.piece.index"
233                            >
234                                <span
235                                    data-wp-text="context.piece.text"
236                                    data-wp-class--jetpack-search-results__highlight="context.piece.isHighlight"
237                                ></span>
238                            </template>
239                        </div>
240                    <?php endif; ?>
241                    <?php if ( $features['show_path'] ) : ?>
242                        <div
243                            class="jetpack-search-results__path"
244                            data-wp-bind--hidden="!context.result.path"
245                            data-wp-text="context.result.path"
246                        ></div>
247                    <?php endif; ?>
248                    <?php if ( $features['show_price'] ) : ?>
249                        <div
250                            class="jetpack-search-results__price"
251                            data-wp-bind--hidden="!context.result.hasPrice"
252                        >
253                            <del
254                                class="jetpack-search-results__price-regular"
255                                data-wp-bind--hidden="!context.result.hasSalePrice"
256                                data-wp-text="context.result.formattedRegularPrice"
257                            ></del>
258                            <ins
259                                class="jetpack-search-results__price-sale"
260                                data-wp-bind--hidden="!context.result.hasSalePrice"
261                                data-wp-text="context.result.formattedSalePrice"
262                            ></ins>
263                            <span
264                                class="jetpack-search-results__price-current"
265                                data-wp-bind--hidden="context.result.hasSalePrice"
266                                data-wp-text="context.result.formattedPrice"
267                            ></span>
268                        </div>
269                    <?php endif; ?>
270                    <?php if ( $features['show_rating'] ) : ?>
271                        <div
272                            class="jetpack-search-results__rating"
273                            role="img"
274                            data-wp-bind--hidden="!context.result.hasRating"
275                            data-wp-bind--aria-label="context.result.ratingAriaLabel"
276                        >
277                            <span class="jetpack-search-results__rating-stars" aria-hidden="true">
278                                <span
279                                    class="jetpack-search-results__rating-fill"
280                                    data-wp-style--width="context.result.ratingPercent"
281                                ></span>
282                            </span>
283                            <span
284                                class="jetpack-search-results__rating-count"
285                                aria-hidden="true"
286                                data-wp-text="context.result.reviewCountLabel"
287                            ></span>
288                        </div>
289                    <?php endif; ?>
290                    <?php if ( 'product' === $layout ) : ?>
291                        <div
292                            class="jetpack-search-results__match-hint"
293                            data-wp-bind--hidden="!context.result.matchHint"
294                        >
295                            <mark>
296                                <span data-wp-bind--hidden="!context.result.matchHintIsComments">
297                                    <?php esc_html_e( 'Matches comments', 'jetpack-search-pkg' ); ?>
298                                </span>
299                                <span data-wp-bind--hidden="context.result.matchHintIsComments">
300                                    <?php esc_html_e( 'Matches content', 'jetpack-search-pkg' ); ?>
301                                </span>
302                            </mark>
303                        </div>
304                    <?php endif; ?>
305                    <?php if ( $features['show_author'] || $features['show_date'] ) : ?>
306                        <div class="jetpack-search-results__meta">
307                            <?php if ( $features['show_author'] ) : ?>
308                                <span
309                                    class="jetpack-search-results__author"
310                                    data-wp-bind--hidden="!context.result.authorLabel"
311                                    data-wp-text="context.result.authorLabel"
312                                ></span>
313                            <?php endif; ?>
314                            <?php if ( $features['show_author'] && $features['show_date'] ) : ?>
315                                <span
316                                    class="jetpack-search-results__meta-separator"
317                                    aria-hidden="true"
318                                    data-wp-bind--hidden="!context.result.authorLabel || !context.result.dateLabel"
319                                >&middot;</span>
320                            <?php endif; ?>
321                            <?php if ( $features['show_date'] ) : ?>
322                                <span
323                                    class="jetpack-search-results__date"
324                                    data-wp-bind--hidden="!context.result.dateLabel"
325                                    data-wp-text="context.result.dateLabel"
326                                ></span>
327                            <?php endif; ?>
328                        </div>
329                    <?php endif; ?>
330                </div>
331                <?php if ( $features['show_image'] && 'product' !== $layout ) : ?>
332                    <a
333                        class="jetpack-search-results__image-link"
334                        data-wp-bind--href="context.result.permalink"
335                        data-wp-bind--hidden="!context.result.imageUrl"
336                        tabindex="-1"
337                        aria-hidden="true"
338                    >
339                        <img
340                            class="jetpack-search-results__image"
341                            data-wp-bind--src="context.result.imageUrl"
342                            alt=""
343                            loading="lazy"
344                        />
345                    </a>
346                <?php endif; ?>
347            </li>
348        </template>
349    </ul>
350    <?php if ( '' !== $legacy_no_results_getter ) : ?>
351    <div
352        class="jetpack-search-results__no-results"
353        data-wp-bind--hidden="!<?php echo esc_attr( $legacy_no_results_getter ); ?>"
354        role="status"
355        hidden
356    >
357        <?php
358        // Both `<p>` variants render without an initial `hidden` attribute —
359        // the outer wrapper's `hidden` covers them on the SSR path, and the IA
360        // runtime resolves the inner `data-wp-bind--hidden` atomically when it
361        // reveals the region. Don't remove the outer `hidden` without also
362        // adding initial `hidden` to one of the variants, or both messages
363        // will flash briefly on hydration.
364        ?>
365        <p data-wp-bind--hidden="state.hasActiveFilters"><?php echo esc_html( $no_results_message ); ?></p>
366        <p data-wp-bind--hidden="!state.hasActiveFilters"><?php echo esc_html( $no_results_with_filters_message ); ?></p>
367    </div>
368    <?php endif; ?>
369    <?php if ( '' !== $legacy_error_getter ) : ?>
370    <div
371        class="jetpack-search-results__error"
372        data-wp-bind--hidden="!<?php echo esc_attr( $legacy_error_getter ); ?>"
373        role="alert"
374        hidden
375    >
376        <p><?php echo esc_html( $error_message ); ?></p>
377    </div>
378    <?php endif; ?>
379</div>