Skip to content

Pattern Editing: Nest content groups and make them selectable to reorder - #82550

Draft
fabiankaegy wants to merge 3 commits into
add/pattern-grouping-using-names-rebasedfrom
add/pattern-grouping-nesting-and-reordering
Draft

fabiankaegy wants to merge 3 commits into
add/pattern-grouping-using-names-rebasedfrom
add/pattern-grouping-nesting-and-reordering

Conversation

@fabiankaegy

@fabiankaegy fabiankaegy commented Sep 7, 2026 •

Copy link
Copy Markdown
Member

Caution

THIS IS AN EARLY DRAFT AND NOT READY FOR REVIEW YET

Important

Stacked PR. Base is add/pattern-grouping-using-names-rebased — @talldan's #79483 replayed onto trunk — so this diff is only the work on top.

What?

Named containers inside a pattern now nest, and a grouping row can be selected and reordered. Refs #75463.

Why?

Naming a nested container was a silent no-op, and a grouping row could be focused but not acted on.

How?

  • Grouping rows nest at any depth. Content panel headings show the path (Pricing table / Premium).
  • A grouping row is selectable; the block toolbar's mover reorders it.
  • canMoveBlock, useHasBlockToolbar and BlockSettingsDropdown each carve out grouping rows — the last so reordering cannot also restyle or unlock.
  • metadata.name is read via a new blockMetadataRevision reducer, so List View is not rebuilt per keystroke.
  • The drag handle is hidden: a disabled container exposes no drop zone, so a drag has nowhere to land.

Testing Instructions

  1. Paste the markup below into the code editor.
  2. Open List View and expand the pattern. The named containers appear as nested rows.
  3. Select a card row, then use Move left / Move right in the block toolbar.
  4. Select a heading inside a card — the Content panel heading reads Pricing table / Lite.
