Integrate Resizable Editor with Device Preview and add Responsive editing#75121
Conversation
|
Size Change: +96 B (0%) Total Size: 7.51 MB 📦 View Changed
|
| <PreviewDropdown | ||
| forceIsAutosaveable={ forceIsDirty } | ||
| disabled={ disablePreviewOption } | ||
| disabled={ isStylesCanvasActive } |
There was a problem hiding this comment.
The only exception is StyleBook, where the device preview dropdown is not available, and the canvas is not resizable. I plan to remove this limitation in a follow-up.
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
Mamaduka
left a comment
There was a problem hiding this comment.
I haven't tested this thoroughly, but I've left some notes based on the initial review.
I think it's an interesting approach. Can you elaborate a bit on why a new global state is needed rather than just using the previous method?
|
@Mamaduka Thanks for the review!
The reason I introduced the new |
|
Yes, but what prevents us from using I'm not saying that either option is better; I'm primarily curious about the technical decisions. |
@tellthemachines Can you try the following steps? I tested it on trunk (f2c4004).
d0d26373639acd414cd859e8ce4e469a.mp4 |
@Mamaduka Can you elaborate a bit more on your concerns? I may not be understanding you properly 😅 |
The inspector content swapped on two parallel conditions: a `(hasPseudoState || isResponsiveEditing)` check for the state badges and `isBlockStyleStateSelected` for the rest. Listing each state type with `||` does not scale as more state types are added. Derive a single `isEditingStyleState` flag inside BlockInspectorSingleBlock (`isBlockStyleStateSelected || isResponsiveEditing`) and drive all of the inspector's conditional content from it. Enabling Responsive editing now switches the whole inspector into the style-state editing view, even at the default viewport. Co-Authored-By: Claude <[email protected]>
…wport The viewport is tracked globally while the pseudo state is per-block, so merging them in getSelectedBlockStyleState is easy to misread. Add a comment explaining the global viewport is injected here on purpose, so the reasoning is clear to future readers and reviewers. Co-Authored-By: Claude <[email protected]>
I am also in favor of this idea, but it may require significant changes, so it might be better to address it in a follow-up. It should be achievable with internal or private API updates without affecting the public API. |
The block style state colors tests selected a viewport (Mobile) through the block card "State" control. After reverting the viewport/style-state decoupling, that control only exposes pseudo states (and renders nothing for blocks without them, such as core/group), so the button never appears and the tests time out. Viewport selection is now global, driven by the device preview. Switch the tests to enable Responsive editing from the View dropdown and pick the Mobile device, matching the current UI. Co-Authored-By: Claude <[email protected]>
The selected-state check had been inlined with a blockType.attributes.style guard to stop non-style blocks from reacting to the global viewport. Now that the render flag is isEditingStyleState = isBlockStyleStateSelected || isResponsiveEditing, and the global viewport is non-default only while Responsive editing is on, that guard is redundant: both forms reduce to the same rendered result. Restore the simple `! isDefaultBlockStyleState( selectedBlockStyleState )` so the check no longer conflates block style support with state selection. Co-Authored-By: Claude <[email protected]>
The handler only forwards the per-block value to setSelectedBlockStyleState, and the caller already passes pseudo-only values, so the note about not writing the global viewport back was not pulling its weight here. Co-Authored-By: Claude <[email protected]>
|
I believe I have addressed all the feedback. |
The block card header no longer renders the viewport StateControl for blocks without pseudo-states (viewport states moved to the editor's device preview). This removes one focusable element before the Columns slider in the inspector, so the keyboard navigation test must Tab 5 times instead of 6 to reach it. Co-Authored-By: Claude <[email protected]>
| // Ensure the block is selected and slider control is visible in the inspector. | ||
| await expect( slider ).toBeVisible(); | ||
| await pageUtils.pressKeys( 'Tab', { times: 6 } ); | ||
| await pageUtils.pressKeys( 'Tab', { times: 5 } ); |
There was a problem hiding this comment.
This change is intentional. In this PR, the viewport state was removed from the block inspector, so if there is no pseudo state, the dropdown toggle button itself will no longer be rendered. Therefore, we need to reduce the number of focusable elements by one.
nikunj8866
left a comment
There was a problem hiding this comment.
While preparing the call for testing for this work, I came across behaviour around the Responsive editing toggle that I'd like to confirm is intended.
Steps I followed:
- In the Post editor, open the device-preview dropdown and enable Responsive editing.
- Select a block with colour support (e.g. Paragraph), on Desktop, and set a text colour - say purple.
- Switch the device preview to Mobile.
- Set a different text colour - say pink.
- Open the device-preview dropdown and disable Responsive editing.
- Save / publish and view the page on mobile width.
What I expected
With Responsive editing disabled, I expected all viewports to fall back to the Desktop value (purple).
What happened
The Mobile override (pink) persists - in the editor and on the published front end at mobile width. Disabling the toggle didn't change it.
reset-styling-bug.mp4
tellthemachines
left a comment
There was a problem hiding this comment.
Thanks for iterating on this Aki! Code LGTM now 🚀
|
@nikunj8866 I can confirm that is expected behaviour. The only thing that "enable Responsive editing" does is to make any edits you do in the block inspector controls apply exclusively to the active breakpoint. If you make mobile-specific edits and then toggle "enable Responsive editing" off, you'll still be able to view the mobile-only styles applied when the mobile breakpoint is active, even though the subsequent edits you make will apply to all breakpoints. It's good feedback that that interaction can be confusing though, so thanks for commenting! |
|
I just cherry-picked this PR to the release/23.5 branch to get it included in the next release: b4d1b1f |
…ting (#75121) * Integrate Resizable Editor with Device Preview * Fix lint error * Return early if enableResizing is false * Use hasCanvasWidth * getCanvasWidthByDeviceType: improde JSDoc and remove fallback value * PreviewDropdown: combine getters * Treat the canvas size as a range and determine the corresponding viewport from it. * Editor: remove unused canvasMinHeight destructuring in VisualEditor The merge kept canvasMinHeight in the useSelect destructuring, but it is not consumed in the component (only hasCanvasWidth is used). Drop the unused binding to satisfy the no-unused-vars lint rule. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Editor: use React.JSX.Element for DEVICE_TYPES icon type The bare global JSX namespace was removed in newer @types/react, so `JSX.Element` fails to resolve ("Cannot find namespace 'JSX'"). Use `React.JSX.Element`, which resolves via the global React namespace and matches the convention used elsewhere in the codebase. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Editor: import store index before actions in store actions test Importing `../actions` before the store index made the public actions module the outermost in the test's module graph. Through the `../actions` -> `./private-actions` -> dataviews -> store index circular import, the store index was re-entered mid-evaluation and ran `registerPrivateActions` before the private actions were defined, so `updateDeviceTypeForViewportState` registered as `undefined` and the test failed. Import the store index first, matching production load order where the index is always the outermost module. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Editor: remove unused ATTACHMENT_POST_TYPE import in header The constant is no longer referenced in the header component, so drop the dangling import. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Editor: drop unused canvasMinHeight select in VisualEditor `canvasMinHeight` was added to the useSelect mapping but never destructured from the result, so `getCanvasMinHeight()` ran on every render with no consumer. Remove the mapping and its now-unused selector. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * WIP * Block Inspector: Drop redundant false fallback for isResponsiveEditing All consumers evaluate isResponsiveEditing in a boolean context, so an undefined value behaves identically to false. The fallback added no value. Co-Authored-By: Claude <[email protected]> * Block states: Always show viewport badge when Responsive editing is enabled The device badge previously required the block to support a style attribute, hiding it for blocks without one even though the viewport is selected globally. Show the badge whenever Responsive editing is on, regardless of style support, and drop the now-unused hasStyleAttribute helper. Co-Authored-By: Claude <[email protected]> * Block states: Group viewport and pseudo props together in StateControlBadges Reorder props so viewport-related props (viewportStates, viewportValue) and pseudo-related props (pseudoStates, pseudoStateValue) are adjacent, improving readability. Co-Authored-By: Claude <[email protected]> * Block editor: Bump useResizeCanvas deprecation version to 7.1 Align the deprecation notice with the release the no-op change actually ships in. Co-Authored-By: Claude <[email protected]> * Block inspector: Tidy style-state comments Trim the explanatory comments around the global viewport / per-block pseudo split to keep them concise. Co-Authored-By: Claude <[email protected]> * Editor: Remove unused canvasMinHeight store state The canvasMinHeight action, selector, and reducer were wired into the store but had no consumers reading or setting them. Drop the dead state to keep the canvas store surface limited to canvasWidth. Co-Authored-By: Claude <[email protected]> * Editor: Move canvasWidth reducer next to renderingMode Group the canvas width reducer with the related rendering-mode state (where the former deviceType reducer lived) for readability. Co-Authored-By: Claude <[email protected]> * Editor: Group isResponsiveEditing reducer with canvas state Move the isResponsiveEditing reducer and its combineReducers entry next to renderingMode/canvasWidth so the related device-preview state stays together. Co-Authored-By: Claude <[email protected]> * Editor: Note future theme.json customization of device breakpoints Document that the currently hardcoded DEVICE_TYPES breakpoints are expected to become customizable via theme.json settings.viewport, so readers know the literals are a temporary baseline. Co-Authored-By: Claude <[email protected]> * Editor: Document the split canvas padding conditions Explain why vertical and horizontal padding now have separate triggers: vertical frames a width-constrained canvas, horizontal reserves space for the resize handles. Co-Authored-By: Claude <[email protected]> * Block editor: Memoize getSelectedBlockStyleState The selector built a fresh object on every call, so useSelect in BlockStyleControls saw a new selectedState reference each render and warned about unstable values / unnecessary re-renders. Wrap it in createSelector keyed on the viewport and per-block style state so the same inputs return a stable reference. Co-Authored-By: Claude <[email protected]> * Block inspector: Simplify style-state info gate Drop the showDeviceBadge and showStyleStateInfo locals and gate the style-state info area directly on hasPseudoState || isResponsiveEditing. This removes the style-support guard so the device badge shows for any block during Responsive editing, matching BlockStateBadges, and resolves the inconsistency where the outer guard suppressed the badge. Co-Authored-By: Claude <[email protected]> * Cover: Move overlay viewport-state test from unit to e2e The unit test selected a viewport state through the per-block "State: Default" dropdown, which no longer exists: viewport states are now chosen via the editor's global device preview (Responsive editing), which lives in @wordpress/editor and cannot be rendered from a block-library unit test. Driving the block editor store directly would require leaking the editor's sub-registry through the shared integration test helper, which is too invasive. Remove the broken unit test and its now-unused helper, and cover the behavior with an e2e test that exercises the real user flow: enable Responsive editing, switch the device preview to Tablet, and assert the Cover overlay controls are hidden. Co-Authored-By: Claude <[email protected]> * Editor: Describe responsive editing menu item scope Add an info description to the "Responsive editing" menu item clarifying that edits made in this mode apply only to the current state, so users understand the scope before enabling it. Co-Authored-By: Claude <[email protected]> * Editor: Sync viewport style state on any canvas width change The viewport badge followed the device preview dropdown but not a manual canvas resize: the dropdown imperatively synced the viewport style state, while the resize handle only updated the canvas width. Since the canvas width is the single source of truth for the device preview, centralize the sync in setCanvasWidth so the viewport style state stays in step however the width changes, and drop the now-redundant sync from the preview dropdown handler. Co-Authored-By: Claude <[email protected]> * Block editor: Drop the dead per-block viewport style state The viewport is selected globally via the device preview, yet the per-block selected style state still carried a viewport field. The reducer seeded it and the inspector wrote it back, but the selector always overrode it with the global viewport, so the per-block copy was never read. Stop storing it: remove the reducer seed, and pass only the changed value from the inspector so the global viewport is not written back. The selector keeps deriving the viewport from the global state. Co-Authored-By: Claude <[email protected]> * Preview dropdown: move Responsive editing item directly below device choices Place the Responsive editing menu item right after the device-type choices so the controls that affect viewport-specific editing stay grouped together, instead of being separated by the template group. Co-Authored-By: Claude <[email protected]> * Preview dropdown: add dynamic help text to device choices Show context-aware help text under Desktop, Tablet, and Mobile that reflects whether Responsive editing is enabled: when on, the text explains that edits apply to that breakpoint; when off, it clarifies the choice only previews the corresponding viewport. This makes the effect of selecting a device clear in both modes. Co-Authored-By: Claude <[email protected]> * Resizable editor: update canvas width live while dragging Previously the canvas width was only committed to the store on resize stop, so the viewport indicator (device icon) did not reflect the device type the user was about to select until the handle was dropped. Update the canvas width on every resize event so the indicator tracks the drag in real time. Extract the shared logic into updateCanvasWidth. Co-Authored-By: Claude <[email protected]> * Preview dropdown: derive device type from getDeviceType selector Subscribe to getDeviceType() instead of the raw canvas width and a local getDeviceTypeByCanvasWidth computation. The selector already returns the derived device type and normalizes zoom-out to Desktop, fixing the broken indicator when zooming out on a non-Desktop viewport. It also returns a stable string, so the dropdown only re-renders when the device type actually changes rather than on every canvas width tick. Co-Authored-By: Claude <[email protected]> * Block editor: clarify useResizeCanvas deprecation message Explain what the hook used to do and where the behavior moved, so anyone still calling the deprecated no-op understands that device preview is now handled by the editor canvas. Co-Authored-By: Claude <[email protected]> * Block editor: remove redundant style-state selector tests Drop the getStyleStateViewport tests and the 'always derives viewport from the global state' case. The former only exercised trivial property reads and a defensive fallback that the reducer never produces, and the latter asserted on a per-block viewport value that no action path can write. Remaining tests still cover the real behavior of these selectors. Co-Authored-By: Claude <[email protected]> * Block editor: fully decouple viewport from block style state The selected viewport was previously folded into the per-block style state object as a `viewport` key, conflating a global value (the active viewport) with per-block pseudo state. This made the data shape misleading and forced callers to compose and spread the two together. Separate the two across all layers: Store: - `getSelectedBlockStyleState` returns pseudo-only per-block state and no longer merges in the global viewport. - Rename the viewport plumbing to drop the misleading "style state" framing: getStyleStateViewport -> getViewportState, setStyleStateViewport -> setViewportState, the `styleStateViewport` reducer -> `viewportState`, SET_STYLE_STATE_VIEWPORT -> SET_VIEWPORT_STATE. Block style helpers: - getStyleForState / setStyleForState / isDefaultBlockStyleState / scopeResetAllFilterToState / getStyleStatePath take `viewportState` as its own argument instead of reading it off the state object. - The block style state context exposes `selectedState` and `viewportState` as separate fields, so useBlockStyleState() returns both without re-coupling them. Block library: - The post-featured-image dimension utilities and their callers take `viewportState` as its own argument instead of reading it off `selectedState`, matching the decoupled block-editor APIs. The block reads the viewport from `getViewportState` separately from the pseudo-only `getSelectedBlockStyleState`. Consistently name the value `viewportState` across arguments, object keys, and props. Co-Authored-By: Claude <[email protected]> * Revert "Block editor: fully decouple viewport from block style state" This reverts commit 7fa1f03. * Editor: Move isResponsiveEditing state to the block-editor store Responsive editing is an implicit part of the block style state system, which lives in block-editor / global-styles. Keeping its state in the editor store required bridging the value down to block-editor through a block editor setting (isResponsiveEditingKey). Move the reducer, action and selector to the block-editor store so the block inspector can read it directly, removing the settings bridge. The editor's device preview now dispatches/selects against the block-editor store. Co-Authored-By: Claude <[email protected]> * Block inspector: Unify style-state editing into a single flag The inspector content swapped on two parallel conditions: a `(hasPseudoState || isResponsiveEditing)` check for the state badges and `isBlockStyleStateSelected` for the rest. Listing each state type with `||` does not scale as more state types are added. Derive a single `isEditingStyleState` flag inside BlockInspectorSingleBlock (`isBlockStyleStateSelected || isResponsiveEditing`) and drive all of the inspector's conditional content from it. Enabling Responsive editing now switches the whole inspector into the style-state editing view, even at the default viewport. Co-Authored-By: Claude <[email protected]> * Block editor: Document why getSelectedBlockStyleState injects the viewport The viewport is tracked globally while the pseudo state is per-block, so merging them in getSelectedBlockStyleState is easy to misread. Add a comment explaining the global viewport is injected here on purpose, so the reasoning is clear to future readers and reviewers. Co-Authored-By: Claude <[email protected]> * E2E: Select viewport state via Responsive editing, not the State control The block style state colors tests selected a viewport (Mobile) through the block card "State" control. After reverting the viewport/style-state decoupling, that control only exposes pseudo states (and renders nothing for blocks without them, such as core/group), so the button never appears and the tests time out. Viewport selection is now global, driven by the device preview. Switch the tests to enable Responsive editing from the View dropdown and pick the Mobile device, matching the current UI. Co-Authored-By: Claude <[email protected]> * Block inspector: Restore isDefaultBlockStyleState for the selected check The selected-state check had been inlined with a blockType.attributes.style guard to stop non-style blocks from reacting to the global viewport. Now that the render flag is isEditingStyleState = isBlockStyleStateSelected || isResponsiveEditing, and the global viewport is non-default only while Responsive editing is on, that guard is redundant: both forms reduce to the same rendered result. Restore the simple `! isDefaultBlockStyleState( selectedBlockStyleState )` so the check no longer conflates block style support with state selection. Co-Authored-By: Claude <[email protected]> * Block inspector: Drop redundant comment on style state persistence The handler only forwards the per-block value to setSelectedBlockStyleState, and the caller already passes pseudo-only values, so the note about not writing the global viewport back was not pulling its weight here. Co-Authored-By: Claude <[email protected]> * E2E: Update tab count after removing viewport state control The block card header no longer renders the viewport StateControl for blocks without pseudo-states (viewport states moved to the editor's device preview). This removes one focusable element before the Columns slider in the inspector, so the keyboard navigation test must Tab 5 times instead of 6 to reach it. Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: t-hamano <[email protected]> Co-authored-by: Mamaduka <[email protected]> Co-authored-by: youknowriad <[email protected]> Co-authored-by: ramonjd <[email protected]> Co-authored-by: jasmussen <[email protected]> Co-authored-by: tellthemachines <[email protected]> Co-authored-by: talldan <[email protected]> Co-authored-by: nikunj8866 <[email protected]> Co-authored-by: stokesman <[email protected]> Co-authored-by: jameskoster <[email protected]> Co-authored-by: fcoveram <[email protected]>
|
Thanks for working on this Aki! How it was working before was definitely confusing me so great to have a better solution in. |
|
The "responsive editing" toggle says: "Edits apply only to the current state". For me there are two things that are ambiguous here.
This is definitely an improvement though 👍 |
|
The other confusing thing is that I had a custom mobile style for a block, and I was on "mobile viewport", if I disable "responsive editing" the style is gone (not talking about the sidebar, but from the preview)? I feel like the preview/rendering of viewports shouldn't depend on the "responsive editing" toggle, it should always render the current viewport's styles. |
Responsive styles should always be applied appropriately according to the current canvas width 🤔 responsive-style.mp4 |
|
I've been playing with the feature for the local meetup demo, and it's working well. Can confirm that responsive styles are remaining in preview after disabling "responsive editing". I've also noticed that alignments can't be set as device-specific, e.g., Group/Image might take up full width on smaller screens. It took me a moment to realize that these controls are still universal and don't account for devices. I don't know what the plan is here, but it might be better to hide them until they're working. |
This didn't work for me for sure, so maybe there's some flow to break it. I'll try to reproduce it at some point, I was also playing with hover styles at that point. |
|
I've also noticed that when Responsive Editing is active and Desktop is selected, a badge is being shown:
I think that's potentially confusing because changes on desktop apply to all breakpoints/viewports. I'll work on a PR (edit: PR is #79615) to revise a couple of the aspects mentioned. |
…ting (WordPress#75121) * Integrate Resizable Editor with Device Preview * Fix lint error * Return early if enableResizing is false * Use hasCanvasWidth * getCanvasWidthByDeviceType: improde JSDoc and remove fallback value * PreviewDropdown: combine getters * Treat the canvas size as a range and determine the corresponding viewport from it. * Editor: remove unused canvasMinHeight destructuring in VisualEditor The merge kept canvasMinHeight in the useSelect destructuring, but it is not consumed in the component (only hasCanvasWidth is used). Drop the unused binding to satisfy the no-unused-vars lint rule. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Editor: use React.JSX.Element for DEVICE_TYPES icon type The bare global JSX namespace was removed in newer @types/react, so `JSX.Element` fails to resolve ("Cannot find namespace 'JSX'"). Use `React.JSX.Element`, which resolves via the global React namespace and matches the convention used elsewhere in the codebase. Co-Authored-By: Claude Opus 4.8 <[email protected]> * Editor: import store index before actions in store actions test Importing `../actions` before the store index made the public actions module the outermost in the test's module graph. Through the `../actions` -> `./private-actions` -> dataviews -> store index circular import, the store index was re-entered mid-evaluation and ran `registerPrivateActions` before the private actions were defined, so `updateDeviceTypeForViewportState` registered as `undefined` and the test failed. Import the store index first, matching production load order where the index is always the outermost module. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Editor: remove unused ATTACHMENT_POST_TYPE import in header The constant is no longer referenced in the header component, so drop the dangling import. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * Editor: drop unused canvasMinHeight select in VisualEditor `canvasMinHeight` was added to the useSelect mapping but never destructured from the result, so `getCanvasMinHeight()` ran on every render with no consumer. Remove the mapping and its now-unused selector. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> * WIP * Block Inspector: Drop redundant false fallback for isResponsiveEditing All consumers evaluate isResponsiveEditing in a boolean context, so an undefined value behaves identically to false. The fallback added no value. Co-Authored-By: Claude <[email protected]> * Block states: Always show viewport badge when Responsive editing is enabled The device badge previously required the block to support a style attribute, hiding it for blocks without one even though the viewport is selected globally. Show the badge whenever Responsive editing is on, regardless of style support, and drop the now-unused hasStyleAttribute helper. Co-Authored-By: Claude <[email protected]> * Block states: Group viewport and pseudo props together in StateControlBadges Reorder props so viewport-related props (viewportStates, viewportValue) and pseudo-related props (pseudoStates, pseudoStateValue) are adjacent, improving readability. Co-Authored-By: Claude <[email protected]> * Block editor: Bump useResizeCanvas deprecation version to 7.1 Align the deprecation notice with the release the no-op change actually ships in. Co-Authored-By: Claude <[email protected]> * Block inspector: Tidy style-state comments Trim the explanatory comments around the global viewport / per-block pseudo split to keep them concise. Co-Authored-By: Claude <[email protected]> * Editor: Remove unused canvasMinHeight store state The canvasMinHeight action, selector, and reducer were wired into the store but had no consumers reading or setting them. Drop the dead state to keep the canvas store surface limited to canvasWidth. Co-Authored-By: Claude <[email protected]> * Editor: Move canvasWidth reducer next to renderingMode Group the canvas width reducer with the related rendering-mode state (where the former deviceType reducer lived) for readability. Co-Authored-By: Claude <[email protected]> * Editor: Group isResponsiveEditing reducer with canvas state Move the isResponsiveEditing reducer and its combineReducers entry next to renderingMode/canvasWidth so the related device-preview state stays together. Co-Authored-By: Claude <[email protected]> * Editor: Note future theme.json customization of device breakpoints Document that the currently hardcoded DEVICE_TYPES breakpoints are expected to become customizable via theme.json settings.viewport, so readers know the literals are a temporary baseline. Co-Authored-By: Claude <[email protected]> * Editor: Document the split canvas padding conditions Explain why vertical and horizontal padding now have separate triggers: vertical frames a width-constrained canvas, horizontal reserves space for the resize handles. Co-Authored-By: Claude <[email protected]> * Block editor: Memoize getSelectedBlockStyleState The selector built a fresh object on every call, so useSelect in BlockStyleControls saw a new selectedState reference each render and warned about unstable values / unnecessary re-renders. Wrap it in createSelector keyed on the viewport and per-block style state so the same inputs return a stable reference. Co-Authored-By: Claude <[email protected]> * Block inspector: Simplify style-state info gate Drop the showDeviceBadge and showStyleStateInfo locals and gate the style-state info area directly on hasPseudoState || isResponsiveEditing. This removes the style-support guard so the device badge shows for any block during Responsive editing, matching BlockStateBadges, and resolves the inconsistency where the outer guard suppressed the badge. Co-Authored-By: Claude <[email protected]> * Cover: Move overlay viewport-state test from unit to e2e The unit test selected a viewport state through the per-block "State: Default" dropdown, which no longer exists: viewport states are now chosen via the editor's global device preview (Responsive editing), which lives in @wordpress/editor and cannot be rendered from a block-library unit test. Driving the block editor store directly would require leaking the editor's sub-registry through the shared integration test helper, which is too invasive. Remove the broken unit test and its now-unused helper, and cover the behavior with an e2e test that exercises the real user flow: enable Responsive editing, switch the device preview to Tablet, and assert the Cover overlay controls are hidden. Co-Authored-By: Claude <[email protected]> * Editor: Describe responsive editing menu item scope Add an info description to the "Responsive editing" menu item clarifying that edits made in this mode apply only to the current state, so users understand the scope before enabling it. Co-Authored-By: Claude <[email protected]> * Editor: Sync viewport style state on any canvas width change The viewport badge followed the device preview dropdown but not a manual canvas resize: the dropdown imperatively synced the viewport style state, while the resize handle only updated the canvas width. Since the canvas width is the single source of truth for the device preview, centralize the sync in setCanvasWidth so the viewport style state stays in step however the width changes, and drop the now-redundant sync from the preview dropdown handler. Co-Authored-By: Claude <[email protected]> * Block editor: Drop the dead per-block viewport style state The viewport is selected globally via the device preview, yet the per-block selected style state still carried a viewport field. The reducer seeded it and the inspector wrote it back, but the selector always overrode it with the global viewport, so the per-block copy was never read. Stop storing it: remove the reducer seed, and pass only the changed value from the inspector so the global viewport is not written back. The selector keeps deriving the viewport from the global state. Co-Authored-By: Claude <[email protected]> * Preview dropdown: move Responsive editing item directly below device choices Place the Responsive editing menu item right after the device-type choices so the controls that affect viewport-specific editing stay grouped together, instead of being separated by the template group. Co-Authored-By: Claude <[email protected]> * Preview dropdown: add dynamic help text to device choices Show context-aware help text under Desktop, Tablet, and Mobile that reflects whether Responsive editing is enabled: when on, the text explains that edits apply to that breakpoint; when off, it clarifies the choice only previews the corresponding viewport. This makes the effect of selecting a device clear in both modes. Co-Authored-By: Claude <[email protected]> * Resizable editor: update canvas width live while dragging Previously the canvas width was only committed to the store on resize stop, so the viewport indicator (device icon) did not reflect the device type the user was about to select until the handle was dropped. Update the canvas width on every resize event so the indicator tracks the drag in real time. Extract the shared logic into updateCanvasWidth. Co-Authored-By: Claude <[email protected]> * Preview dropdown: derive device type from getDeviceType selector Subscribe to getDeviceType() instead of the raw canvas width and a local getDeviceTypeByCanvasWidth computation. The selector already returns the derived device type and normalizes zoom-out to Desktop, fixing the broken indicator when zooming out on a non-Desktop viewport. It also returns a stable string, so the dropdown only re-renders when the device type actually changes rather than on every canvas width tick. Co-Authored-By: Claude <[email protected]> * Block editor: clarify useResizeCanvas deprecation message Explain what the hook used to do and where the behavior moved, so anyone still calling the deprecated no-op understands that device preview is now handled by the editor canvas. Co-Authored-By: Claude <[email protected]> * Block editor: remove redundant style-state selector tests Drop the getStyleStateViewport tests and the 'always derives viewport from the global state' case. The former only exercised trivial property reads and a defensive fallback that the reducer never produces, and the latter asserted on a per-block viewport value that no action path can write. Remaining tests still cover the real behavior of these selectors. Co-Authored-By: Claude <[email protected]> * Block editor: fully decouple viewport from block style state The selected viewport was previously folded into the per-block style state object as a `viewport` key, conflating a global value (the active viewport) with per-block pseudo state. This made the data shape misleading and forced callers to compose and spread the two together. Separate the two across all layers: Store: - `getSelectedBlockStyleState` returns pseudo-only per-block state and no longer merges in the global viewport. - Rename the viewport plumbing to drop the misleading "style state" framing: getStyleStateViewport -> getViewportState, setStyleStateViewport -> setViewportState, the `styleStateViewport` reducer -> `viewportState`, SET_STYLE_STATE_VIEWPORT -> SET_VIEWPORT_STATE. Block style helpers: - getStyleForState / setStyleForState / isDefaultBlockStyleState / scopeResetAllFilterToState / getStyleStatePath take `viewportState` as its own argument instead of reading it off the state object. - The block style state context exposes `selectedState` and `viewportState` as separate fields, so useBlockStyleState() returns both without re-coupling them. Block library: - The post-featured-image dimension utilities and their callers take `viewportState` as its own argument instead of reading it off `selectedState`, matching the decoupled block-editor APIs. The block reads the viewport from `getViewportState` separately from the pseudo-only `getSelectedBlockStyleState`. Consistently name the value `viewportState` across arguments, object keys, and props. Co-Authored-By: Claude <[email protected]> * Revert "Block editor: fully decouple viewport from block style state" This reverts commit 7fa1f03. * Editor: Move isResponsiveEditing state to the block-editor store Responsive editing is an implicit part of the block style state system, which lives in block-editor / global-styles. Keeping its state in the editor store required bridging the value down to block-editor through a block editor setting (isResponsiveEditingKey). Move the reducer, action and selector to the block-editor store so the block inspector can read it directly, removing the settings bridge. The editor's device preview now dispatches/selects against the block-editor store. Co-Authored-By: Claude <[email protected]> * Block inspector: Unify style-state editing into a single flag The inspector content swapped on two parallel conditions: a `(hasPseudoState || isResponsiveEditing)` check for the state badges and `isBlockStyleStateSelected` for the rest. Listing each state type with `||` does not scale as more state types are added. Derive a single `isEditingStyleState` flag inside BlockInspectorSingleBlock (`isBlockStyleStateSelected || isResponsiveEditing`) and drive all of the inspector's conditional content from it. Enabling Responsive editing now switches the whole inspector into the style-state editing view, even at the default viewport. Co-Authored-By: Claude <[email protected]> * Block editor: Document why getSelectedBlockStyleState injects the viewport The viewport is tracked globally while the pseudo state is per-block, so merging them in getSelectedBlockStyleState is easy to misread. Add a comment explaining the global viewport is injected here on purpose, so the reasoning is clear to future readers and reviewers. Co-Authored-By: Claude <[email protected]> * E2E: Select viewport state via Responsive editing, not the State control The block style state colors tests selected a viewport (Mobile) through the block card "State" control. After reverting the viewport/style-state decoupling, that control only exposes pseudo states (and renders nothing for blocks without them, such as core/group), so the button never appears and the tests time out. Viewport selection is now global, driven by the device preview. Switch the tests to enable Responsive editing from the View dropdown and pick the Mobile device, matching the current UI. Co-Authored-By: Claude <[email protected]> * Block inspector: Restore isDefaultBlockStyleState for the selected check The selected-state check had been inlined with a blockType.attributes.style guard to stop non-style blocks from reacting to the global viewport. Now that the render flag is isEditingStyleState = isBlockStyleStateSelected || isResponsiveEditing, and the global viewport is non-default only while Responsive editing is on, that guard is redundant: both forms reduce to the same rendered result. Restore the simple `! isDefaultBlockStyleState( selectedBlockStyleState )` so the check no longer conflates block style support with state selection. Co-Authored-By: Claude <[email protected]> * Block inspector: Drop redundant comment on style state persistence The handler only forwards the per-block value to setSelectedBlockStyleState, and the caller already passes pseudo-only values, so the note about not writing the global viewport back was not pulling its weight here. Co-Authored-By: Claude <[email protected]> * E2E: Update tab count after removing viewport state control The block card header no longer renders the viewport StateControl for blocks without pseudo-states (viewport states moved to the editor's device preview). This removes one focusable element before the Columns slider in the inspector, so the keyboard navigation test must Tab 5 times instead of 6 to reach it. Co-Authored-By: Claude <[email protected]> --------- Co-authored-by: t-hamano <[email protected]> Co-authored-by: Mamaduka <[email protected]> Co-authored-by: youknowriad <[email protected]> Co-authored-by: ramonjd <[email protected]> Co-authored-by: jasmussen <[email protected]> Co-authored-by: tellthemachines <[email protected]> Co-authored-by: talldan <[email protected]> Co-authored-by: nikunj8866 <[email protected]> Co-authored-by: stokesman <[email protected]> Co-authored-by: jameskoster <[email protected]> Co-authored-by: fcoveram <[email protected]>


Note
This PR originally aimed to integrate Device Preview with the Resizable Editor. However, following discussion, its scope shifted to relocating the Responsive editing UI.
What?
This PR does two things:
How?
The core idea is to treat the canvas width (in pixels) as the single source of truth, instead of a discrete device type. The device preview dropdown becomes just a tool to set a specific canvas width, and resizing the canvas is the same operation expressed differently.
New private APIs
getCanvasWidth()/setCanvasWidth( width )(core/editor) — Gets/sets the canvas width in pixels. While Responsive editing is enabled, setting it also drives the viewport state.isResponsiveEditing()/setResponsiveEditing( enabled )(core/block-editor) — Gets/toggles whether Responsive editing is enabled (session-only). Lives in theblock-editorstore because it is an implicit part of the block style state system.getStyleStateViewport()/setStyleStateViewport( viewport )(core/block-editor) — Gets/sets the globally selected viewport state. Block style edits in the inspector apply to this viewport. The viewport is tracked globally, separate from the per-block style state.Changed / deprecated APIs
getDeviceType()/setDeviceType()(core/editor) —getDeviceTypenow derives the device type fromcanvasWidthinstead of a storeddeviceType(and returnsDesktopwhile zoomed out), andsetDeviceTypeconverts the device type to a width and dispatchessetCanvasWidthrather than storing the device type directly.useResizeCanvas()— Deprecated and turned into a no-op. This hook only existed for the old device view, which canvas width now replaces.updateDeviceTypeForViewportState()(private) — Removed. The viewport ↔ device-preview sync now lives insidesetCanvasWidth.getSelectedBlockStyleState()(private) — Theviewportin the returned state now comes from the global viewport state (getStyleStateViewport()) instead of the per-block stored value; the per-blockpseudostate is unchanged.Testing Instructions
Post Editor, Template Editor
4dd92e2f8dde1b20e416c7c63516bf3e.mp4
Pattern Editor
The resize handles are always visible in this editor. Make sure the device preview dropdown and canvas width work together nicely.
7f71a97bacedc7e0526cc396311b7b28.mp4
Responsive editing UI
defaultand the badge disappears.