Skip to content

Integrate Resizable Editor with Device Preview and add Responsive editing#75121

Merged
t-hamano merged 50 commits into
trunkfrom
integrate-device-preview-resizable-editor
Jun 26, 2026
Merged

Integrate Resizable Editor with Device Preview and add Responsive editing#75121
t-hamano merged 50 commits into
trunkfrom
integrate-device-preview-resizable-editor

Conversation

@t-hamano

@t-hamano t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor

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:

  1. Integration — Unifies device view and the resizable editor, which are currently mutually exclusive. In the post and template editors, you could only set the three specific device widths and nothing in between. Meanwhile, in the pattern and navigation editors, you could resize freely but could not fit the canvas to a specific device width.
  2. Relocating the Responsive editing UI — Moves the Responsive editing entry point into the device preview dropdown. Along with this, the responsive (viewport) style state becomes global rather than per-block: switching the device preview (or resizing the canvas) drives which viewport the block style edits in the inspector apply to.

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 the block-editor store 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) — getDeviceType now derives the device type from canvasWidth instead of a stored deviceType (and returns Desktop while zoomed out), and setDeviceType converts the device type to a width and dispatches setCanvasWidth rather 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 inside setCanvasWidth.
  • getSelectedBlockStyleState() (private) — The viewport in the returned state now comes from the global viewport state (getStyleStateViewport()) instead of the per-block stored value; the per-block pseudo state is unchanged.

Testing Instructions

Post Editor, Template Editor

  • Please note that the resize handles will not be visible initially when the canvas width corresponds to the Desktop view. This is because there is not enough space on the left or right side of the canvas for them to appear. This may be addressed in a follow-up PR. See Allow access to responsive canvas resizing beyond just for template parts #71210 (comment). In the post editor and template editor, you will need to use the device preview dropdown first.
  • In mobile or tablet width, expand the resize handles to their fullest extent. The resize handles should disappear and you should switch to desktop view.
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

  • Open the device preview dropdown and enable Responsive editing.
  • Select a block that supports styles. A viewport badge should appear in the block inspector.
  • Switch the device preview to Tablet or Mobile (or resize the canvas to the corresponding width). The viewport badge should follow the current device.
  • Change a style and confirm the edit applies only to the selected viewport, not to Desktop.
  • Switch back to Desktop and confirm the per-viewport edits are preserved and shown per device.
  • Disable Responsive editing and confirm the viewport editing state resets to default and the badge disappears.

@github-actions github-actions Bot added [Package] Editor /packages/editor [Package] Block editor /packages/block-editor [Package] Edit Post /packages/edit-post labels Feb 2, 2026
@github-actions

github-actions Bot commented Feb 2, 2026

Copy link
Copy Markdown

Size Change: +96 B (0%)

Total Size: 7.51 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 383 kB -106 B (-0.03%)
build/scripts/edit-post/index.min.js 53.3 kB -17 B (-0.03%)
build/scripts/editor/index.min.js 475 kB -54 B (-0.01%)
build/styles/editor/style-rtl.css 30.8 kB +67 B (+0.22%)
build/styles/editor/style-rtl.min.css 26.2 kB +62 B (+0.24%)
build/styles/editor/style.css 30.9 kB +80 B (+0.26%)
build/styles/editor/style.min.css 26.2 kB +64 B (+0.24%)

compressed-size-action

<PreviewDropdown
forceIsAutosaveable={ forceIsDirty }
disabled={ disablePreviewOption }
disabled={ isStylesCanvasActive }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@t-hamano t-hamano added [Type] Enhancement A suggestion for improvement. General Interface Parts of the UI which don't fall neatly under other labels. labels Feb 2, 2026
@t-hamano t-hamano marked this pull request as ready for review February 2, 2026 03:12
@github-actions

github-actions Bot commented Feb 2, 2026

Copy link
Copy Markdown

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 props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

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]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@t-hamano t-hamano self-assigned this Feb 2, 2026

@Mamaduka Mamaduka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread packages/editor/src/components/preview-dropdown/index.js Outdated
Comment thread packages/editor/src/components/resizable-editor/index.js Outdated
Comment thread packages/editor/src/components/visual-editor/index.js Outdated
Comment thread packages/editor/src/utils/device-type.js Outdated
@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

@Mamaduka Thanks for the review!

Can you elaborate a bit on why a new global state is needed rather than just using the previous method?

The reason I introduced the new canvasWidth state is because getDeviceType and setDeviceType are public APIs. I know several plugins depend on them, and those APIs need to continue to be available in the same way as before.

@Mamaduka

Mamaduka commented Feb 2, 2026

Copy link
Copy Markdown
Member

Yes, but what prevents us from using useResizeCanvas and the old system, but only enabling resizing when deviceType !== 'Desktop' and probably some other conditions depending on the post type.

I'm not saying that either option is better; I'm primarily curious about the technical decisions.

@tellthemachines

Copy link
Copy Markdown
Contributor

In resizable editors, getDeviceType always returns Desktop, so the block hiding feature does not work in resizable editors, such as the Pattern Editor and Navigation Editor.

