From 0f9d889bbcc84eb9683436bf590f571768b3af50 Mon Sep 17 00:00:00 2001 From: Khokan Sardar Date: Thu, 13 Aug 2026 08:26:13 +0530 Subject: [PATCH] Editor: Drop mixed-unit viewport breakpoint pairs in theme.json. A `settings.viewport` pair that mixes a relative `em`/`rem` breakpoint with an absolute `px` one is ordered against a fixed 16px base, but the generated media query keeps the original units. Because a media query resolves `em` and `rem` against the browser's default font size, the emitted `@tablet` range inverts into an empty range for readers who change that default, silently dropping the responsive styles and viewport visibility rules bound to it. Remove `tablet` when it does not share an absolute or relative unit type with `mobile`, alongside the existing check that removes it when it is not larger than `mobile`. Pairs that mix `em` and `rem` are kept, since a media query resolves both against the same base. Fixes #65865. --- src/wp-includes/class-wp-theme-json.php | 15 ++++++-- tests/phpunit/tests/theme/wpThemeJson.php | 43 +++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 7175d7a88747d..b2152fb6b8dd0 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -816,7 +816,8 @@ private static function get_viewport_breakpoint_value_in_pixels( $value ) { * the default breakpoints when no valid custom breakpoint is provided. When * only one breakpoint is valid, it remains keyed by its configured state and * uses a single max-width media query. When `tablet` is not larger than - * `mobile`, it is removed. + * `mobile`, or does not share an absolute or relative unit type with it, it + * is removed. * * @since 7.1.0 * @@ -852,7 +853,17 @@ private static function sanitize_viewport_settings( $viewport_settings ) { $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] ); if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px'] ) { - $sanitized['tablet'] = $breakpoints['tablet']['value']; + /* + * A media query resolves `em` and `rem` against the browser's default + * font size, so the order of a pair that mixes a relative unit with + * `px` only holds at the 16px base assumed above. + */ + $mobile_is_absolute = str_ends_with( $breakpoints['mobile']['value'], 'px' ); + $tablet_is_absolute = str_ends_with( $breakpoints['tablet']['value'], 'px' ); + + if ( $mobile_is_absolute === $tablet_is_absolute ) { + $sanitized['tablet'] = $breakpoints['tablet']['value']; + } } return $sanitized; diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index c2cda7bb158d0..5ef2d4d76e1ac 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -1248,6 +1248,49 @@ public function test_get_viewport_media_queries_omits_tablet_when_its_breakpoint ); } + /** + * @ticket 65865 + */ + public function test_get_viewport_media_queries_omits_tablet_when_breakpoints_mix_absolute_and_relative_units() { + $this->assertSame( + array( + '@mobile' => '@media (width <= 30em)', + '@desktop' => '@media (width > 30em)', + ), + WP_Theme_JSON::get_viewport_media_queries( + array( + 'mobile' => '30em', + 'tablet' => '580px', + ), + array( + 'include_desktop' => true, + ) + ) + ); + } + + /** + * @ticket 65865 + */ + public function test_get_viewport_media_queries_keeps_tablet_when_breakpoints_mix_em_and_rem() { + $this->assertSame( + array( + '@mobile' => '@media (width <= 30em)', + '@tablet' => '@media (30em < width <= 48rem)', + '@desktop' => '@media (width > 48rem)', + ), + WP_Theme_JSON::get_viewport_media_queries( + array( + 'mobile' => '30em', + 'tablet' => '48rem', + ), + array( + 'include_desktop' => true, + ) + ) + ); + } + /** * @ticket 65596 */