Changeset 62444
- Timestamp:
- 06/02/2026 06:03:58 AM (3 days ago)
- Location:
- trunk
- Files:
-
- 2 edited
-
src/wp-includes/class-wp-theme-json.php (modified) (23 diffs)
-
tests/phpunit/tests/theme/wpThemeJson.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/class-wp-theme-json.php
r62415 r62444 646 646 647 647 /** 648 * Responsive breakpoint state keys and their corresponding CSS media queries. 649 * These are available for all blocks and wrap their styles in the given media query. 650 * Keep in sync with RESPONSIVE_BREAKPOINTS in packages/global-styles-engine/src/core/render.tsx. 651 * 652 * @since 7.1.0 653 * @var array 654 */ 655 const RESPONSIVE_BREAKPOINTS = array( 656 'mobile' => '@media (width <= 480px)', 657 'tablet' => '@media (480px < width <= 782px)', 658 ); 659 660 /** 648 661 * The valid elements that can be found under styles. 649 662 * … … 1055 1068 1056 1069 /* 1057 * Set allowed element pseudo selectors based on per element allow list.1070 * Set allowed element pseudo selectors and responsive breakpoint states. 1058 1071 * Target data structure in schema: 1059 1072 * e.g. 1060 1073 * - top level elements: `$schema['styles']['elements']['link'][':hover']`. 1061 1074 * - block level elements: `$schema['styles']['blocks']['core/button']['elements']['link'][':hover']`. 1075 * - block responsive elements: `$schema['styles']['blocks']['core/button']['tablet']['elements']['link'][':hover']`. 1062 1076 */ 1063 1077 foreach ( $valid_element_names as $element ) { … … 1069 1083 } 1070 1084 } 1085 1086 // Add responsive breakpoint states for elements. 1087 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1088 $schema_styles_elements[ $element ][ $breakpoint_state ] = $styles_non_top_level; 1089 } 1071 1090 } 1072 1091 … … 1076 1095 /* 1077 1096 * Generate a schema for blocks. 1078 * - Block styles can contain `elements` & `variations`definitions.1097 * - Block styles can contain `elements`, `variations`, and responsive breakpoint state definitions. 1079 1098 * - Variations definitions cannot be nested. 1080 * - Variations can contain styles for inner `blocks` .1081 * - Variation inner `blocks` styles can contain `elements` .1099 * - Variations can contain styles for inner `blocks`, `elements`, and responsive breakpoint states. 1100 * - Variation inner `blocks` styles can contain `elements` and responsive breakpoint states. 1082 1101 * 1083 * As each variation needs a `blocks` schema but further nested1084 * inner `blocks`, the overall schema will begenerated in multiple passes.1102 * As each variation needs both a `blocks` schema and responsive `blocks` schemas 1103 * for further nested inner `blocks`, the overall schema is generated in multiple passes. 1085 1104 */ 1086 1105 foreach ( $valid_block_names as $block ) { … … 1088 1107 $schema_styles_blocks[ $block ] = $styles_non_top_level; 1089 1108 $schema_styles_blocks[ $block ]['elements'] = $schema_styles_elements; 1109 1110 // Add responsive breakpoint states for all blocks. 1111 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1112 $schema_styles_blocks[ $block ][ $breakpoint_state ] = $styles_non_top_level; 1113 $schema_styles_blocks[ $block ][ $breakpoint_state ]['elements'] = $schema_styles_elements; 1114 1115 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1116 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1117 $schema_styles_blocks[ $block ][ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; 1118 } 1119 } 1120 } 1090 1121 1091 1122 // Add pseudo-selectors for blocks that support them … … 1119 1150 foreach ( $style_variation_names as $variation_name ) { 1120 1151 $variation_schema = $block_style_variation_styles; 1152 1153 // Add responsive breakpoint states to block style variations. 1154 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint_state ) { 1155 $variation_schema[ $breakpoint_state ] = $styles_non_top_level; 1156 $variation_schema[ $breakpoint_state ]['elements'] = $schema_styles_elements; 1157 $variation_schema[ $breakpoint_state ]['blocks'] = $schema_styles_blocks; 1158 1159 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] ) ) { 1160 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block ] as $pseudo_selector ) { 1161 $variation_schema[ $breakpoint_state ][ $pseudo_selector ] = $styles_non_top_level; 1162 } 1163 } 1164 } 1121 1165 1122 1166 // Add pseudo-selectors to variations for blocks that support them … … 1887 1931 } 1888 1932 } 1933 1934 if ( ! empty( $options['media_query'] ) && ! empty( $block_rules ) ) { 1935 $block_rules = $options['media_query'] . '{' . $block_rules . '}'; 1936 } 1937 1889 1938 return $block_rules; 1890 1939 } … … 2874 2923 2875 2924 $variation_selectors = array(); 2925 2876 2926 if ( $include_variations && isset( $node['variations'] ) ) { 2877 2927 foreach ( $node['variations'] as $variation => $node ) { … … 2888 2938 'selector' => $selector, 2889 2939 'selectors' => $feature_selectors, 2940 'elements' => $selectors[ $name ]['elements'] ?? array(), 2890 2941 'duotone' => $duotone_selector, 2891 'features' => $feature_selectors,2892 2942 'variations' => $variation_selectors, 2893 2943 'css' => $selector, 2894 2944 ); 2895 2945 2946 // Responsive block nodes: emit one node per breakpoint that has styles. 2947 // These are rendered immediately after the base block node so that 2948 // the cascade order is: .block{} → @media{.block{}} 2949 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 2950 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ] ) ) { 2951 $nodes[] = array( 2952 'name' => $name, 2953 'path' => array( 'styles', 'blocks', $name, $breakpoint ), 2954 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 2955 'selector' => $selector, 2956 'selectors' => $feature_selectors, 2957 'elements' => $selectors[ $name ]['elements'] ?? array(), 2958 'variations' => $variation_selectors, 2959 'css' => $selector, 2960 ); 2961 } 2962 } 2963 2896 2964 // Handle any pseudo selectors for the block. 2897 2965 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] ) ) { 2898 2966 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $name ] as $pseudo_selector ) { 2899 if ( isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ) ) { 2967 $has_pseudo = isset( $theme_json['styles']['blocks'][ $name ][ $pseudo_selector ] ); 2968 $has_responsive_pseudo = false; 2969 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 2970 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) { 2971 $has_responsive_pseudo = true; 2972 break; 2973 } 2974 } 2975 2976 if ( ! $has_pseudo && ! $has_responsive_pseudo ) { 2977 continue; 2978 } 2979 2980 /* 2981 * Append the pseudo-selector to each feature selector so that 2982 * get_feature_declarations_for_node generates CSS scoped to the 2983 * pseudo-state (e.g. '.wp-block-button:hover') rather than the 2984 * default state (e.g. '.wp-block-button'). 2985 */ 2986 $pseudo_feature_selectors = array(); 2987 foreach ( $feature_selectors ?? array() as $feature => $feature_selector ) { 2988 if ( is_array( $feature_selector ) ) { 2989 $pseudo_feature_selectors[ $feature ] = array(); 2990 foreach ( $feature_selector as $subfeature => $subfeature_selector ) { 2991 $pseudo_feature_selectors[ $feature ][ $subfeature ] = static::append_to_selector( $subfeature_selector, $pseudo_selector ); 2992 } 2993 } else { 2994 $pseudo_feature_selectors[ $feature ] = static::append_to_selector( $feature_selector, $pseudo_selector ); 2995 } 2996 } 2997 2998 if ( $has_pseudo ) { 2900 2999 $nodes[] = array( 2901 3000 'name' => $name, 2902 3001 'path' => array( 'styles', 'blocks', $name, $pseudo_selector ), 2903 3002 'selector' => static::append_to_selector( $selector, $pseudo_selector ), 2904 'selectors' => $feature_selectors, 3003 'selectors' => $pseudo_feature_selectors, 3004 'elements' => $selectors[ $name ]['elements'] ?? array(), 2905 3005 'duotone' => $duotone_selector, 2906 3006 'variations' => $variation_selectors, … … 2908 3008 ); 2909 3009 } 2910 } 2911 } 2912 } 2913 3010 3011 // Responsive pseudo nodes: emit one node per breakpoint that has 3012 // this pseudo state, immediately after the default pseudo node. 3013 // Cascade order: .block:hover{} → @media{.block:hover{}} 3014 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3015 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ][ $pseudo_selector ] ) ) { 3016 $nodes[] = array( 3017 'name' => $name, 3018 'path' => array( 'styles', 'blocks', $name, $breakpoint, $pseudo_selector ), 3019 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3020 'selector' => static::append_to_selector( $selector, $pseudo_selector ), 3021 'selectors' => $pseudo_feature_selectors, 3022 'elements' => $selectors[ $name ]['elements'] ?? array(), 3023 'variations' => $variation_selectors, 3024 'css' => static::append_to_selector( $selector, $pseudo_selector ), 3025 ); 3026 } 3027 } 3028 } 3029 } 3030 } 2914 3031 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'] ) ) { 2915 3032 foreach ( $theme_json['styles']['blocks'][ $name ]['elements'] as $element => $node ) { 2916 $node_path = array( 'styles', 'blocks', $name, 'elements', $element ); 2917 3033 $element_path = array( 'styles', 'blocks', $name, 'elements', $element ); 2918 3034 if ( $include_node_paths_only ) { 2919 3035 $nodes[] = array( 2920 'path' => $ node_path,3036 'path' => $element_path, 2921 3037 ); 2922 3038 continue; 2923 3039 } 2924 3040 3041 $element_selector = $selectors[ $name ]['elements'][ $element ]; 3042 2925 3043 $nodes[] = array( 2926 'path' => $ node_path,2927 'selector' => $ selectors[ $name ]['elements'][ $element ],3044 'path' => $element_path, 3045 'selector' => $element_selector, 2928 3046 ); 3047 3048 // Responsive element nodes: one node per breakpoint that has 3049 // styles for this element. Cascade: a{} → @media{a{}} 3050 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3051 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ] ) ) { 3052 $nodes[] = array( 3053 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), 3054 'selector' => $element_selector, 3055 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3056 ); 3057 } 3058 } 2929 3059 2930 3060 // Handle any pseudo selectors for the element. 2931 3061 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] ) ) { 2932 3062 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element ] as $pseudo_selector ) { 2933 if ( isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ) ) { 2934 $node_path = array( 'styles', 'blocks', $name, 'elements', $element ); 3063 // Create element pseudo node if default or any responsive breakpoint has the pseudo. 3064 $has_element_pseudo = isset( $theme_json['styles']['blocks'][ $name ]['elements'][ $element ][ $pseudo_selector ] ); 3065 if ( ! $has_element_pseudo ) { 3066 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $bp ) { 3067 if ( isset( $theme_json['styles']['blocks'][ $name ][ $bp ]['elements'][ $element ][ $pseudo_selector ] ) ) { 3068 $has_element_pseudo = true; 3069 break; 3070 } 3071 } 3072 } 3073 3074 if ( $has_element_pseudo ) { 3075 $element_pseudo_path = array( 'styles', 'blocks', $name, 'elements', $element ); 3076 if ( $include_node_paths_only ) { 3077 $nodes[] = array( 3078 'path' => $element_pseudo_path, 3079 ); 3080 continue; 3081 } 2935 3082 2936 3083 $nodes[] = array( 2937 'path' => $ node_path,2938 'selector' => static::append_to_selector( $ selectors[ $name ]['elements'][ $element ], $pseudo_selector ),3084 'path' => $element_pseudo_path, 3085 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), 2939 3086 ); 3087 3088 // Responsive element pseudo nodes: one node per breakpoint 3089 // that has this pseudo state for this element. 3090 // Cascade: a:hover{} → @media{a:hover{}} 3091 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3092 if ( isset( $theme_json['styles']['blocks'][ $name ][ $breakpoint ]['elements'][ $element ][ $pseudo_selector ] ) ) { 3093 $nodes[] = array( 3094 'path' => array( 'styles', 'blocks', $name, $breakpoint, 'elements', $element ), 3095 'selector' => static::append_to_selector( $element_selector, $pseudo_selector ), 3096 'media_query' => static::RESPONSIVE_BREAKPOINTS[ $breakpoint ], 3097 ); 3098 } 3099 } 2940 3100 } 2941 3101 } … … 2966 3126 $feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $node ); 2967 3127 $is_root_selector = static::ROOT_BLOCK_SELECTOR === $selector; 3128 $media_query = $block_metadata['media_query'] ?? null; 2968 3129 2969 3130 // Update text indent selector for paragraph blocks based on the textIndent setting. 2970 3131 $block_name = $block_metadata['name'] ?? null; 2971 3132 $feature_declarations = static::update_paragraph_text_indent_selector( $feature_declarations, $settings, $block_name ); 3133 $block_elements = $block_metadata['elements'] ?? array(); 2972 3134 2973 3135 // If there are style variations, generate the declarations for them, including any feature selectors the block may have. 2974 3136 $style_variation_declarations = array(); 2975 3137 $style_variation_custom_css = array(); 3138 $style_variation_responsive_css = array(); 2976 3139 $style_variation_layout_metadata = array(); 2977 if ( ! empty( $block_metadata['variations'] ) ) {3140 if ( ! $media_query && ! empty( $block_metadata['variations'] ) ) { 2978 3141 foreach ( $block_metadata['variations'] as $style_variation ) { 2979 3142 $style_variation_node = _wp_array_get( $this->theme_json, $style_variation['path'], array() ); … … 3018 3181 $block_name = $block_metadata['name']; 3019 3182 } elseif ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 3 ) { 3020 $block_name = $block_metadata['path'][2];3183 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); 3021 3184 } else { 3022 3185 $block_name = null; … … 3041 3204 ); 3042 3205 } 3206 3207 // Store responsive breakpoint CSS for the style variation. 3208 // This includes both base properties and feature-level selectors. 3209 $variation_responsive_css = ''; 3210 3211 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 3212 if ( ! isset( $style_variation_node[ $breakpoint ] ) ) { 3213 continue; 3214 } 3215 3216 $breakpoint_node = $style_variation_node[ $breakpoint ]; 3217 $breakpoint_media = static::RESPONSIVE_BREAKPOINTS[ $breakpoint ]; 3218 // Process feature-level declarations for this breakpoint. 3219 $breakpoint_feature_declarations = static::get_feature_declarations_for_node( $block_metadata, $breakpoint_node ); 3220 $breakpoint_feature_declarations = static::update_paragraph_text_indent_selector( $breakpoint_feature_declarations, $settings, $block_name ); 3221 foreach ( $breakpoint_feature_declarations as $feature_selector => $feature_decl ) { 3222 $clean_feature_selector = preg_replace( '/,\s+/', ',', $feature_selector ); 3223 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_feature_selector ); 3224 3225 if ( $block_metadata['selector'] && ! str_contains( $clean_feature_selector, $block_metadata['selector'] ) ) { 3226 /* 3227 * Feature selector is block-level (e.g. `.wp-block-button` for 3228 * dimensions/width) — apply the variation class directly to it. 3229 */ 3230 $feature_element_selector = str_replace( $shortened_selector, '', $clean_style_variation_selector ); 3231 $combined_selectors = str_replace( $feature_element_selector, '', $clean_style_variation_selector ); 3232 } else { 3233 // Prepend the variation selector to the current selector. 3234 $split_selectors = explode( ',', $shortened_selector ); 3235 $updated_selectors = array_map( 3236 static function ( $split_selector ) use ( $clean_style_variation_selector ) { 3237 return $clean_style_variation_selector . $split_selector; 3238 }, 3239 $split_selectors 3240 ); 3241 $combined_selectors = implode( ',', $updated_selectors ); 3242 } 3243 3244 $feature_ruleset = static::to_ruleset( ':root :where(' . $combined_selectors . ')', $feature_decl ); 3245 $variation_responsive_css .= $breakpoint_media . '{' . $feature_ruleset . '}'; 3246 } 3247 3248 // Process base properties for this breakpoint. 3249 $breakpoint_declarations = static::compute_style_properties( $breakpoint_node, $settings, null, $this->theme_json ); 3250 if ( ! empty( $breakpoint_declarations ) ) { 3251 $base_ruleset = static::to_ruleset( ':root :where(' . $style_variation['selector'] . ')', $breakpoint_declarations ); 3252 $variation_responsive_css .= $breakpoint_media . '{' . $base_ruleset . '}'; 3253 } 3254 3255 $breakpoint_pseudo_declarations = static::process_pseudo_selectors( $breakpoint_node, $style_variation['selector'], $settings, $block_name ); 3256 foreach ( $breakpoint_pseudo_declarations as $pseudo_selector => $pseudo_declarations ) { 3257 if ( empty( $pseudo_declarations ) ) { 3258 continue; 3259 } 3260 $pseudo_ruleset = static::to_ruleset( ':root :where(' . $pseudo_selector . ')', $pseudo_declarations ); 3261 $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_ruleset . '}'; 3262 } 3263 3264 // Process custom CSS for this breakpoint. 3265 if ( isset( $breakpoint_node['css'] ) ) { 3266 $breakpoint_custom_css = static::process_blocks_custom_css( $breakpoint_node['css'], $style_variation['selector'] ); 3267 $variation_responsive_css .= $breakpoint_media . '{' . $breakpoint_custom_css . '}'; 3268 } 3269 3270 // Process blockGap responsive layout styles for this variation. 3271 if ( isset( $breakpoint_node['spacing']['blockGap'] ) ) { 3272 $variation_layout_metadata = $style_variation; 3273 $variation_layout_metadata['selector'] = $style_variation['selector'] . $block_metadata['css']; 3274 $variation_responsive_css .= $this->get_layout_styles( 3275 $variation_layout_metadata, 3276 array( 3277 'node' => $breakpoint_node, 3278 'media_query' => $breakpoint_media, 3279 ) 3280 ); 3281 } 3282 3283 // Process nested element styles for this breakpoint state. 3284 if ( isset( $breakpoint_node['elements'] ) && ! empty( $block_elements ) ) { 3285 foreach ( $breakpoint_node['elements'] as $element_name => $element_node ) { 3286 if ( ! isset( $block_elements[ $element_name ] ) ) { 3287 continue; 3288 } 3289 3290 $clean_element_selector = preg_replace( '/,\s+/', ',', $block_elements[ $element_name ] ); 3291 $shortened_selector = str_replace( $block_metadata['selector'], '', $clean_element_selector ); 3292 $split_selectors = explode( ',', $shortened_selector ); 3293 $updated_selectors = array_map( 3294 static function ( $split_selector ) use ( $clean_style_variation_selector ) { 3295 return $clean_style_variation_selector . $split_selector; 3296 }, 3297 $split_selectors 3298 ); 3299 $variation_element_selector = implode( ',', $updated_selectors ); 3300 3301 $element_declarations = static::compute_style_properties( $element_node, $settings, null, $this->theme_json ); 3302 if ( ! empty( $element_declarations ) ) { 3303 $element_ruleset = static::to_ruleset( ':root :where(' . $variation_element_selector . ')', $element_declarations ); 3304 $variation_responsive_css .= $breakpoint_media . '{' . $element_ruleset . '}'; 3305 } 3306 3307 if ( isset( $element_node['css'] ) ) { 3308 $element_custom_css = static::process_blocks_custom_css( $element_node['css'], $variation_element_selector ); 3309 $variation_responsive_css .= $breakpoint_media . '{' . $element_custom_css . '}'; 3310 } 3311 3312 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { 3313 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { 3314 if ( ! isset( $element_node[ $pseudo_selector ] ) ) { 3315 continue; 3316 } 3317 3318 $pseudo_declarations = static::compute_style_properties( $element_node[ $pseudo_selector ], $settings, null, $this->theme_json ); 3319 if ( empty( $pseudo_declarations ) ) { 3320 continue; 3321 } 3322 3323 $pseudo_selector_ruleset = static::to_ruleset( ':root :where(' . static::append_to_selector( $variation_element_selector, $pseudo_selector ) . ')', $pseudo_declarations ); 3324 $variation_responsive_css .= $breakpoint_media . '{' . $pseudo_selector_ruleset . '}'; 3325 } 3326 } 3327 } 3328 } 3329 } 3330 3331 if ( ! empty( $variation_responsive_css ) ) { 3332 $style_variation_responsive_css[ $style_variation['selector'] ] = $variation_responsive_css; 3333 } 3043 3334 } 3044 3335 } … … 3066 3357 $block_pseudo_selector = null; 3067 3358 if ( in_array( 'blocks', $block_metadata['path'], true ) && count( $block_metadata['path'] ) >= 4 ) { 3068 $block_name = $block_metadata['path'][2]; // 'core/button'3359 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); // 'core/button' 3069 3360 $last_path_element = $block_metadata['path'][ count( $block_metadata['path'] ) - 1 ]; // ':hover' 3070 3361 … … 3108 3399 // Process block pseudo-selector styles 3109 3400 // For block pseudo-selectors, we need to get the block data first, then access the pseudo-selector 3110 $block_name = $block_metadata['path'][2]; // 'core/button'3401 $block_name = static::get_block_name_from_metadata_path( $block_metadata ); // 'core/button' 3111 3402 $block_data = _wp_array_get( $this->theme_json, array( 'styles', 'blocks', $block_name ), array() ); 3112 3403 $pseudo_data = $block_data[ $block_pseudo_selector ] ?? array(); … … 3225 3516 $block_rules .= $style_variation_custom_css[ $style_variation_selector ]; 3226 3517 } 3518 if ( isset( $style_variation_responsive_css[ $style_variation_selector ] ) ) { 3519 $block_rules .= $style_variation_responsive_css[ $style_variation_selector ]; 3520 } 3227 3521 } 3228 3522 … … 3230 3524 if ( isset( $node['css'] ) && ! $is_root_selector ) { 3231 3525 $block_rules .= $this->process_blocks_custom_css( $node['css'], $selector ); 3526 } 3527 3528 // 8. Wrap the entire block output in a media query if this is a responsive node. 3529 // Responsive nodes are created by get_block_nodes() for each breakpoint and carry 3530 // a 'media_query' key. 3531 if ( $media_query && ! empty( $block_rules ) ) { 3532 $block_rules = $media_query . '{' . $block_rules . '}'; 3232 3533 } 3233 3534 … … 3727 4028 } 3728 4029 4030 $block_name = in_array( 'blocks', $metadata['path'], true ) 4031 ? static::get_block_name_from_metadata_path( $metadata ) 4032 : null; 4033 3729 4034 // The global styles custom CSS is not sanitized, but can only be edited by users with 'edit_css' capability. 3730 4035 if ( isset( $input['css'] ) && current_user_can( 'edit_css' ) ) { … … 3752 4057 } 3753 4058 4059 // Re-add and process responsive breakpoint styles. 4060 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4061 if ( isset( $input[ $breakpoint ] ) ) { 4062 $output[ $breakpoint ] = static::remove_insecure_styles( $input[ $breakpoint ] ); 4063 4064 if ( isset( $input[ $breakpoint ]['elements'] ) ) { 4065 $output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $input[ $breakpoint ]['elements'] ); 4066 } 4067 4068 if ( isset( $input[ $breakpoint ]['blocks'] ) ) { 4069 $output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $input[ $breakpoint ]['blocks'] ); 4070 } 4071 4072 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { 4073 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { 4074 if ( isset( $input[ $breakpoint ][ $pseudo_selector ] ) ) { 4075 $output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $input[ $breakpoint ][ $pseudo_selector ] ); 4076 } 4077 } 4078 } 4079 4080 // Responsive custom CSS is allowed for users with 'edit_css' capability. 4081 if ( isset( $input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { 4082 $output[ $breakpoint ]['css'] = $input[ $breakpoint ]['css']; 4083 } 4084 } 4085 } 4086 3754 4087 if ( ! empty( $output ) ) { 3755 4088 _wp_array_set( $sanitized, $metadata['path'], $output ); … … 3773 4106 } 3774 4107 4108 // Re-add and process responsive breakpoint styles for variations. 4109 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4110 if ( isset( $variation_input[ $breakpoint ] ) ) { 4111 $variation_output[ $breakpoint ] = static::remove_insecure_styles( $variation_input[ $breakpoint ] ); 4112 4113 if ( isset( $variation_input[ $breakpoint ]['elements'] ) ) { 4114 $variation_output[ $breakpoint ]['elements'] = static::remove_insecure_element_styles( $variation_input[ $breakpoint ]['elements'] ); 4115 } 4116 4117 if ( isset( $variation_input[ $breakpoint ]['blocks'] ) ) { 4118 $variation_output[ $breakpoint ]['blocks'] = static::remove_insecure_inner_block_styles( $variation_input[ $breakpoint ]['blocks'] ); 4119 } 4120 4121 if ( $block_name && isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] ) ) { 4122 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_name ] as $pseudo_selector ) { 4123 if ( isset( $variation_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4124 $variation_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $variation_input[ $breakpoint ][ $pseudo_selector ] ); 4125 } 4126 } 4127 } 4128 4129 // Responsive custom CSS is allowed for users with 'edit_css' capability. 4130 if ( isset( $variation_input[ $breakpoint ]['css'] ) && current_user_can( 'edit_css' ) ) { 4131 $variation_output[ $breakpoint ]['css'] = $variation_input[ $breakpoint ]['css']; 4132 } 4133 } 4134 } 4135 3775 4136 if ( ! empty( $variation_output ) ) { 3776 4137 _wp_array_set( $sanitized, $variation['path'], $variation_output ); … … 3833 4194 } 3834 4195 4196 // Re-add and process responsive breakpoint styles for elements. 4197 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4198 if ( isset( $element_input[ $breakpoint ] ) ) { 4199 $element_output[ $breakpoint ] = static::remove_insecure_styles( $element_input[ $breakpoint ] ); 4200 4201 if ( isset( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] ) ) { 4202 foreach ( static::VALID_ELEMENT_PSEUDO_SELECTORS[ $element_name ] as $pseudo_selector ) { 4203 if ( isset( $element_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4204 $element_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $element_input[ $breakpoint ][ $pseudo_selector ] ); 4205 } 4206 } 4207 } 4208 } 4209 } 4210 3835 4211 $sanitized[ $element_name ] = $element_output; 3836 4212 } … … 3854 4230 if ( isset( $block_input['elements'] ) ) { 3855 4231 $block_output['elements'] = static::remove_insecure_element_styles( $block_input['elements'] ); 4232 } 4233 4234 // Re-add and process responsive breakpoint styles for inner blocks. 4235 foreach ( array_keys( static::RESPONSIVE_BREAKPOINTS ) as $breakpoint ) { 4236 if ( isset( $block_input[ $breakpoint ] ) ) { 4237 $block_output[ $breakpoint ] = static::remove_insecure_styles( $block_input[ $breakpoint ] ); 4238 4239 if ( isset( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] ) ) { 4240 foreach ( static::VALID_BLOCK_PSEUDO_SELECTORS[ $block_type ] as $pseudo_selector ) { 4241 if ( isset( $block_input[ $breakpoint ][ $pseudo_selector ] ) ) { 4242 $block_output[ $breakpoint ][ $pseudo_selector ] = static::remove_insecure_styles( $block_input[ $breakpoint ][ $pseudo_selector ] ); 4243 } 4244 } 4245 } 4246 } 3856 4247 } 3857 4248 … … 4844 5235 return $valid_variations; 4845 5236 } 5237 5238 /** 5239 * Extracts the block name from the block metadata path. 5240 * 5241 * @since 7.1 5242 * 5243 * @param array $block_metadata Block metadata. 5244 * @return string|null The block name or null if not found. 5245 */ 5246 private static function get_block_name_from_metadata_path( $block_metadata ) { 5247 return $block_metadata['path'][2] ?? null; 5248 } 4846 5249 } -
trunk/tests/phpunit/tests/theme/wpThemeJson.php
r62415 r62444 938 938 939 939 /** 940 * @ticket 65164 941 */ 942 public function test_get_styles_for_block_responsive_feature_selector_not_duplicated_on_base_selector() { 943 register_block_type( 944 'test/responsive-feature', 945 array( 946 'api_version' => 3, 947 'selectors' => array( 948 'root' => '.wp-block-test-responsive-feature', 949 'color' => '.wp-block-test-responsive-feature .color-target', 950 ), 951 ) 952 ); 953 954 $theme_json = new WP_Theme_JSON( 955 array( 956 'version' => WP_Theme_JSON::LATEST_SCHEMA, 957 'styles' => array( 958 'blocks' => array( 959 'test/responsive-feature' => array( 960 'mobile' => array( 961 'color' => array( 962 'text' => 'red', 963 ), 964 ), 965 ), 966 ), 967 ), 968 ) 969 ); 970 971 $base_metadata = array( 972 'name' => 'test/responsive-feature', 973 'path' => array( 'styles', 'blocks', 'test/responsive-feature' ), 974 'selector' => '.wp-block-test-responsive-feature', 975 'selectors' => array( 976 'color' => '.wp-block-test-responsive-feature .color-target', 977 ), 978 ); 979 980 $mobile_metadata = array( 981 'name' => 'test/responsive-feature', 982 'path' => array( 'styles', 'blocks', 'test/responsive-feature', 'mobile' ), 983 'selector' => '.wp-block-test-responsive-feature', 984 'selectors' => array( 985 'color' => '.wp-block-test-responsive-feature .color-target', 986 ), 987 'media_query' => '@media (width <= 480px)', 988 ); 989 990 $actual_styles = $theme_json->get_styles_for_block( $base_metadata ); 991 $actual_styles .= $theme_json->get_styles_for_block( $mobile_metadata ); 992 993 unregister_block_type( 'test/responsive-feature' ); 994 995 $this->assertStringContainsString( 996 '@media (width <= 480px){:root :where(.wp-block-test-responsive-feature .color-target){color: red;}}', 997 $actual_styles 998 ); 999 $this->assertStringNotContainsString( 1000 '@media (width <= 480px){:root :where(.wp-block-test-responsive-feature){color: red;}}', 1001 $actual_styles 1002 ); 1003 } 1004 1005 /** 1006 * @ticket 65164 1007 */ 1008 public function test_get_styles_for_block_outputs_responsive_block_gap_after_default_gap() { 1009 $theme_json = new WP_Theme_JSON( 1010 array( 1011 'version' => WP_Theme_JSON::LATEST_SCHEMA, 1012 'settings' => array( 1013 'spacing' => array( 1014 'blockGap' => true, 1015 ), 1016 ), 1017 'styles' => array( 1018 'blocks' => array( 1019 'core/group' => array( 1020 'spacing' => array( 1021 'blockGap' => '5rem', 1022 ), 1023 'mobile' => array( 1024 'spacing' => array( 1025 'blockGap' => '2rem', 1026 ), 1027 ), 1028 ), 1029 ), 1030 ), 1031 ) 1032 ); 1033 1034 $base_metadata = array( 1035 'name' => 'core/group', 1036 'path' => array( 'styles', 'blocks', 'core/group' ), 1037 'selector' => '.wp-block-group', 1038 'css' => '.wp-block-group', 1039 ); 1040 1041 $mobile_metadata = array( 1042 'name' => 'core/group', 1043 'path' => array( 'styles', 'blocks', 'core/group', 'mobile' ), 1044 'selector' => '.wp-block-group', 1045 'css' => '.wp-block-group', 1046 'media_query' => '@media (width <= 480px)', 1047 ); 1048 1049 $actual_styles = $theme_json->get_styles_for_block( $base_metadata ); 1050 $actual_styles .= $theme_json->get_styles_for_block( $mobile_metadata ); 1051 1052 $default_gap = ':root :where(.wp-block-group-is-layout-flex){gap: 5rem;}'; 1053 $mobile_gap = ':root :where(.wp-block-group-is-layout-flex){gap: 2rem;}'; 1054 1055 $this->assertStringContainsString( $default_gap, $actual_styles ); 1056 $this->assertStringContainsString( '@media (width <= 480px)', $actual_styles ); 1057 $this->assertStringContainsString( $mobile_gap, $actual_styles ); 1058 $this->assertLessThan( strpos( $actual_styles, $mobile_gap ), strpos( $actual_styles, $default_gap ) ); 1059 } 1060 1061 /** 1062 * @ticket 65164 1063 */ 1064 public function test_get_styles_for_block_responsive_element_pseudo_styles_preserve_order_and_do_not_duplicate_pseudo() { 1065 $theme_json = new WP_Theme_JSON( 1066 array( 1067 'version' => WP_Theme_JSON::LATEST_SCHEMA, 1068 'styles' => array( 1069 'blocks' => array( 1070 'core/group' => array( 1071 'elements' => array( 1072 'link' => array( 1073 'color' => array( 1074 'text' => 'blue', 1075 ), 1076 ':hover' => array( 1077 'color' => array( 1078 'text' => 'navy', 1079 ), 1080 ), 1081 ), 1082 ), 1083 'mobile' => array( 1084 'elements' => array( 1085 'link' => array( 1086 'color' => array( 1087 'text' => 'red', 1088 ), 1089 ':hover' => array( 1090 'color' => array( 1091 'text' => 'darkred', 1092 ), 1093 ), 1094 ), 1095 ), 1096 ), 1097 ), 1098 ), 1099 ), 1100 ) 1101 ); 1102 1103 $link_selector = '.wp-block-group a:where(:not(.wp-element-button))'; 1104 1105 // Nodes are assembled in cascade order: default, responsive, pseudo, responsive pseudo. 1106 $link_node = array( 1107 'path' => array( 'styles', 'blocks', 'core/group', 'elements', 'link' ), 1108 'selector' => $link_selector, 1109 ); 1110 1111 $mobile_link_node = array( 1112 'path' => array( 'styles', 'blocks', 'core/group', 'mobile', 'elements', 'link' ), 1113 'selector' => $link_selector, 1114 'media_query' => '@media (width <= 480px)', 1115 ); 1116 1117 $hover_node = array( 1118 'path' => array( 'styles', 'blocks', 'core/group', 'elements', 'link' ), 1119 'selector' => $link_selector . ':hover', 1120 ); 1121 1122 $mobile_hover_node = array( 1123 'path' => array( 'styles', 'blocks', 'core/group', 'mobile', 'elements', 'link' ), 1124 'selector' => $link_selector . ':hover', 1125 'media_query' => '@media (width <= 480px)', 1126 ); 1127 1128 $actual_styles = $theme_json->get_styles_for_block( $link_node ); 1129 $actual_styles .= $theme_json->get_styles_for_block( $mobile_link_node ); 1130 $actual_styles .= $theme_json->get_styles_for_block( $hover_node ); 1131 $actual_styles .= $theme_json->get_styles_for_block( $mobile_hover_node ); 1132 1133 $default_link = ':root :where(.wp-block-group a:where(:not(.wp-element-button))){color: blue;}'; 1134 $mobile_link = '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button))){color: red;}}'; 1135 $default_hov = ':root :where(.wp-block-group a:where(:not(.wp-element-button)):hover){color: navy;}'; 1136 $mobile_hov = '@media (width <= 480px){:root :where(.wp-block-group a:where(:not(.wp-element-button)):hover){color: darkred;}}'; 1137 1138 $this->assertStringContainsString( $default_link, $actual_styles ); 1139 $this->assertStringContainsString( $mobile_link, $actual_styles ); 1140 $this->assertStringContainsString( $default_hov, $actual_styles ); 1141 $this->assertStringContainsString( $mobile_hov, $actual_styles ); 1142 1143 $this->assertLessThan( strpos( $actual_styles, $mobile_link ), strpos( $actual_styles, $default_link ) ); 1144 $this->assertLessThan( strpos( $actual_styles, $default_hov ), strpos( $actual_styles, $mobile_link ) ); 1145 $this->assertLessThan( strpos( $actual_styles, $mobile_hov ), strpos( $actual_styles, $default_hov ) ); 1146 $this->assertStringNotContainsString( ':hover:hover', $actual_styles ); 1147 } 1148 1149 /** 1150 * @ticket 65164 1151 */ 1152 public function test_get_styles_for_block_with_style_variations_and_responsive_block_gap() { 1153 register_block_style( 1154 'core/group', 1155 array( 1156 'name' => 'withGap', 1157 'label' => 'With Gap', 1158 ) 1159 ); 1160 1161 $theme_json = new WP_Theme_JSON( 1162 array( 1163 'version' => WP_Theme_JSON::LATEST_SCHEMA, 1164 'settings' => array( 1165 'spacing' => array( 1166 'blockGap' => true, 1167 ), 1168 ), 1169 'styles' => array( 1170 'blocks' => array( 1171 'core/group' => array( 1172 'variations' => array( 1173 'withGap' => array( 1174 'spacing' => array( 1175 'blockGap' => '5rem', 1176 ), 1177 'mobile' => array( 1178 'spacing' => array( 1179 'blockGap' => '2rem', 1180 ), 1181 ), 1182 ), 1183 ), 1184 ), 1185 ), 1186 ), 1187 ) 1188 ); 1189 1190 $metadata = array( 1191 'name' => 'core/group', 1192 'path' => array( 'styles', 'blocks', 'core/group' ), 1193 'selector' => '.wp-block-group', 1194 'css' => '.wp-block-group', 1195 'variations' => array( 1196 array( 1197 'path' => array( 'styles', 'blocks', 'core/group', 'variations', 'withGap' ), 1198 'selector' => '.is-style-withGap.wp-block-group', 1199 ), 1200 ), 1201 ); 1202 1203 $actual_styles = $theme_json->get_styles_for_block( $metadata ); 1204 1205 unregister_block_style( 'core/group', 'withGap' ); 1206 1207 $default_gap = ':root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flex){gap: 5rem;}'; 1208 $mobile_gap = ':root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flex){gap: 2rem;}'; 1209 1210 $this->assertStringContainsString( $default_gap, $actual_styles ); 1211 $this->assertStringContainsString( '@media (width <= 480px)', $actual_styles ); 1212 $this->assertStringContainsString( $mobile_gap, $actual_styles ); 1213 $this->assertLessThan( strpos( $actual_styles, $mobile_gap ), strpos( $actual_styles, $default_gap ) ); 1214 } 1215 1216 /** 1217 * @ticket 65164 1218 */ 1219 public function test_get_styles_for_block_outputs_tablet_responsive_styles_only() { 1220 register_block_type( 1221 'test/tablet-only', 1222 array( 1223 'api_version' => 3, 1224 ) 1225 ); 1226 1227 $theme_json = new WP_Theme_JSON( 1228 array( 1229 'version' => WP_Theme_JSON::LATEST_SCHEMA, 1230 'styles' => array( 1231 'blocks' => array( 1232 'test/tablet-only' => array( 1233 'tablet' => array( 1234 'color' => array( 1235 'text' => 'purple', 1236 ), 1237 ), 1238 ), 1239 ), 1240 ), 1241 ) 1242 ); 1243 1244 $tablet_metadata = array( 1245 'name' => 'test/tablet-only', 1246 'path' => array( 'styles', 'blocks', 'test/tablet-only', 'tablet' ), 1247 'selector' => '.wp-block-test-tablet-only', 1248 'media_query' => '@media (480px < width <= 782px)', 1249 ); 1250 1251 $actual_styles = $theme_json->get_styles_for_block( $tablet_metadata ); 1252 1253 unregister_block_type( 'test/tablet-only' ); 1254 1255 $this->assertStringContainsString( 1256 '@media (480px < width <= 782px){:root :where(.wp-block-test-tablet-only){color: purple;}}', 1257 $actual_styles 1258 ); 1259 $this->assertStringNotContainsString( '@media (width <= 480px)', $actual_styles ); 1260 } 1261 1262 /** 940 1263 * Tests that if an element has nothing but pseudo selector styles, they are still output by get_stylesheet. 941 1264 * … … 2857 3180 ), 2858 3181 ); 3182 $this->assertEqualSetsWithIndex( $expected, $actual ); 3183 } 3184 3185 /** 3186 * @covers WP_Theme_JSON::remove_insecure_properties 3187 * 3188 * @ticket 65164 3189 */ 3190 public function test_remove_insecure_properties_preserves_responsive_block_element_styles() { 3191 $actual = WP_Theme_JSON::remove_insecure_properties( 3192 array( 3193 'version' => WP_Theme_JSON::LATEST_SCHEMA, 3194 'styles' => array( 3195 'blocks' => array( 3196 'core/group' => array( 3197 'elements' => array( 3198 'link' => array( 3199 'color' => array( 3200 'text' => 'var:preset|color|dark-gray', 3201 ), 3202 'mobile' => array( 3203 'color' => array( 3204 'text' => 'var:preset|color|dark-pink', 3205 ), 3206 ), 3207 'tablet' => array( 3208 'color' => array( 3209 'text' => 'var:preset|color|dark-red', 3210 ), 3211 ), 3212 ), 3213 ), 3214 ), 3215 ), 3216 ), 3217 ) 3218 ); 3219 3220 $expected = array( 3221 'version' => WP_Theme_JSON::LATEST_SCHEMA, 3222 'styles' => array( 3223 'blocks' => array( 3224 'core/group' => array( 3225 'elements' => array( 3226 'link' => array( 3227 'color' => array( 3228 'text' => 'var(--wp--preset--color--dark-gray)', 3229 ), 3230 'mobile' => array( 3231 'color' => array( 3232 'text' => 'var(--wp--preset--color--dark-pink)', 3233 ), 3234 ), 3235 'tablet' => array( 3236 'color' => array( 3237 'text' => 'var(--wp--preset--color--dark-red)', 3238 ), 3239 ), 3240 ), 3241 ), 3242 ), 3243 ), 3244 ), 3245 ); 3246 3247 $this->assertEqualSetsWithIndex( $expected, $actual ); 3248 } 3249 3250 /** 3251 * @covers WP_Theme_JSON::remove_insecure_properties 3252 * 3253 * @ticket 65164 3254 */ 3255 public function test_remove_insecure_properties_preserves_responsive_elements_within_block_state() { 3256 $actual = WP_Theme_JSON::remove_insecure_properties( 3257 array( 3258 'version' => WP_Theme_JSON::LATEST_SCHEMA, 3259 'styles' => array( 3260 'blocks' => array( 3261 'core/group' => array( 3262 'mobile' => array( 3263 'elements' => array( 3264 'link' => array( 3265 'color' => array( 3266 'text' => 'var:preset|color|dark-pink', 3267 ), 3268 ), 3269 ), 3270 ), 3271 ), 3272 ), 3273 ), 3274 ) 3275 ); 3276 3277 $expected = array( 3278 'version' => WP_Theme_JSON::LATEST_SCHEMA, 3279 'styles' => array( 3280 'blocks' => array( 3281 'core/group' => array( 3282 'mobile' => array( 3283 'elements' => array( 3284 'link' => array( 3285 'color' => array( 3286 'text' => 'var(--wp--preset--color--dark-pink)', 3287 ), 3288 ), 3289 ), 3290 ), 3291 ), 3292 ), 3293 ), 3294 ); 3295 2859 3296 $this->assertEqualSetsWithIndex( $expected, $actual ); 2860 3297 }
Note: See TracChangeset
for help on using the changeset viewer.