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