Pattern markup
<!-- wp:group {"metadata":{"patternName":"test/pricing","name":"Pricing, 3 columns"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:group {"metadata":{"name":"Pricing table"},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
<div class="wp-block-group"><!-- wp:group {"metadata":{"name":"Lite"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Lite</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For getting started.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Premium"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Premium</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For teams.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Expert"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Expert</h3>
<!-- /wp:heading -->

<!-- wp:paragraph -->
<p>For agencies.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:group --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->
Edge cases — deeply nested

The block names say what each case is. Design only and Empty should not become rows; the rest should.

<!-- wp:group {"metadata":{"patternName":"test/kitchen-sink","name":"Kitchen sink"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:group {"metadata":{"name":"Level 1"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:group {"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:group {"metadata":{"name":"Level 3 under unnamed"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":4} -->
<h4 class="wp-block-heading">Deep heading</h4>
<!-- /wp:heading --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Level 1"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:paragraph -->
<p>Duplicate name nested inside the same name.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Design only"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Twin"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":4} -->
<h4 class="wp-block-heading">Twin A</h4>
<!-- /wp:heading --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Twin"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:heading {"level":4} -->
<h4 class="wp-block-heading">Twin B</h4>
<!-- /wp:heading --></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Empty"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"></div>
<!-- /wp:group -->

<!-- wp:group {"metadata":{"name":"Holder"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:group {"metadata":{"name":"Inner"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:paragraph -->
<p>Only child is another named group.</p>
<!-- /wp:paragraph --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->

<!-- wp:columns {"metadata":{"name":"Grid"}} -->
<div class="wp-block-columns"><!-- wp:column {"metadata":{"name":"Col A"}} -->
<div class="wp-block-column"><!-- wp:heading {"level":4} -->
<h4 class="wp-block-heading">Col A heading</h4>
<!-- /wp:heading --></div>
<!-- /wp:column -->

<!-- wp:column {"metadata":{"name":"Col B"}} -->
<div class="wp-block-column"><!-- wp:heading {"level":4} -->
<h4 class="wp-block-heading">Col B heading</h4>
<!-- /wp:heading --></div>
<!-- /wp:column --></div>
<!-- /wp:columns -->

<!-- wp:group {"metadata":{"name":"Has a list view block"},"layout":{"type":"constrained"}} -->
<div class="wp-block-group"><!-- wp:gallery {"linkTo":"none"} -->
<figure class="wp-block-gallery has-nested-images columns-default is-cropped"><!-- wp:image -->
<figure class="wp-block-image"><img alt=""/></figure>
<!-- /wp:image --></figure>
<!-- /wp:gallery --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->

Screenshots or screencast

Same pattern and post in both.

Before After
List View and Content panel on the base branch: a flat list of headings and paragraphs under a single Pricing table label List View and Content panel with grouping: Lite, Premium and Expert appear as nested rows, and the Content panel headings read Pricing table/Lite, Pricing table/Premium and Pricing table/Expert

A selected grouping row gets the movers and nothing else:

The Lite grouping row selected in List View, with a block toolbar showing only the parent selector, the block icon and the move left and move right buttons

Open questions

  • Drag support: do it properly, or is mover-only enough for now?
  • Duplicate / Remove are suppressed by the content-only restriction. Add them back?

Use of AI Tools

Written with Claude Code; I reviewed it.

Lets a named container inside another named container become a group of its
own, so a named card inside a named grid keeps both rows instead of only the
outermost one. The previous top-most-only rule made a nested name a silent
no-op, which read as the feature being broken rather than deliberately capped.

Nesting mirrors the markup the author wrote: a container only becomes a row by
being named, so the result is as deep as the author asked for rather than as
deep as the block tree.

Because groups nest, a bare "Premium" heading no longer identifies a group on
its own — two cards under different parents can share a name. Headings are now
built from the whole group path ("Pricing table / Premium"), which also removes
the duplicated heading markup and styles that the content panel and the block
fields panel each carried.

Group membership reads `metadata.name`, and the List View tree must not depend
on `state.blocks.attributes` to see it: that map is replaced on every attribute
change, so depending on it rebuilds the tree on every keystroke. Trunk guards
against this with `getListViewClientIdsTree`'s referential-stability test. A
`blockMetadataRevision` reducer counts only the attribute updates that touch
`metadata`, mirroring how `patternName` is already handled, and the tree and
group selectors depend on that instead.
@github-actions

github-actions Bot commented Sep 7, 2026 •

Copy link
Copy Markdown

🤖 PR meta 🤖

🏷️ Labels

This pull request needs exactly one label indicating its type, and has 0.

  • Required: any label starting with [Type].
  • Found: none.

Read more about Type labels in Gutenberg. If you cannot add labels yourself, a reviewer can do it for you.

📦 Bundle size

Size Change: +335 B (0%)

Total Size: 8 MB

📦 View Changed
Filename Size Change
build/scripts/block-editor/index.min.js 480 kB +244 B (+0.05%)
build/styles/block-editor/style-rtl.css 20.7 kB +39 B (+0.19%)
build/styles/block-editor/style-rtl.min.css 17.4 kB +6 B (+0.03%)
build/styles/block-editor/style.css 20.7 kB +40 B (+0.19%)
build/styles/block-editor/style.min.css 17.4 kB +6 B (+0.03%)

eb76fdc Run

⚡ Performance

Show the results

Client side metrics exclude the server response time.

front-end-block-theme

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
timeToFirstByte 57.7 ms +14.21% -4.85% 61.05 ms +8.27% -8.03% -5.49%
largestContentfulPaint 94 ms +6.38% -6.38% 96 ms +4.17% -8.33% -2.08%
lcpMinusTtfb 33.1 ms +11.48% -5.14% 32 ms +4.69% -3.59% 3.44%
wpBeforeTemplate 29.84 ms +17.56% -7.21% 30.07 ms +18.16% -6.92% -0.76%
wpTemplate 24.24 ms +3.75% -4.17% 24.79 ms +2.58% -4.52% -2.22%
wpTotal 54.02 ms +12.79% -4.92% 57.15 ms +6.86% -8.36% -5.48%
wpMemoryUsage 7.52 MB +0% -0% 7.52 MB +0% -0% 0%
wpDbQueries 17 +0% -0% 17 +0% -0% 0%

front-end-classic-theme

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
timeToFirstByte 45.5 ms +13.96% -6.04% 45.95 ms +4.35% -5.55% -0.98%
largestContentfulPaint 94 ms +4.26% -6.38% 92 ms +4.35% -0% 2.17%
lcpMinusTtfb 46.1 ms +4.23% -1.74% 46.05 ms +4.34% -2.93% 0.11%
wpBeforeTemplate 26.6 ms +23.12% -2.97% 26.54 ms +12.55% -1.32% 0.23%
wpTemplate 14.57 ms +6.31% -3.29% 14.68 ms +7.7% -3% -0.75%
wpTotal 41.94 ms +14.5% -4.84% 42.53 ms +5.03% -4.96% -1.39%
wpMemoryUsage 5.51 MB +0% -0% 5.51 MB +0% -0% 0%
wpDbQueries 14 +0% -0% 14 +0% -0% 0%

media-processing

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
mediaProcessingJpeg 403.55 ms +1.93% -0.42% 404.14 ms +0.47% -0.87% -0.15%
mediaProcessingAvif 6175.08 ms +0.12% -0.14% 6211.89 ms +0.43% -0.2% -0.59%
mediaProcessingJpegToAvif 4380.77 ms +0.4% -0.07% 4402.55 ms +0.22% -0.16% -0.49%

media-upload

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
jpegUploadProcessing 1414.82 ms +38.98% -0.72% 1425.84 ms +0.33% -0.53% -0.77%
pngUploadProcessing 209.91 ms +4.31% -3.15% 206.2 ms +6.37% -2.57% 1.8%
largeJpegUploadProcessing 1423.61 ms +0.8% -1.01% 1410.52 ms +1.26% -0.6% 0.93%
multipleImageUploadProcessing 1544.15 ms +1.73% -1.02% 1584.9 ms +0.51% -0.77% -2.57%

post-editor

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
serverResponse 535.59 ms +6.06% -3.06% 511.73 ms +9.25% -5.75% 4.66%
firstPaint 252.42 ms +15.89% -8.77% 214.35 ms +20.17% -9.68% 17.76%
domContentLoaded 1449.54 ms +1.5% -4% 1344.61 ms +2% -3.76% 7.8%
loaded 1451.01 ms +1.51% -4.01% 1346.01 ms +2.01% -3.77% 7.8%
firstContentfulPaint 467.73 ms +3.95% -1.5% 449.86 ms +3.22% -5.71% 3.97%
firstBlock 3977.23 ms +0.75% -5.64% 3645.3 ms +1.81% -1.21% 9.11%
type 16.17 ms +2.6% -4.08% 15.54 ms +10.04% -4.12% 4.05%
typeWithoutInspector 14.81 ms +9.39% -2.7% 15.49 ms +9.62% -6.07% -4.39%
typeWithTopToolbar 18.89 ms +4.55% -8.84% 18.54 ms +5.77% -7.17% 1.89%
typeContainer 9.96 ms +7.53% -12.85% 9.68 ms +3.62% -8.57% 2.89%
focus 82.9 ms +8.61% -4.23% 78.16 ms +13.83% -5.25% 6.06%
firstFocus 182.33 ms +0% -0% 168.2 ms +0% -0% 8.4%
selectAll 607.52 ms +1.28% -0.62% 606.17 ms +2.43% -1.3% 0.22%
listViewOpen 78.49 ms +7.33% -11.82% 71.33 ms +24.14% -5.61% 10.04%
inserterOpen 23.34 ms +14.91% -12.47% 24.45 ms +5.19% -4.34% -4.54%
inserterHover 3.95 ms +7.34% -6.84% 3.87 ms +9.56% -4.39% 2.07%
inserterSearch 8.25 ms +6.18% -9.33% 8.34 ms +5.76% -4.68% -1.08%
loadPatterns 652.12 ms +5.52% -3.37% 656.14 ms +2.61% -3.77% -0.61%
wpTotal 524.72 ms +6.12% -3.04% 501.09 ms +9.27% -5.8% 4.72%
wpMemoryUsage 13.06 MB +0% -0% 13.06 MB +0% -0% 0%
wpDbQueries 54 +0% -1.85% 54 +0% -1.85% 0%

site-editor

Metric bd827ab add/pattern-grouping-using-names-rebased % Change
serverResponse 412.08 ms +1.55% -7.2% 391.1 ms +3.49% -0.91% 5.36%
firstPaint 270.78 ms +4.29% -22.24% 227.73 ms +27.8% -7.36% 18.9%
domContentLoaded 1361.47 ms +1.56% -0.63% 1363.08 ms +0.64% -0.96% -0.12%
loaded 1362.72 ms +1.56% -0.64% 1364.27 ms +0.66% -0.95% -0.11%
firstContentfulPaint 433.51 ms +3.12% -1.5% 438.31 ms +3.24% -2.02% -1.1%
firstBlock 4248.28 ms +0.78% -1.24% 4225.57 ms +0.5% -0.3% 0.54%
type 14.69 ms +5.79% -1.91% 15.29 ms +2.62% -7.98% -3.92%
navigate 100.27 ms +6.25% -8.26% 110.77 ms +9.44% -2.7% -9.48%
loadPatterns 1517.55 ms +2.02% -9.51% 1417.34 ms +10.62% -11.5% 7.07%
loadPages 1144.02 ms +7.43% -5.73% 1243.52 ms +17.97% -12.69% -8%
wpTotal 401.38 ms +1.49% -7.46% 380.33 ms +3.64% -1.03% 5.53%
wpMemoryUsage 12.07 MB +0% -0% 12.07 MB +0% -0% 0%
wpDbQueries 43 +2.33% -0% 43 +2.33% -0% 0%

eb76fdc Run

@github-actions github-actions Bot added the [Package] Block editor /packages/block-editor label Sep 7, 2026
@fabiankaegy
fabiankaegy force-pushed the add/pattern-grouping-nesting-and-reordering branch from 6a37f8f to 1497cc3 Compare September 7, 2026 15:34
Makes a grouping row something you can act on. Reviewers of #79483 all
reached for the same thing first — dragging a pricing card into the middle —
and found a row that could be focused but did nothing, which read as broken
rather than as restrained.

A grouping row is selectable, and selecting it gives the ordinary block
toolbar, whose mover is what reorders the group among its siblings. That also
answers the objection that an unselectable row breaks List View's heuristic
that every row is selectable: rather than making the row a special case, it
gives selection something to do.

Grouping rows and the faded out-of-section context rows are both disabled, but
they are not the same kind of row, and until now they shared one treatment. A
context row sits outside the pattern being edited and is genuinely inert. A
grouping row sits inside it and holds no content of its own, but it is a real
target: selectable, expandable, movable. `isFadedContext` now carries the inert
treatment and grouping rows opt out of it.

Three gates stood between selection and a working mover:

- `canMoveBlock` refuses the move twice over: a container is not a content
  block, and the parent it sits in is disabled. Both rules exist to stop
  editors restructuring a pattern's design, but reordering named siblings is
  the restructuring the pattern author opted into by naming them, so named
  grouping rows are carved out. The carve-out permits a reorder and nothing
  more: every lock that could forbid the move — preview mode, static inner
  content, `lock.move`, a `templateLock` of 'all' — is applied before it, and
  `moveBlocksToPosition` only skips its remove and insert checks when the move
  stays inside the same parent, so a group cannot be lifted out of its pattern.

- `useHasBlockToolbar` renders no toolbar at all for a disabled block, so
  there was nothing to reorder from. Content groups are carved out.

- `BlockSettingsDropdown` would then have offered Paste styles, Lock and Hide,
  which would make reordering a way around the design lock. It already
  restricts itself for content-only blocks, so grouping rows take the same
  restriction. The menu is left with nothing and so does not render.

The resulting toolbar is the block icon, the parent selector, and drag plus
movers — no design controls, because those are gated on the default editing
mode, which a grouping row does not have.

Because a grouping row is now actionable, it is no longer announced as
disabled, so the e2e assertion added with the base branch is inverted.

Also fixes the base branch's e2e test, which asserted that content blocks are
visible in List View without expanding the grouping row that now sits above
them. Introducing that row put the content a level deeper, so the assertion
could not pass; it fails on the base branch too.

Duplicate and remove are deliberately left out. They raise the question of
what an appender inserts once every card is gone, which is worth settling
separately.
@fabiankaegy
fabiankaegy force-pushed the add/pattern-grouping-nesting-and-reordering branch from 1497cc3 to acffbe8 Compare September 7, 2026 17:09
@fabiankaegy fabiankaegy changed the title Pattern Editing: Nest content groups and let them be reordered Pattern Editing: Nest content groups and make them selectable to reorder Sep 7, 2026
The toolbar movers reorder a grouping row, but the drag handle beside them
does nothing. Dragging needs a drop zone on the container the group sits in,
and that container is disabled, so `useInnerBlocksProps` gives it none. The
drag starts — the store reports the group as being dragged — but no drop
target is ever computed, so the drop is a no-op.

Enabling the container's drop zone is not a one-line change. Doing it from
the block's own `isDropZoneDisabled` flag is too late, because that flag is
what attaches the listeners and it would have to flip mid-drag; doing it
statically attaches the zone but still produced no drop indicator, so
something further down declines the target. List View dragging is a third
path again, through `useListViewDropZone`.

Rather than ship a handle that looks like it works, hide it for grouping rows
so the movers are the only reorder affordance on offer. Reinstating it is a
follow-up, and the e2e test asserts its absence so that reinstating it has to
be deliberate.
@ramonjd

ramonjd commented Sep 7, 2026

Copy link
Copy Markdown
Member

Thanks for moving this topic forward.

I only smoke tested and reordering and grouping appears to be working well. Nice!

What's required here as the first step. I assume getting #79483 ready for re-testing and merge? Maybe now is the time so there's plenty of time for testing before 7.2.

I'm not sure how much bandwidth folks have at the moment to carry it forward, but we can help test and review.

@talldan

talldan commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for continuing to build on this.

These giant AI generated PR descriptions are really challenging to review. Screenshots or videos go a long way to helping, so I'd appreciate if you could add some. I probably won't look at this PR until that gets sorted.

@fabiankaegy

Copy link
Copy Markdown
Member Author

@talldan yeah sorry about this. I had added:

Caution

THIS IS AN EARLY DRAFT AND NOT READY FOR REVIEW YET

to the top of the PR description but then an update overwrote it and I didn't notice 🤦

Will ping you if and when this ever becomes ready :)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Package] Block editor /packages/block-editor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants