Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.97% covered (success)
96.97%
96 / 99
n/a
0 / 0
CRAP
n/a
0 / 0
1<?php
2/**
3 * Results Sort block render.
4 *
5 * WordPress passes `$attributes` in at runtime; VariableAnalysis can't see
6 * that because this file is include()'d rather than declared as a callback
7 * parameter, hence the sniff disable.
8 *
9 * @package automattic/jetpack-search
10 */
11
12namespace Automattic\Jetpack\Search;
13
14if ( ! defined( 'ABSPATH' ) ) {
15    exit( 0 );
16}
17
18// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
19
20// @phan-suppress-next-line PhanUndeclaredGlobalVariable -- WP always supplies $attributes.
21$attrs         = (array) $attributes;
22$options       = Results_Sort::resolve_available_options( $attrs );
23$option_labels = Results_Sort::get_option_labels();
24$default_sort  = Results_Sort::normalize_default_sort( $attrs );
25$display_as    = Results_Sort::normalize_display_as( $attrs );
26$label         = Results_Sort::resolve_label( $attrs );
27
28// Determine the effective sort for first paint and hydration. A URL
29// `?orderby=...` always wins so deep links keep their meaning -- same
30// precedence as the instant-search overlay. When no URL sort is present,
31// fall back to the block's `defaultSort` attribute.
32$url_sort       = Results_Sort::parse_url_sort( $options );
33$effective_sort = $url_sort ?? $default_sort;
34if ( ! in_array( $effective_sort, $options, true ) ) {
35    // `defaultSort` may land outside `availableSortOptions` (e.g. an
36    // author saved a default, then unchecked it from the list). Rather
37    // than render a control that can't represent the current state,
38    // pick the first allowed option so the selection is always a valid
39    // choice.
40    $effective_sort = $options[0];
41}
42
43// `wp_interactivity_data_wp_context()` (used in the radio template) was
44// introduced in WP 6.5; calling it on older cores would fatal. Fall back
45// to the `select` variant -- its template only emits inert
46// `data-wp-bind--*` attributes, which degrade harmlessly without
47// Interactivity.
48if ( 'radio' === $display_as && ! function_exists( 'wp_interactivity_data_wp_context' ) ) {
49    $display_as = 'select';
50}
51
52// Seed the shared Interactivity state with the resolved sort so the JS
53// store hydrates against the value the server rendered. Whether this seed
54// "wins" depends on theme type: under classic themes the block renders
55// during `wp_enqueue_scripts`, after the Search_Blocks state seeder, so
56// `wp_interactivity_state()`'s deep-merge layers on top. Under FSE/block
57// themes the block template renders before `wp_enqueue_scripts` fires --
58// the Search_Blocks seeder later overwrites this `sortOrder`, falling
59// back to `relevance` for any sort key it doesn't recognise. Resolving
60// the FSE precedence is tracked separately; this call still does the
61// right thing under classic themes and is harmless under FSE.
62if ( function_exists( 'wp_interactivity_state' ) ) {
63    wp_interactivity_state( 'jetpack-search', array( 'sortOrder' => $effective_sort ) );
64}
65
66$select_id = wp_unique_id( 'jetpack-search-results-sort-' );
67$menu_id   = wp_unique_id( 'jetpack-search-results-sort-menu-' );
68?>
69<?php if ( 'popover' === $display_as ) : ?>
70    <div
71        <?php echo wp_kses_data( get_block_wrapper_attributes( array( 'class' => 'jetpack-search-results-sort--popover' ) ) ); ?>
72        data-wp-interactive="jetpack-search"
73        data-jetpack-search-popover-root
74        data-wp-on-window--click="actions.onWindowClickClosePopovers"
75        data-wp-on-window--keydown="actions.onEscapeClosePopovers"
76    >
77        <button
78            type="button"
79            class="jetpack-search-results-sort__trigger"
80            aria-haspopup="menu"
81            aria-expanded="false"
82            data-wp-bind--aria-expanded="state.isSortPopoverOpen"
83            disabled
84            data-wp-bind--disabled="state.isSortTriggerDisabled"
85            aria-controls="<?php echo esc_attr( $menu_id ); ?>"
86            data-wp-on--click="actions.toggleSortPopover"
87            data-wp-on--keydown="actions.onSortTriggerKeydown"
88        >
89            <svg class="jetpack-search-results-sort__icon" width="18" height="18" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
90                <path fill="currentColor" d="M8 4l-4 4h3v12h2V8h3L8 4zm8 16l4-4h-3V4h-2v12h-3l4 4z"/>
91            </svg>
92            <span class="screen-reader-text"><?php esc_html_e( 'Sort results', 'jetpack-search-pkg' ); ?></span>
93        </button>
94        <div
95            id="<?php echo esc_attr( $menu_id ); ?>"
96            class="jetpack-search-results-sort__menu"
97            role="menu"
98            data-wp-bind--hidden="!state.isSortPopoverOpen"
99            hidden
100        >
101            <?php foreach ( $options as $sort_key ) : ?>
102                <?php $option_label = $option_labels[ $sort_key ] ?? $sort_key; ?>
103                <button
104                    type="button"
105                    role="menuitemradio"
106                    class="jetpack-search-results-sort__menu-item"
107                    value="<?php echo esc_attr( $sort_key ); ?>"
108                    tabindex="-1"
109                    data-wp-context='<?php echo esc_attr( wp_json_encode( array( 'sortKey' => $sort_key ), JSON_HEX_AMP | JSON_UNESCAPED_SLASHES ) ); ?>'
110                    data-wp-bind--aria-checked="state.isSortOptionSelected"
111                    data-wp-bind--tabindex="state.sortMenuItemTabIndex"
112                    data-wp-on--click="actions.selectSortOrder"
113                    data-wp-on--keydown="actions.onSortMenuKeydown"
114                    data-wp-watch="callbacks.focusSelectedSortMenuItem"
115                >
116                    <?php echo esc_html( $option_label ); ?>
117                </button>
118            <?php endforeach; ?>
119        </div>
120    </div>
121<?php else : ?>
122    <div
123        <?php echo wp_kses_data( get_block_wrapper_attributes() ); ?>
124        data-wp-interactive="jetpack-search"
125    >
126        <?php if ( 'radio' === $display_as ) : ?>
127            <?php
128            // Shared `name` groups the radios so the browser enforces single-
129            // selection semantics across the whole block instance. The wrapper's
130            // uniquely generated id doubles as the group name -- two results-sort
131            // blocks on the same page therefore get distinct names and don't
132            // interfere with each other.
133            $group_name = $select_id;
134            ?>
135            <fieldset class="jetpack-search-results-sort__radio-group">
136                <legend><?php echo esc_html( $label ); ?></legend>
137                <?php foreach ( $options as $sort_key ) : ?>
138                    <?php
139                    $option_label = $option_labels[ $sort_key ] ?? $sort_key;
140                    $radio_id     = $select_id . '-' . sanitize_key( $sort_key );
141                    ?>
142                    <div
143                        class="jetpack-search-results-sort__radio-item"
144                        <?php echo wp_kses_data( wp_interactivity_data_wp_context( array( 'sortKey' => $sort_key ) ) ); ?>
145                    >
146                        <input
147                            type="radio"
148                            id="<?php echo esc_attr( $radio_id ); ?>"
149                            name="<?php echo esc_attr( $group_name ); ?>"
150                            value="<?php echo esc_attr( $sort_key ); ?>"
151                            <?php checked( $effective_sort, $sort_key ); ?>
152                            data-wp-bind--checked="state.isSortOptionSelected"
153                            data-wp-on--change="actions.onSortChange"
154                        />
155                        <label for="<?php echo esc_attr( $radio_id ); ?>">
156                            <?php echo esc_html( $option_label ); ?>
157                        </label>
158                    </div>
159                <?php endforeach; ?>
160            </fieldset>
161        <?php else : ?>
162            <label for="<?php echo esc_attr( $select_id ); ?>">
163                <?php echo esc_html( $label ); ?>
164            </label>
165            <select
166                id="<?php echo esc_attr( $select_id ); ?>"
167                data-wp-bind--value="state.sortOrder"
168                data-wp-on--change="actions.onSortChange"
169            >
170                <?php foreach ( $options as $sort_key ) : ?>
171                    <?php $option_label = $option_labels[ $sort_key ] ?? $sort_key; ?>
172                    <option
173                        value="<?php echo esc_attr( $sort_key ); ?>"
174                        <?php selected( $effective_sort, $sort_key ); ?>
175                    ><?php echo esc_html( $option_label ); ?></option>
176                <?php endforeach; ?>
177            </select>
178        <?php endif; ?>
179    </div>
180<?php endif; ?>