From 34994ccaaa6c6e8ac02f64692ea958b64d5a7170 Mon Sep 17 00:00:00 2001 From: Jignesh Bhavani Date: Sun, 9 Aug 2026 22:40:25 +0530 Subject: [PATCH 1/2] Editor: order viewport breakpoints only against a comparable base Media queries resolve em and rem against the initial font size, so a px breakpoint cannot be ordered against a font-relative one. Keep tablet only when both breakpoints share a base, matching how it is already dropped when it is not larger than mobile. See #65833. --- src/wp-includes/class-wp-theme-json.php | 26 ++++- tests/phpunit/tests/theme/wpThemeJson.php | 111 ++++++++++++++++++++++ 2 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 1517605bf4aa7..6d19584939594 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -809,6 +809,22 @@ private static function get_viewport_breakpoint_value_in_pixels( $value ) { return 'px' === $unit ? $number : $number * 16; } + /** + * Returns the base a viewport breakpoint size is measured against. + * + * Media queries resolve `em` and `rem` against the initial font size, so both + * share a base and can be compared with each other. A `px` length cannot be + * compared with either. + * + * @since 7.1.0 + * + * @param string $value Valid viewport breakpoint size. + * @return string Either 'px' or 'font-relative'. + */ + private static function get_viewport_breakpoint_base( $value ) { + return str_ends_with( trim( $value ), 'px' ) ? 'px' : 'font-relative'; + } + /** * Sanitizes and normalizes viewport breakpoint settings. * @@ -818,6 +834,9 @@ private static function get_viewport_breakpoint_value_in_pixels( $value ) { * uses a single max-width media query. When `tablet` is not larger than * `mobile`, it is removed. * + * `tablet` is also removed when the two breakpoints are measured against + * different bases, since their order cannot be determined. + * * @since 7.1.0 * * @param mixed $viewport_settings Viewport settings from theme.json. @@ -836,6 +855,7 @@ private static function sanitize_viewport_settings( $viewport_settings ) { $breakpoints[ $breakpoint ] = array( 'value' => trim( $value ), 'px' => $px, + 'base' => self::get_viewport_breakpoint_base( $value ), ); } } @@ -851,7 +871,11 @@ private static function sanitize_viewport_settings( $viewport_settings ) { $sanitized = array( 'mobile' => $breakpoints['mobile']['value'] ); - if ( isset( $breakpoints['tablet'] ) && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px'] ) { + if ( + isset( $breakpoints['tablet'] ) + && $breakpoints['mobile']['base'] === $breakpoints['tablet']['base'] + && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px'] + ) { $sanitized['tablet'] = $breakpoints['tablet']['value']; } diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index c2cda7bb158d0..1b607e5ac38e2 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -1248,6 +1248,117 @@ public function test_get_viewport_media_queries_omits_tablet_when_its_breakpoint ); } + /** + * A `px` breakpoint cannot be ordered against a font-relative one, so the + * tablet range is dropped rather than emitted unordered. + * + * @ticket 65833 + * + * @dataProvider data_viewport_breakpoints_with_incomparable_units + * + * @param array $viewport_settings Viewport settings to sanitize. + * @param array $expected Expected media queries. + */ + public function test_get_viewport_media_queries_omits_tablet_when_breakpoint_units_are_not_comparable( $viewport_settings, $expected ) { + $this->assertSame( + $expected, + WP_Theme_JSON::get_viewport_media_queries( + $viewport_settings, + array( + 'include_desktop' => true, + ) + ) + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_viewport_breakpoints_with_incomparable_units() { + return array( + 'font-relative mobile, pixel tablet' => array( + 'viewport_settings' => array( + 'mobile' => '30em', + 'tablet' => '500px', + ), + 'expected' => array( + '@mobile' => '@media (width <= 30em)', + '@desktop' => '@media (width > 30em)', + ), + ), + 'pixel mobile, font-relative tablet' => array( + 'viewport_settings' => array( + 'mobile' => '400px', + 'tablet' => '30em', + ), + 'expected' => array( + '@mobile' => '@media (width <= 400px)', + '@desktop' => '@media (width > 400px)', + ), + ), + 'pixel mobile, rem tablet' => array( + 'viewport_settings' => array( + 'mobile' => '400px', + 'tablet' => '30rem', + ), + 'expected' => array( + '@mobile' => '@media (width <= 400px)', + '@desktop' => '@media (width > 400px)', + ), + ), + ); + } + + /** + * `em` and `rem` share a base, so they can be ordered against each other. + * + * @ticket 65833 + */ + 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 <= 40rem)', + '@desktop' => '@media (width > 40rem)', + ), + WP_Theme_JSON::get_viewport_media_queries( + array( + 'mobile' => '30em', + 'tablet' => '40rem', + ), + array( + 'include_desktop' => true, + ) + ) + ); + } + + /** + * @ticket 65833 + */ + public function test_viewport_settings_omit_tablet_when_breakpoint_units_are_not_comparable() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'viewport' => array( + 'mobile' => '30em', + 'tablet' => '500px', + ), + ), + ) + ); + + $this->assertSame( + array( + 'mobile' => '30em', + ), + $theme_json->get_raw_data()['settings']['viewport'] + ); + } + /** * @ticket 65596 */ From 4d14624e4fbd82955816f18931a16c1b7c5c584b Mon Sep 17 00:00:00 2001 From: Aki Hamano Date: Mon, 10 Aug 2026 22:41:07 +0900 Subject: [PATCH 2/2] Editor: sync viewport breakpoint base check with Gutenberg Co-Authored-By: Claude --- src/wp-includes/class-wp-theme-json.php | 20 +------ tests/phpunit/tests/theme/wpThemeJson.php | 71 +++++++++++++++-------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/src/wp-includes/class-wp-theme-json.php b/src/wp-includes/class-wp-theme-json.php index 6d19584939594..0440ec7206b0f 100644 --- a/src/wp-includes/class-wp-theme-json.php +++ b/src/wp-includes/class-wp-theme-json.php @@ -809,22 +809,6 @@ private static function get_viewport_breakpoint_value_in_pixels( $value ) { return 'px' === $unit ? $number : $number * 16; } - /** - * Returns the base a viewport breakpoint size is measured against. - * - * Media queries resolve `em` and `rem` against the initial font size, so both - * share a base and can be compared with each other. A `px` length cannot be - * compared with either. - * - * @since 7.1.0 - * - * @param string $value Valid viewport breakpoint size. - * @return string Either 'px' or 'font-relative'. - */ - private static function get_viewport_breakpoint_base( $value ) { - return str_ends_with( trim( $value ), 'px' ) ? 'px' : 'font-relative'; - } - /** * Sanitizes and normalizes viewport breakpoint settings. * @@ -855,7 +839,7 @@ private static function sanitize_viewport_settings( $viewport_settings ) { $breakpoints[ $breakpoint ] = array( 'value' => trim( $value ), 'px' => $px, - 'base' => self::get_viewport_breakpoint_base( $value ), + 'is_px' => str_ends_with( trim( $value ), 'px' ), ); } } @@ -873,7 +857,7 @@ private static function sanitize_viewport_settings( $viewport_settings ) { if ( isset( $breakpoints['tablet'] ) - && $breakpoints['mobile']['base'] === $breakpoints['tablet']['base'] + && $breakpoints['mobile']['is_px'] === $breakpoints['tablet']['is_px'] && $breakpoints['mobile']['px'] < $breakpoints['tablet']['px'] ) { $sanitized['tablet'] = $breakpoints['tablet']['value']; diff --git a/tests/phpunit/tests/theme/wpThemeJson.php b/tests/phpunit/tests/theme/wpThemeJson.php index 1b607e5ac38e2..e6d5605d93f69 100644 --- a/tests/phpunit/tests/theme/wpThemeJson.php +++ b/tests/phpunit/tests/theme/wpThemeJson.php @@ -1254,12 +1254,12 @@ public function test_get_viewport_media_queries_omits_tablet_when_its_breakpoint * * @ticket 65833 * - * @dataProvider data_viewport_breakpoints_with_incomparable_units + * @dataProvider data_viewport_breakpoints_without_a_shared_base * * @param array $viewport_settings Viewport settings to sanitize. * @param array $expected Expected media queries. */ - public function test_get_viewport_media_queries_omits_tablet_when_breakpoint_units_are_not_comparable( $viewport_settings, $expected ) { + public function test_get_viewport_media_queries_omits_tablet_when_breakpoints_do_not_share_a_base( $viewport_settings, $expected ) { $this->assertSame( $expected, WP_Theme_JSON::get_viewport_media_queries( @@ -1276,7 +1276,7 @@ public function test_get_viewport_media_queries_omits_tablet_when_breakpoint_uni * * @return array[] */ - public function data_viewport_breakpoints_with_incomparable_units() { + public function data_viewport_breakpoints_without_a_shared_base() { return array( 'font-relative mobile, pixel tablet' => array( 'viewport_settings' => array( @@ -1298,36 +1298,25 @@ public function data_viewport_breakpoints_with_incomparable_units() { '@desktop' => '@media (width > 400px)', ), ), - 'pixel mobile, rem tablet' => array( - 'viewport_settings' => array( - 'mobile' => '400px', - 'tablet' => '30rem', - ), - 'expected' => array( - '@mobile' => '@media (width <= 400px)', - '@desktop' => '@media (width > 400px)', - ), - ), ); } /** - * `em` and `rem` share a base, so they can be ordered against each other. + * `em` and `rem` resolve against the same base in a media query, so they can + * be ordered against each other. * * @ticket 65833 + * + * @dataProvider data_viewport_breakpoints_with_a_shared_base + * + * @param array $viewport_settings Viewport settings to sanitize. + * @param array $expected Expected media queries. */ - public function test_get_viewport_media_queries_keeps_tablet_when_breakpoints_mix_em_and_rem() { + public function test_get_viewport_media_queries_keeps_tablet_when_breakpoints_share_a_base( $viewport_settings, $expected ) { $this->assertSame( - array( - '@mobile' => '@media (width <= 30em)', - '@tablet' => '@media (30em < width <= 40rem)', - '@desktop' => '@media (width > 40rem)', - ), + $expected, WP_Theme_JSON::get_viewport_media_queries( - array( - 'mobile' => '30em', - 'tablet' => '40rem', - ), + $viewport_settings, array( 'include_desktop' => true, ) @@ -1335,10 +1324,42 @@ public function test_get_viewport_media_queries_keeps_tablet_when_breakpoints_mi ); } + /** + * Data provider. + * + * @return array[] + */ + public function data_viewport_breakpoints_with_a_shared_base() { + return array( + 'em mobile, rem tablet' => array( + 'viewport_settings' => array( + 'mobile' => '30em', + 'tablet' => '40rem', + ), + 'expected' => array( + '@mobile' => '@media (width <= 30em)', + '@tablet' => '@media (30em < width <= 40rem)', + '@desktop' => '@media (width > 40rem)', + ), + ), + 'rem mobile, em tablet' => array( + 'viewport_settings' => array( + 'mobile' => '30rem', + 'tablet' => '40em', + ), + 'expected' => array( + '@mobile' => '@media (width <= 30rem)', + '@tablet' => '@media (30rem < width <= 40em)', + '@desktop' => '@media (width > 40em)', + ), + ), + ); + } + /** * @ticket 65833 */ - public function test_viewport_settings_omit_tablet_when_breakpoint_units_are_not_comparable() { + public function test_viewport_settings_omit_tablet_when_breakpoints_do_not_share_a_base() { $theme_json = new WP_Theme_JSON( array( 'version' => WP_Theme_JSON::LATEST_SCHEMA,