Can you give me some steps to repro this? A quick check of the pattern editor by creating a new pattern, adding a block and selecting "hide on desktop" hides it successfully for me:

Screenshot 2026-02-02 at 4 59 54 pm

We're not only checking the device type but also the viewport size for the block hiding logic, so if it doesn't work in responsive editing there's something wrong with it 😅

@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

In resizable editors, getDeviceType always returns Desktop, so the block hiding feature does not work in resizable editors, such as the Pattern Editor and Navigation Editor.

Can you give me some steps to repro this?

@tellthemachines Can you try the following steps? I tested it on trunk (f2c4004).

  1. Appearance > Editor > Patterns
  2. Create a new pattern
  3. Enter some text into the default Paragraph block.
  4. Open the Hide block modal and check "Hide on Tablet”.
  5. Change the editor width slightly. The block will always be visible.
d0d26373639acd414cd859e8ce4e469a.mp4

@t-hamano

t-hamano commented Feb 2, 2026

Copy link
Copy Markdown
Contributor Author

Yes, but what prevents us from using useResizeCanvas and the old system, but only enabling resizing when deviceType !== 'Desktop' and probably some other conditions depending on the post type.

@Mamaduka Can you elaborate a bit more on your concerns? I may not be understanding you properly 😅

t-hamano and others added 2 commits June 25, 2026 15:31
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]>
@t-hamano

Copy link
Copy Markdown
Contributor Author

I personally don't mind if we have APIs like:

setActiveGlobalStyleState( { responsive: '@mobile' } )
getActiveGlobalStyleState(); // { responsive: '@mobile' }

setActiveBlockStyleState( { pseudo: ':hover' } );
getActiveBlockStyleState();  // { pseudo: ':hover' }

getActiveStyleStates(); // { responsive: '@mobile', pseudo: ':hover' }
hasActiveStyleState(); // true

Separately there's also the responsive editing toggle and the 'show states on canvas', which could become:

isEditingGlobalStyleState: true | false
isShowingBlockStyleState: true | false

I'm not really sure that isEditingGlobalStyleState is required though, I think it'd be better to only call setActiveGlobalStyleState when the toggle is active.

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.

t-hamano and others added 3 commits June 25, 2026 16:41
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]>
@t-hamano

Copy link
Copy Markdown
Contributor Author

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 } );

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 nikunj8866 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. In the Post editor, open the device-preview dropdown and enable Responsive editing.
  2. Select a block with colour support (e.g. Paragraph), on Desktop, and set a text colour - say purple.
  3. Switch the device preview to Mobile.
  4. Set a different text colour - say pink.
  5. Open the device-preview dropdown and disable Responsive editing.
  6. 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 tellthemachines left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for iterating on this Aki! Code LGTM now 🚀

@tellthemachines

Copy link
Copy Markdown
Contributor

@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!

@t-hamano t-hamano merged commit 9812cbe into trunk Jun 26, 2026
44 of 45 checks passed
@t-hamano t-hamano deleted the integrate-device-preview-resizable-editor branch June 26, 2026 01:39
@github-actions github-actions Bot added this to the Gutenberg 23.6 milestone Jun 26, 2026
@bgrgicak bgrgicak added the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Jun 26, 2026
@tellthemachines

Copy link
Copy Markdown
Contributor

I just cherry-picked this PR to the release/23.5 branch to get it included in the next release: b4d1b1f

tellthemachines added a commit that referenced this pull request Jun 26, 2026
…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]>
@tellthemachines tellthemachines removed the Backport to Gutenberg RC Pull request that needs to be backported to a Gutenberg release candidate (RC) label Jun 26, 2026
@talldan

talldan commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Thanks for working on this Aki! How it was working before was definitely confusing me so great to have a better solution in.

@youknowriad

Copy link
Copy Markdown
Contributor

The "responsive editing" toggle says: "Edits apply only to the current state". For me there are two things that are ambiguous here.

  • "Edits": which edits? I added / removed blocks and they applied to all states. So should this say "Style edits" or something like that.
  • "current state": Should this say "current viewport". State can mean other things

This is definitely an improvement though 👍

@youknowriad

Copy link
Copy Markdown
Contributor

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.

@t-hamano

Copy link
Copy Markdown
Contributor Author

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

@Mamaduka

Copy link
Copy Markdown
Member

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.

@youknowriad

Copy link
Copy Markdown
Contributor

Responsive styles should always be applied appropriately according to the current canvas width 🤔

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.

@talldan

talldan commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

I've also noticed that when Responsive Editing is active and Desktop is selected, a badge is being shown:

Screenshot 2026-06-29 at 10 39 45 am

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.

SainathPoojary pushed a commit to SainathPoojary/gutenberg that referenced this pull request Jun 29, 2026
…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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

General Interface Parts of the UI which don't fall neatly under other labels. [Package] Block editor /packages/block-editor [Package] Block library /packages/block-library [Package] Edit Post /packages/edit-post [Package] Editor /packages/editor [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow access to responsive canvas resizing beyond just for template parts