PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 10.9.4
WooCommerce v10.9.4
10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Internal / ShopperLists / ShopperListRenderer.php
woocommerce / src / Internal / ShopperLists Last commit date
ShopperList.php 1 month ago ShopperListItem.php 2 weeks ago ShopperListRenderer.php 1 month ago ShopperListsController.php 1 month ago
ShopperListRenderer.php
332 lines
1 <?php
2
3 declare( strict_types = 1 );
4
5 namespace Automattic\WooCommerce\Internal\ShopperLists;
6
7 /**
8 * Shared markup helpers for blocks that render a shopper-list item card
9 * (Saved for Later, Wishlist, …). Static helpers, not an abstract base —
10 * the two blocks' lifecycles diverge enough (auto-injected vs merchant-
11 * placed, different actions, different empty-state gating) that inheritance
12 * is not a clean fit. Consumers stitch the fragments together with their
13 * own quantity / action button / heading bits.
14 *
15 * Any change here is co-reviewed with every consuming block — drift in the
16 * shared row shape will break first paint for whoever didn't get the memo.
17 */
18 final class ShopperListRenderer {
19
20 /**
21 * Shared CSS root class for the row. Each section helper outputs
22 * BEM-style modifiers off this base (`__image-slot`, `__remove`, …).
23 */
24 public const ROW_CLASS = 'wc-block-shopper-list-item';
25
26 /**
27 * Wrap `$inner` in the block's outer `<section><ul>…</ul></section>`
28 * grid scaffold. `$wrapper_attrs` are merged with the block's wrapper
29 * attributes via `get_block_wrapper_attributes()`.
30 *
31 * Trust contract: callers are responsible for ensuring `$inner` and
32 * `$before_list` contain only safe, escaped HTML — typically composed
33 * from the section helpers below, never from raw schema/request input.
34 *
35 * @param array<string, mixed> $wrapper_attrs Attributes for the outer `<section>`.
36 * @param string $list_class Class attribute for the inner `<ul>`.
37 * @param string $inner Markup placed inside the `<ul>` (template + SSR rows + empty state).
38 * @param string $before_list Markup placed between `<section>` and `<ul>` (header, notices region).
39 * @return string
40 */
41 public static function render_grid_wrapper( array $wrapper_attrs, string $list_class, string $inner, string $before_list = '' ): string {
42 return sprintf(
43 '<section %1$s>%2$s<ul class="%3$s">%4$s</ul></section>',
44 get_block_wrapper_attributes( $wrapper_attrs ),
45 $before_list,
46 esc_attr( $list_class ),
47 $inner
48 );
49 }
50
51 /**
52 * Wrap `$row_inner_markup` in a `<template data-wp-each>` element that
53 * iAPI uses to render new rows. `$row_inner_markup` is the inner HTML
54 * for the `<li>` — everything between `<li>` and `</li>`.
55 *
56 * Trust contract: caller is responsible for ensuring `$row_inner_markup`
57 * contains only safe, escaped HTML.
58 *
59 * @param string $row_inner_markup Inner markup for the `<li>`.
60 * @return string
61 */
62 public static function render_each_template( string $row_inner_markup ): string {
63 return sprintf(
64 '<template data-wp-each--list-item="state.currentItems" data-wp-each-key="context.listItem.key"><li class="%1$s">%2$s</li></template>',
65 esc_attr( self::ROW_CLASS ),
66 $row_inner_markup
67 );
68 }
69
70 /**
71 * Wrap `$row_inner_markup` in an SSR `<li data-wp-each-child>` element
72 * seeded with the per-row iAPI context derived from `$item`. iAPI's
73 * hydration treats this as a no-op diff against the `<template>` if
74 * the inner markup matches.
75 *
76 * Trust contract: caller is responsible for ensuring `$row_inner_markup`
77 * contains only safe, escaped HTML.
78 *
79 * @param array<string, mixed> $item Schema-shape item.
80 * @param string $row_inner_markup Inner markup for the `<li>`.
81 * @return string
82 */
83 public static function render_each_child( array $item, string $row_inner_markup ): string {
84 $context = array( 'listItem' => $item );
85 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- wp_interactivity_data_wp_context() returns a safely-encoded attribute pair; $row_inner_markup is composed of escaped fragments from the section helpers below.
86 return sprintf(
87 '<li class="%1$s" data-wp-each-child %2$s>%3$s</li>',
88 esc_attr( self::ROW_CLASS ),
89 wp_interactivity_data_wp_context( $context ),
90 $row_inner_markup
91 );
92 }
93
94 /**
95 * Render the image + title + price triplet for the template-mode row
96 * (no static attrs; bindings only). Identical between consumer blocks.
97 *
98 * @return string
99 */
100 public static function render_template_common_row(): string {
101 ob_start();
102 ?>
103 <div class="wc-block-components-product-image wc-block-components-product-image--aspect-ratio-auto">
104 <a data-wp-bind--href="context.listItem.permalink">
105 <span class="<?php echo esc_attr( self::ROW_CLASS ); ?>__image-slot" data-wp-context='{"htmlField":"image_html"}' data-wp-watch="callbacks.updateInnerHtml"></span>
106 </a>
107 <button
108 type="button"
109 class="<?php echo esc_attr( self::ROW_CLASS ); ?>__remove"
110 data-wp-on--click="actions.onClickRemove"
111 data-wp-bind--aria-label="state.currentItemRemoveLabel"
112 data-wp-bind--disabled="state.isCurrentItemPending"
113 >
114 <?php echo self::get_remove_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?>
115 </button>
116 <span class="<?php echo esc_attr( self::ROW_CLASS ); ?>__variation" data-wp-bind--hidden="!state.currentItemVariationLabel" data-wp-text="state.currentItemVariationLabel"></span>
117 </div>
118 <h2 class="wp-block-post-title has-text-align-center has-medium-font-size">
119 <a data-wp-bind--href="context.listItem.permalink" data-wp-text="state.currentItemDisplayName"></a>
120 </h2>
121 <div class="price wc-block-components-product-price has-text-align-center has-small-font-size" data-wp-bind--hidden="state.isPriceHidden" data-wp-context='{"htmlField":"price_html"}' data-wp-watch="callbacks.updateInnerHtml"></div>
122 <?php
123 return (string) ob_get_clean();
124 }
125
126 /**
127 * Render the image + title + price triplet for the SSR-mode row, with
128 * values populated from `$item` and `$remove_aria_label_template`. The
129 * binding directives match the template-mode markup so iAPI's hydration
130 * is a no-op diff after first paint.
131 *
132 * @param array<string, mixed> $item Schema-shape item.
133 * @param string $remove_aria_label_template Sprintf template for the remove button's aria-label. `%s` is replaced with the product name.
134 * @return string
135 */
136 public static function render_ssr_common_row( array $item, string $remove_aria_label_template ): string {
137 $is_live = ! empty( $item['is_live'] );
138 $name = (string) ( $item['name'] ?? '' );
139 $permalink = (string) ( $item['permalink'] ?? '' );
140 $alt = html_entity_decode( $name, ENT_QUOTES, 'UTF-8' );
141 $image_html = (string) ( $item['image_html'] ?? '' );
142 $price_html = (string) ( $item['price_html'] ?? '' );
143 $variation_label = self::get_variation_label( $item );
144 $remove_aria = sprintf( $remove_aria_label_template, $alt );
145 $is_price_hidden = '' === $price_html;
146 // Tombstone rows (`is_live=false` or empty permalink) render `<a>`
147 // without an href — keeps the element shape stable for iAPI
148 // reconciliation against the live-row template, and the CSS in the
149 // shared partial drops link affordances when the anchor has no href.
150 $href_attr = $is_live && '' !== $permalink ? 'href="' . esc_url( $permalink ) . '"' : '';
151
152 ob_start();
153 ?>
154 <div class="wc-block-components-product-image wc-block-components-product-image--aspect-ratio-auto">
155 <a <?php echo $href_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped above with esc_url(). ?> data-wp-bind--href="context.listItem.permalink">
156 <span
157 class="<?php echo esc_attr( self::ROW_CLASS ); ?>__image-slot"
158 data-wp-context='{"htmlField":"image_html"}'
159 data-wp-watch="callbacks.updateInnerHtml"
160 >
161 <?php echo wp_kses_post( $image_html ); ?>
162 </span>
163 </a>
164 <button
165 type="button"
166 class="<?php echo esc_attr( self::ROW_CLASS ); ?>__remove"
167 aria-label="<?php echo esc_attr( $remove_aria ); ?>"
168 data-wp-on--click="actions.onClickRemove"
169 data-wp-bind--aria-label="state.currentItemRemoveLabel"
170 data-wp-bind--disabled="state.isCurrentItemPending"
171 >
172 <?php echo self::get_remove_icon_svg(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static SVG markup. ?>
173 </button>
174 <span
175 class="<?php echo esc_attr( self::ROW_CLASS ); ?>__variation"
176 data-wp-bind--hidden="!state.currentItemVariationLabel"
177 data-wp-text="state.currentItemVariationLabel"
178 <?php
179 if ( '' === $variation_label ) {
180 echo 'hidden';
181 }
182 ?>
183 ><?php echo esc_html( $variation_label ); ?></span>
184 </div>
185 <h2 class="wp-block-post-title has-text-align-center has-medium-font-size">
186 <a <?php echo $href_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- pre-escaped above with esc_url(). ?> data-wp-bind--href="context.listItem.permalink" data-wp-text="state.currentItemDisplayName"><?php echo esc_html( $alt ); ?></a>
187 </h2>
188 <div
189 class="price wc-block-components-product-price has-text-align-center has-small-font-size"
190 data-wp-bind--hidden="state.isPriceHidden"
191 data-wp-context='{"htmlField":"price_html"}'
192 data-wp-watch="callbacks.updateInnerHtml"
193 <?php
194 if ( $is_price_hidden ) {
195 echo 'hidden';
196 }
197 ?>
198 >
199 <?php echo wp_kses_post( $price_html ); ?>
200 </div>
201 <?php
202 return (string) ob_get_clean();
203 }
204
205 /**
206 * Empty-state `<li>` that the block toggles on once `state.isEmpty`
207 * flips. `$start_hidden = true` makes SSR ship with `hidden` so the
208 * message doesn't flash for shoppers whose list is being populated
209 * client-side. `$start_hidden = false` is for blocks (e.g. Wishlist)
210 * where the message should show on first paint when the list is empty.
211 *
212 * @param string $message Visible empty-state message.
213 * @param string $css_class Class attribute for the `<li>`.
214 * @param bool $start_hidden Whether the `<li>` should be `hidden` on first paint.
215 * @return string
216 */
217 public static function render_empty_state( string $message, string $css_class, bool $start_hidden = true ): string {
218 return sprintf(
219 '<li class="%1$s" data-wp-bind--hidden="!state.isEmpty"%2$s>%3$s</li>',
220 esc_attr( $css_class ),
221 $start_hidden ? ' hidden' : '',
222 esc_html( $message )
223 );
224 }
225
226 /**
227 * Render the iAPI store-notices region used by the row-level error
228 * banners. Mirrors `AddToCartWithOptions::render_interactivity_notices_region()`
229 * — keep in sync if the shape changes.
230 *
231 * @param string $wrapper_class Class attribute for the outer `<div>`.
232 * @return string
233 */
234 public static function render_interactivity_notices_region( string $wrapper_class ): string {
235 ob_start();
236 ?>
237 <div class="<?php echo esc_attr( $wrapper_class ); ?> wc-block-components-notices" data-wp-interactive="woocommerce/store-notices" data-wp-bind--hidden="!context.notices.length" hidden>
238 <template data-wp-each--notice="context.notices" data-wp-each-key="context.notice.id">
239 <div
240 class="wc-block-components-notice-banner"
241 data-wp-class--is-error="state.isError"
242 data-wp-class--is-success="state.isSuccess"
243 data-wp-class--is-info="state.isInfo"
244 data-wp-class--is-dismissible="context.notice.dismissible"
245 data-wp-bind--role="state.role"
246 data-wp-watch="callbacks.injectIcon"
247 >
248 <div class="wc-block-components-notice-banner__content">
249 <span data-wp-init="callbacks.renderNoticeContent" aria-live="assertive" aria-atomic="true"></span>
250 </div>
251 <button
252 type="button"
253 data-wp-bind--hidden="!context.notice.dismissible"
254 class="wc-block-components-button wp-element-button wc-block-components-notice-banner__dismiss contained"
255 aria-label="<?php esc_attr_e( 'Dismiss this notice', 'woocommerce' ); ?>"
256 data-wp-on--click="actions.removeNotice"
257 >
258 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
259 <path d="M13 11.8l6.1-6.3-1-1-6.1 6.2-6.1-6.2-1 1 6.1 6.3-6.5 6.7 1 1 6.5-6.6 6.5 6.6 1-1z" />
260 </svg>
261 </button>
262 </div>
263 </template>
264 </div>
265 <?php
266 return (string) ob_get_clean();
267 }
268
269 /**
270 * Markup for the trash icon used in the remove-item button. Mirrors the
271 * `trash` icon from `@wordpress/icons` that the cart line item uses for
272 * `wc-block-cart-item__remove-link`, inlined here so SSR first paint
273 * matches what JS would render after hydration. `currentColor` lets the
274 * surrounding badge wrapper drive the fill.
275 *
276 * @return string
277 */
278 public static function get_remove_icon_svg(): string {
279 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M12 5.5A2.25 2.25 0 0 0 9.878 7h4.244A2.251 2.251 0 0 0 12 5.5ZM12 4a3.751 3.751 0 0 0-3.675 3H5v1.5h1.27l.818 8.997a2.75 2.75 0 0 0 2.739 2.501h4.347a2.75 2.75 0 0 0 2.738-2.5L17.73 8.5H19V7h-3.325A3.751 3.751 0 0 0 12 4Zm4.224 4.5H7.776l.806 8.861a1.25 1.25 0 0 0 1.245 1.137h4.347a1.25 1.25 0 0 0 1.245-1.137l.805-8.861Z"/></svg>';
280 }
281
282 /**
283 * Markup for the empty-star icon. Mirrors `starEmpty` from
284 * `@wordpress/icons`, inlined here so SSR first paint matches what JS
285 * renders after hydration. `currentColor` lets the surrounding button
286 * drive the fill.
287 *
288 * @return string
289 */
290 public static function get_star_empty_svg(): string {
291 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M9.706 8.646a.25.25 0 01-.188.137l-4.626.672a.25.25 0 00-.139.427l3.348 3.262a.25.25 0 01.072.222l-.79 4.607a.25.25 0 00.362.264l4.138-2.176a.25.25 0 01.233 0l4.137 2.175a.25.25 0 00.363-.263l-.79-4.607a.25.25 0 01.072-.222l3.347-3.262a.25.25 0 00-.139-.427l-4.626-.672a.25.25 0 01-.188-.137l-2.069-4.192a.25.25 0 00-.448 0L9.706 8.646zM12 7.39l-.948 1.921a1.75 1.75 0 01-1.317.957l-2.12.308 1.534 1.495c.412.402.6.982.503 1.55l-.362 2.11 1.896-.997a1.75 1.75 0 011.629 0l1.895.997-.362-2.11a1.75 1.75 0 01.504-1.55l1.533-1.495-2.12-.308a1.75 1.75 0 01-1.317-.957L12 7.39z"/></svg>';
292 }
293
294 /**
295 * Markup for the filled-star icon. Mirrors `starFilled` from
296 * `@wordpress/icons`, inlined here so SSR first paint matches what JS
297 * renders after hydration. `currentColor` lets the surrounding button
298 * drive the fill.
299 *
300 * @return string
301 */
302 public static function get_star_filled_svg(): string {
303 return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M11.776 4.454a.25.25 0 01.448 0l2.069 4.192a.25.25 0 00.188.137l4.626.672a.25.25 0 01.139.426l-3.348 3.263a.25.25 0 00-.072.222l.79 4.607a.25.25 0 01-.362.263l-4.138-2.175a.25.25 0 00-.232 0l-4.138 2.175a.25.25 0 01-.363-.263l.79-4.607a.25.25 0 00-.071-.222L4.754 9.881a.25.25 0 01.139-.426l4.626-.672a.25.25 0 00.188-.137l2.069-4.192z"/></svg>';
304 }
305
306 /**
307 * Build a comma-separated variation label like "Color: Blue, Size: M".
308 *
309 * @param array<string, mixed> $item Schema-shape item.
310 * @return string
311 */
312 public static function get_variation_label( array $item ): string {
313 $variation = $item['variation'] ?? array();
314 if ( ! is_array( $variation ) || empty( $variation ) ) {
315 return '';
316 }
317 $parts = array();
318 foreach ( $variation as $entry ) {
319 if ( ! is_array( $entry ) ) {
320 continue;
321 }
322 $attribute = isset( $entry['attribute'] ) ? html_entity_decode( (string) $entry['attribute'], ENT_QUOTES, 'UTF-8' ) : '';
323 $value = isset( $entry['value'] ) ? html_entity_decode( (string) $entry['value'], ENT_QUOTES, 'UTF-8' ) : '';
324 if ( '' === $attribute && '' === $value ) {
325 continue;
326 }
327 $parts[] = $attribute . ': ' . $value;
328 }
329 return implode( ', ', $parts );
330 }
331 }
332