From 00e4b406a5181d9776c006830ac746b4d6189770 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Wed, 9 Sep 2026 11:03:03 +0800 Subject: [PATCH 1/3] fix: stackable should follow editor breakpoints from theme if valid --- gulpfile.js | 11 +-- src/components/block-css/index.js | 9 ++- src/components/block-css/util.js | 36 +++++++++- src/editor-settings.php | 16 +++++ src/styles/breakpoints.scss | 110 +++++++++++++++++++++--------- src/styles/cssvars.scss | 24 +++++-- 6 files changed, 160 insertions(+), 46 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 94b00421da..f00aec03d2 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -571,15 +571,10 @@ exit; gulp.task( 'style-editor', function() { return gulp.src( [ path.resolve( __dirname, './src/**/editor.scss' ), '!' + path.resolve( __dirname, './src/deprecated/**/editor.scss' ) ] ) - // Override the breakpoints in the editor in - // src/styles/breakpoints.scss, we do it here because there are various - // files that use the breakpoints and it's easier to override it here. + // The active theme can change the editor preview widths at runtime. + // Use the editor's current device class rather than fixed Sass breakpoints. .pipe( sassVariables( { - // Match the Block Editor's fixed preview widths. getMediaQuery subtracts 1, - // so these default values target 781px tablet and 479px mobile in WordPress 7.0. - // https://github.com/WordPress/gutenberg/pull/74339 - '$desktop-width': 782, - '$tablet-width': 480, + '$use-editor-preview-classes': true, } ) ) .pipe( sass( sassOptions ).on( 'error', sass.logError ) ) .pipe( concat( 'editor_blocks.css' ) ) diff --git a/src/components/block-css/index.js b/src/components/block-css/index.js index 47956abd95..f43c24dfd5 100644 --- a/src/components/block-css/index.js +++ b/src/components/block-css/index.js @@ -17,6 +17,7 @@ import { getBlockUniqueClassname, getDependencyAttrnamesFast, getMediaQuery, + getViewportMediaQuery, isVersionSupported, prependClass, } from './util' @@ -31,6 +32,7 @@ import { * External dependencies */ import { pick, kebabCase } from 'lodash' +import { settings } from 'stackable' /** * WordPress dependencies @@ -546,7 +548,12 @@ function createCssEdit( selector, rule, value, device = 'desktop', vendorPrefixe } ) - const mediaQuery = getMediaQuery( device, tabletBreakpoint, mobileBreakpoint ) + const editorBreakpoints = settings.stackable_editor_breakpoints || {} + const mediaQuery = getViewportMediaQuery( device, settings.stackable_editor_viewport_breakpoints ) || getMediaQuery( + device, + editorBreakpoints.tablet || tabletBreakpoint, + editorBreakpoints.mobile || mobileBreakpoint + ) if ( mediaQuery ) { css = `\n${ mediaQuery } {${ css }\n}` } diff --git a/src/components/block-css/util.js b/src/components/block-css/util.js index 475d8968f9..43990508fd 100644 --- a/src/components/block-css/util.js +++ b/src/components/block-css/util.js @@ -33,7 +33,41 @@ export const getMediaQuery = ( devices = 'desktop', breakDesktop = 1024, breakTa } else if ( devices === 'mobile' ) { return '@media screen and (max-width: ' + ( breakTablet - 1 ) + 'px)' } - return null + return null +} + +/** + * Forms a media query string from WordPress theme.json viewport settings. + * + * WordPress 7.1 allows themes to configure these values and uses the same + * ranges for responsive editor previews. Unlike getMediaQuery, these are + * maximum viewport widths rather than the start of the next device range. + * + * @param {string} devices A list of devices: desktop, tablet or mobile. + * @param {Object} viewports WordPress viewport settings. + * @param {string} viewports.tablet Maximum Tablet viewport width. + * @param {string} viewports.mobile Maximum Mobile viewport width. + * @return {string|null} A media query, or null for missing settings. + */ +export const getViewportMediaQuery = ( devices = 'desktop', viewports = {} ) => { + const { tablet, mobile } = viewports + if ( ! tablet || ! mobile ) { + return null + } + + if ( devices === 'desktopTablet' ) { + return `@media screen and (width > ${ mobile })` + } else if ( devices === 'desktopOnly' ) { + return `@media screen and (width > ${ tablet })` + } else if ( devices === 'tablet' ) { + return `@media screen and (width <= ${ tablet })` + } else if ( devices === 'tabletOnly' ) { + return `@media screen and (width > ${ mobile }) and (width <= ${ tablet })` + } else if ( devices === 'mobile' ) { + return `@media screen and (width <= ${ mobile })` + } + + return null } /** diff --git a/src/editor-settings.php b/src/editor-settings.php index c07d859db4..2604409b93 100644 --- a/src/editor-settings.php +++ b/src/editor-settings.php @@ -321,6 +321,22 @@ public function add_settings( $settings ) { $settings['stackable_enable_heading_default_theme_margins_non_posts'] = get_option( 'stackable_enable_heading_default_theme_margins_non_posts' ); $settings['stackable_icon_list_block_default_icon'] = get_option( 'stackable_icon_list_block_default_icon' ); + // WordPress 7.1 allows themes define the Tablet and Mobile editor preview + // breakpoints in theme.json. Keep Stackable's generated editor CSS in sync + // with those previews when the active theme provides valid values. + $viewport_breakpoints = function_exists( 'wp_get_global_settings' ) ? wp_get_global_settings( array( 'viewport' ) ) : array(); + if ( + is_array( $viewport_breakpoints ) && + isset( $viewport_breakpoints['tablet'], $viewport_breakpoints['mobile'] ) && + is_string( $viewport_breakpoints['tablet'] ) && + is_string( $viewport_breakpoints['mobile'] ) + ) { + $settings['stackable_editor_viewport_breakpoints'] = array( + 'tablet' => $viewport_breakpoints['tablet'], + 'mobile' => $viewport_breakpoints['mobile'], + ); + } + // Inserter variations are registered before the block Edit component renders, // so provide the post type here. $current_screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; diff --git a/src/styles/breakpoints.scss b/src/styles/breakpoints.scss index 472d4608e8..2620a55b36 100644 --- a/src/styles/breakpoints.scss +++ b/src/styles/breakpoints.scss @@ -1,32 +1,75 @@ -// These breakpoints are also overridden by gulpfile.js when building styles for -// the editor. We need to define these as integers here because in our -// definition in gulpfile.js, we cannot use px. +// The editor build uses its selected device class instead of viewport media +// queries. The active theme controls the preview width at runtime, so Sass +// cannot safely provide a fixed editor breakpoint. $desktop-width: 1024 !default; $tablet-width: 768 !default; +$use-editor-preview-classes: false !default; -@mixin desktop { - @media only screen and (min-width: #{$desktop-width + 0px}) { +@mixin editor-preview-device( $device ) { + @at-root .stk-preview-device-#{ $device } #{ & } { @content; } } + +@mixin desktop { + @if $use-editor-preview-classes { + @include editor-preview-device( desktop ) { + @content; + } + } @else { + @media only screen and (min-width: #{$desktop-width + 0px}) { + @content; + } + } +} @mixin desktop-tablet { - @media only screen and (min-width: #{$tablet-width + 0px}) { - @content; + @if $use-editor-preview-classes { + @include editor-preview-device( desktop ) { + @content; + } + @include editor-preview-device( tablet ) { + @content; + } + } @else { + @media only screen and (min-width: #{$tablet-width + 0px}) { + @content; + } } } @mixin tablet { - @media only screen and (min-width: #{$tablet-width + 0px}) and (max-width: #{$desktop-width - 1px}) { - @content; + @if $use-editor-preview-classes { + @include editor-preview-device( tablet ) { + @content; + } + } @else { + @media only screen and (min-width: #{$tablet-width + 0px}) and (max-width: #{$desktop-width - 1px}) { + @content; + } } } @mixin tablet-mobile { - @media only screen and (max-width: #{$desktop-width - 1px}) { - @content; + @if $use-editor-preview-classes { + @include editor-preview-device( tablet ) { + @content; + } + @include editor-preview-device( mobile ) { + @content; + } + } @else { + @media only screen and (max-width: #{$desktop-width - 1px}) { + @content; + } } } @mixin mobile { - @media only screen and (max-width: #{$tablet-width - 1px}) { - @content; + @if $use-editor-preview-classes { + @include editor-preview-device( mobile ) { + @content; + } + } @else { + @media only screen and (max-width: #{$tablet-width - 1px}) { + @content; + } } } @@ -41,28 +84,31 @@ $tablet-width: 768 !default; * These dummy styles are removed by gulpfile.js in the `style-editor` and * `style` tasks. */ -@include desktop { - .z { - opacity: 1; + +@if not $use-editor-preview-classes { + @include desktop { + .z { + opacity: 1; + } } -} -@include desktop-tablet { - .z { - opacity: 1; + @include desktop-tablet { + .z { + opacity: 1; + } } -} -@include tablet { - .z { - opacity: 1; + @include tablet { + .z { + opacity: 1; + } } -} -@include tablet-mobile { - .z { - opacity: 1; + @include tablet-mobile { + .z { + opacity: 1; + } } -} -@include mobile { - .z { - opacity: 1; + @include mobile { + .z { + opacity: 1; + } } } diff --git a/src/styles/cssvars.scss b/src/styles/cssvars.scss index 7af78941b3..f366774a3b 100644 --- a/src/styles/cssvars.scss +++ b/src/styles/cssvars.scss @@ -75,22 +75,38 @@ $_cssvars: (); } @if length( $tablet ) > 0 { - @include tablet { - :root { + @if $use-editor-preview-classes { + body.stk-preview-device-tablet { @each $name, $value in $tablet { --stk-#{ $name }: #{ $value }; } } + } @else { + @include tablet { + :root { + @each $name, $value in $tablet { + --stk-#{ $name }: #{ $value }; + } + } + } } } @if length( $mobile ) > 0 { - @include mobile { - :root { + @if $use-editor-preview-classes { + body.stk-preview-device-mobile { @each $name, $value in $mobile { --stk-#{ $name }: #{ $value }; } } + } @else { + @include mobile { + :root { + @each $name, $value in $mobile { + --stk-#{ $name }: #{ $value }; + } + } + } } } } From 829d795e5d6256f477f70ff2945257047b3d4130 Mon Sep 17 00:00:00 2001 From: Arukuen Date: Wed, 9 Sep 2026 20:34:20 +0800 Subject: [PATCH 2/3] fix: remove strict selector to body --- src/styles/cssvars.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/styles/cssvars.scss b/src/styles/cssvars.scss index f366774a3b..6d69547628 100644 --- a/src/styles/cssvars.scss +++ b/src/styles/cssvars.scss @@ -76,7 +76,7 @@ $_cssvars: (); @if length( $tablet ) > 0 { @if $use-editor-preview-classes { - body.stk-preview-device-tablet { + .stk-preview-device-tablet { @each $name, $value in $tablet { --stk-#{ $name }: #{ $value }; } @@ -94,7 +94,7 @@ $_cssvars: (); @if length( $mobile ) > 0 { @if $use-editor-preview-classes { - body.stk-preview-device-mobile { + .stk-preview-device-mobile { @each $name, $value in $mobile { --stk-#{ $name }: #{ $value }; } From be4a191e8832deadad868faa71253e67a89584e9 Mon Sep 17 00:00:00 2001 From: bfintal Date: Fri, 11 Sep 2026 11:29:13 +0800 Subject: [PATCH 3/3] test: cover editor styles when theme.json sets custom viewports --- e2e/tests/editor-theme-viewports.spec.ts | 150 +++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 e2e/tests/editor-theme-viewports.spec.ts diff --git a/e2e/tests/editor-theme-viewports.spec.ts b/e2e/tests/editor-theme-viewports.spec.ts new file mode 100644 index 0000000000..03b733675f --- /dev/null +++ b/e2e/tests/editor-theme-viewports.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from 'e2e/test-utils' + +const TABLET_VIEWPORT = '1000px' +const MOBILE_VIEWPORT = '690px' + +const getUserGlobalStylesId = async requestUtils => { + const themes = await requestUtils.rest( { path: '/wp/v2/themes?status=active' } ) + const href = themes?.[ 0 ]?._links?.[ 'wp:user-global-styles' ]?.[ 0 ]?.href + if ( ! href ) { + return null + } + return String( href ).split( '/' ).pop() +} + +const setUserViewportSettings = async ( requestUtils, viewport ) => { + const id = await getUserGlobalStylesId( requestUtils ) + if ( ! id ) { + return null + } + + const current = await requestUtils.rest( { path: `/wp/v2/global-styles/${ id }` } ) + await requestUtils.rest( { + method: 'POST', + path: `/wp/v2/global-styles/${ id }`, + data: { + settings: { + ...( current.settings || {} ), + viewport, + }, + }, + } ) + return id +} + +const setEditorDeviceType = async ( page, deviceType ) => { + await page.evaluate( device => { + const dispatch = window.wp.data.dispatch + if ( dispatch( 'core/editor' )?.setDeviceType ) { + dispatch( 'core/editor' ).setDeviceType( device ) + return + } + if ( dispatch( 'core/edit-post' )?.__experimentalSetPreviewDeviceType ) { + dispatch( 'core/edit-post' ).__experimentalSetPreviewDeviceType( device ) + } + }, deviceType ) +} + +test.describe( 'Editor theme viewports', () => { + let pid = null + let stylesId = null + let previousSettings = null + + test.beforeEach( async ( { editor, admin, requestUtils } ) => { + const wpVersion = process.env.WP_VERSION || 'latest' + test.skip( wpVersion !== 'latest' && wpVersion < '7.1', 'settings.viewport requires WordPress 7.1.' ) + + const id = await getUserGlobalStylesId( requestUtils ) + test.skip( ! id, 'User global styles REST is unavailable.' ) + + stylesId = id + previousSettings = ( await requestUtils.rest( { + path: `/wp/v2/global-styles/${ id }`, + } ) ).settings || {} + + const saved = await setUserViewportSettings( requestUtils, { + tablet: TABLET_VIEWPORT, + mobile: MOBILE_VIEWPORT, + } ) + test.skip( ! saved, 'Could not write settings.viewport via Global Styles REST.' ) + + const after = await requestUtils.rest( { path: `/wp/v2/global-styles/${ id }` } ) + test.skip( + after?.settings?.viewport?.tablet !== TABLET_VIEWPORT || + after?.settings?.viewport?.mobile !== MOBILE_VIEWPORT, + 'Global Styles REST did not persist custom viewport settings.' + ) + + await admin.createNewPost( { title: 'Editor theme viewports' } ) + await editor.saveDraft() + pid = new URLSearchParams( new URL( editor.page.url() ).search ).get( 'post' ) + } ) + + test.afterEach( async ( { requestUtils } ) => { + if ( stylesId ) { + await requestUtils.rest( { + method: 'POST', + path: `/wp/v2/global-styles/${ stylesId }`, + data: { settings: previousSettings || {} }, + } ).catch( () => undefined ) + } + if ( pid ) { + await requestUtils.deletePost( pid ) + } + } ) + + test( 'tablet preview uses theme viewport settings for Stackable styles', async ( { + page, + editor, + } ) => { + await editor.insertBlock( { + name: 'stackable/text', + attributes: { + text: 'theme viewport preview', + fontSize: '16', + fontSizeTablet: '48', + }, + } ) + + const text = editor.canvas.locator( '[data-type="stackable/text"] p' ).first() + await expect( text ).toBeVisible() + + await setEditorDeviceType( page, 'Tablet' ) + + await expect.poll( async () => { + return page.evaluate( () => { + return window.wp.data.select( 'core/editor' )?.getDeviceType?.() || + window.wp.data.select( 'core/edit-post' )?.__experimentalGetPreviewDeviceType?.() || + '' + } ) + } ).toBe( 'Tablet' ) + + const preview = await page.evaluate( () => { + const editorSettings = window.wp.data.select( 'core/editor' )?.getEditorSettings?.() || {} + const blockSettings = window.wp.data.select( 'core/block-editor' )?.getSettings?.() || {} + const canvas = document.querySelector( 'iframe[name="editor-canvas"], iframe.edit-post-visual-editor__content-area' ) + const canvasWidth = canvas ? Math.round( canvas.getBoundingClientRect().width ) : null + const features = blockSettings.__experimentalFeatures || editorSettings.__experimentalFeatures || {} + return { + stackableViewports: window.stackable?.settings?.stackable_editor_viewport_breakpoints || null, + featuresViewport: features.viewport || null, + canvasWidth, + } + } ) + + expect( + preview.featuresViewport?.tablet === TABLET_VIEWPORT || + preview.stackableViewports?.tablet === TABLET_VIEWPORT, + `Editor did not see custom viewports: ${ JSON.stringify( preview ) }` + ).toBeTruthy() + + // Custom tablet is 1000px. The editor chrome can clamp the canvas below + // that, but it must stay wider than Stackable's old 781px query. + expect( + preview.canvasWidth, + `Tablet canvas should be wider than 781px so the pre-fix query misses. Got ${ JSON.stringify( preview ) }` + ).toBeGreaterThan( 781 ) + + await expect( text ).toHaveCSS( 'font-size', '48px' ) + } ) +} )