feat(widgets): add phase 4 validation and accessibility

This commit is contained in:
2026-08-17 11:40:50 +00:00
parent 8da6439675
commit be1979d378
23 changed files with 8078 additions and 53 deletions
@@ -97,10 +97,10 @@ final class ContentRenderer {
private static function posts(string $slug, array $instance, array $context): string {
$widget_id = (string) ($context['widget_id'] ?? '');
if (($instance['load_mode'] ?? 'sync') === 'ajax') {
return '<div class="aksrt-widget-items is-loading" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1"><div class="aksrt-widget-skeleton" aria-label="' . esc_attr__('正在加载', 'aksrt') . '"></div></div>';
return '<div class="aksrt-widget-items is-loading" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1" aria-live="polite" aria-busy="true"><div class="aksrt-widget-skeleton" role="status" aria-label="' . esc_attr__('正在加载', 'aksrt') . '"></div></div>';
}
$payload = PostRenderer::payload($instance, $slug, 1);
return '<div class="aksrt-widget-items" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1">' . $payload['html'] . $payload['paginate_html'] . '</div>';
return '<div class="aksrt-widget-items" data-widget-items data-widget="' . esc_attr($slug) . '" data-instance="' . esc_attr($widget_id) . '" data-page="1" aria-live="polite" aria-busy="false">' . $payload['html'] . $payload['paginate_html'] . '</div>';
}
private static function tab_posts(array $instance, array $context): string {
@@ -109,11 +109,15 @@ final class ContentRenderer {
return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt'));
}
$buttons = '';
$key = sanitize_html_class((string) ($context['widget_id'] ?? 'preview')) ?: 'preview';
$panel_id = 'aksrt-tab-panel-' . $key;
$first_tab_id = 'aksrt-tab-' . $key . '-0';
foreach ($tabs as $index => $tab) {
$buttons .= '<button type="button" role="tab" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" data-segment="' . $index . '">' . esc_html((string) ($tab['title'] ?? sprintf(__('栏目 %d', 'aksrt'), $index + 1))) . '</button>';
$tab_id = 'aksrt-tab-' . $key . '-' . $index;
$buttons .= '<button type="button" role="tab" id="' . esc_attr($tab_id) . '" aria-controls="' . esc_attr($panel_id) . '" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" tabindex="' . ($index === 0 ? '0' : '-1') . '" data-segment="' . $index . '">' . esc_html((string) ($tab['title'] ?? sprintf(__('栏目 %d', 'aksrt'), $index + 1))) . '</button>';
}
$payload = PostRenderer::payload($instance, 'tab_posts', 1, '0');
return '<div class="aksrt-widget-tabs" data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '"><div class="aksrt-widget-tabs__nav" role="tablist">' . $buttons . '</div><div class="aksrt-widget-tabs__panel" role="tabpanel" data-widget-items data-segment="0">' . $payload['html'] . $payload['paginate_html'] . '</div></div>';
return '<div class="aksrt-widget-tabs" data-widget="tab_posts" data-instance="' . esc_attr((string) ($context['widget_id'] ?? '')) . '"><div class="aksrt-widget-tabs__nav" role="tablist" aria-label="' . esc_attr__('文章栏目', 'aksrt') . '">' . $buttons . '</div><div class="aksrt-widget-tabs__panel" role="tabpanel" id="' . esc_attr($panel_id) . '" aria-labelledby="' . esc_attr($first_tab_id) . '" tabindex="0" data-widget-items data-segment="0" aria-live="polite" aria-busy="false">' . $payload['html'] . $payload['paginate_html'] . '</div></div>';
}
private static function post_pay(array $instance): string {
@@ -67,7 +67,7 @@ final class GeneralRenderer {
return RendererHelpers::empty_state(__('请配置视频地址', 'aksrt'));
}
$ratio = (string) $i['ratio'];
return '<div class="aksrt-widget-video aksrt-widget-video--' . esc_attr(str_replace(':', '-', $ratio)) . '" data-video-url="' . $url . '" data-video-poster="' . esc_url((string) $i['poster']) . '" data-autoplay="' . (!empty($i['autoplay']) ? '1' : '0') . '" data-muted="' . (!empty($i['muted']) ? '1' : '0') . '"><video controls playsinline preload="metadata"' . (!empty($i['autoplay']) ? ' autoplay' : '') . (!empty($i['muted']) ? ' muted' : '') . (!empty($i['poster']) ? ' poster="' . esc_url((string) $i['poster']) . '"' : '') . '><source src="' . $url . '"></video><p class="aksrt-widget-media-error" hidden>' . esc_html__('视频加载失败,请稍后重试。', 'aksrt') . '</p></div>';
return '<div class="aksrt-widget-video aksrt-widget-video--' . esc_attr(str_replace(':', '-', $ratio)) . '" data-video-url="' . $url . '" data-video-poster="' . esc_url((string) $i['poster']) . '" data-autoplay="' . (!empty($i['autoplay']) ? '1' : '0') . '" data-muted="' . (!empty($i['muted']) ? '1' : '0') . '"><video controls playsinline preload="metadata"' . (!empty($i['autoplay']) ? ' autoplay' : '') . (!empty($i['muted']) ? ' muted' : '') . (!empty($i['poster']) ? ' poster="' . esc_url((string) $i['poster']) . '"' : '') . '><source src="' . $url . '"></video><p class="aksrt-widget-media-error" role="alert" aria-live="assertive" hidden>' . esc_html__('视频加载失败,请稍后重试。', 'aksrt') . '</p></div>';
}
private static function render_embed(array $i): string {
@@ -9,14 +9,13 @@ defined('ABSPATH') || exit;
/** Render the seven knowledge-product presentation widgets. */
final class KnowledgeProductRenderer {
public static function render(string $slug, array $instance, array $context = []): string {
unset($context);
switch ($slug) {
case 'pricing_plans': return self::pricing($instance);
case 'reviews': return self::reviews($instance);
case 'faq': return self::faq($instance);
case 'stats': return self::stats($instance);
case 'highlights': return self::highlights($instance);
case 'feature_intro': return self::feature_intro($instance);
case 'feature_intro': return self::feature_intro($instance, $context);
case 'tilt_cases': return self::tilt($instance);
}
return '';
@@ -96,17 +95,20 @@ final class KnowledgeProductRenderer {
return $html . '</div>';
}
private static function feature_intro(array $i): string {
private static function feature_intro(array $i, array $context): string {
$tabs = $panels = '';
$key = sanitize_html_class((string) ($context['widget_id'] ?? 'preview')) ?: 'preview';
foreach ((array) $i['items'] as $index => $item) {
$tabs .= '<button type="button" role="tab" data-feature-index="' . $index . '" aria-selected="' . ($index === 0 ? 'true' : 'false') . '">' . esc_html((string) ($item['title'] ?? '')) . '</button>';
$tab_id = 'aksrt-feature-' . $key . '-tab-' . $index;
$panel_id = 'aksrt-feature-' . $key . '-panel-' . $index;
$tabs .= '<button type="button" role="tab" id="' . esc_attr($tab_id) . '" aria-controls="' . esc_attr($panel_id) . '" data-feature-index="' . $index . '" aria-selected="' . ($index === 0 ? 'true' : 'false') . '" tabindex="' . ($index === 0 ? '0' : '-1') . '">' . esc_html((string) ($item['title'] ?? '')) . '</button>';
$features = '';
foreach (RendererHelpers::lines((string) ($item['features'] ?? '')) as $feature) {
$features .= '<li>' . esc_html($feature) . '</li>';
}
$panels .= '<article role="tabpanel" data-feature-panel="' . $index . '"' . ($index ? ' hidden' : '') . '><div class="aksrt-feature-intro__media">' . RendererHelpers::card_media((array) $item) . '</div><div class="aksrt-feature-intro__copy"><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p><ul>' . $features . '</ul></div></article>';
$panels .= '<article role="tabpanel" id="' . esc_attr($panel_id) . '" aria-labelledby="' . esc_attr($tab_id) . '" tabindex="0" data-feature-panel="' . $index . '"' . ($index ? ' hidden' : '') . '><div class="aksrt-feature-intro__media">' . RendererHelpers::card_media((array) $item) . '</div><div class="aksrt-feature-intro__copy"><h3>' . esc_html((string) ($item['title'] ?? '')) . '</h3><p>' . esc_html((string) ($item['description'] ?? '')) . '</p><ul>' . $features . '</ul></div></article>';
}
return '<div class="aksrt-feature-intro"><div class="aksrt-feature-intro__tabs" role="tablist">' . $tabs . '</div><div class="aksrt-feature-intro__panels">' . $panels . '</div></div>';
return '<div class="aksrt-feature-intro"><div class="aksrt-feature-intro__tabs" role="tablist" aria-label="' . esc_attr__('知识产品亮点', 'aksrt') . '">' . $tabs . '</div><div class="aksrt-feature-intro__panels">' . $panels . '</div></div>';
}
private static function tilt(array $i): string {