Skip to content

Convert an image-only role="img" graphic to a named group of images - #5253

Merged
chubes4 merged 1 commit into
trunkfrom
fix/issue-5233-blocks-engine
Nov 9, 2026
Merged

chubes4 merged 1 commit into
trunkfrom
fix/issue-5233-blocks-engine

Conversation

@chubes4

@chubes4 chubes4 commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

An element with role="img" that holds only images is one named graphic (ARIA makes its children presentational), but the classifier treated it as phrasing content because it is a <span>.

  • Beside a text span: SourceElementClassifier::hasOnlyPhrasingChildren() accepted the parent, so it took the inline-RichText paragraph route. SourceDom::carriesOnlyRichTextInlineImages() rejects role-bearing wrappers, so the RichText fallback gate fired and the whole parent became core/html.
  • Inside a link tile: this is the same root cause. ButtonLinkDispatcher::hasLinkWrapperChildren() saw only phrasing children, so the anchor went to paragraphHost() and hit the same gate, which produced core/html. Fallback reduction then reparsed that island and rebuilt the anchor as a core/button whose label held the images.

Fix:

  • Add SourceDom::isImageOnlyNamedGraphic(), which matches role="img" with no text and only <img> descendants.
  • In the two phrasing checks above, treat such a graphic as a flow child.
  • Lower it in InlineContentElementConverter to a core/group of core/image blocks. The group carries the accessible name in the Group's ariaLabel attribute.
  • Change BlockFactory to save the Group's aria-label, the same way core's ariaLabel block support does.

With this, the link tile goes through the existing link-wrapper group route, the same one used when the graphic is a <div>.

Before / after (reduced page r1.html)

<main><div class="image"><span class="mosaic" role="img" aria-label="Plates"><img src="a.png" alt=""><img src="b.png" alt=""></span><span>04</span></div></main>

Before (Studio import, released engine):

<!-- wp:html --><div class="image"><span class="mosaic" role="img" aria-label="Plates"><img src="…/placeholder.svg" alt=""><img src="…/placeholder.svg" alt=""></span><span>04</span></div><!-- /wp:html -->

After:

<!-- wp:group {"className":"image"} --><div class="wp-block-group image"><!-- wp:group {"className":"mosaic","ariaLabel":"Plates"} --><div class="wp-block-group mosaic" aria-label="Plates"><!-- wp:image {"className":"blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline"} --><figure class="wp-block-image blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline blocks-engine-image"><img src="a.png" alt=""/></figure><!-- /wp:image --><!-- wp:image {"className":"blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline"} --><figure class="wp-block-image blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline blocks-engine-image"><img src="b.png" alt=""/></figure><!-- /wp:image --></div><!-- /wp:group --><!-- wp:paragraph {"className":"blocks-engine-synthetic-paragraph"} --><p class="blocks-engine-synthetic-paragraph"><span>04</span></p><!-- /wp:paragraph --></div><!-- /wp:group -->

Link tile variant. Before, this was core/buttons > core/button, and the button label contained the <span role="img"> and its images. After:

<!-- wp:group {"className":"tile"} --><div class="wp-block-group tile"><!-- wp:group {"ariaLabel":"Plates"} --><div class="wp-block-group" aria-label="Plates"><!-- wp:image {"className":"blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline","linkDestination":"custom"} --><figure class="wp-block-image blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline blocks-engine-image blocks-engine-linked-image-figure"><a href="/x/"><img src="a.png" alt=""/></a></figure><!-- /wp:image --><!-- wp:image {"className":"blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline","linkDestination":"custom"} --><figure class="wp-block-image blocks-engine-synthetic-image-figure blocks-engine-synthetic-image-figure-inline blocks-engine-image blocks-engine-linked-image-figure"><a href="/x/"><img src="b.png" alt=""/></a></figure><!-- /wp:image --></div><!-- /wp:group --><!-- wp:paragraph {"className":"blocks-engine-synthetic-paragraph blocks-engine-propagated-link"} --><p class="blocks-engine-synthetic-paragraph blocks-engine-propagated-link"><a href="/x/"><mark class="index" style="background-color:transparent;color:inherit" role="none">04</mark></a></p><!-- /wp:paragraph --><!-- wp:paragraph {"className":"blocks-engine-synthetic-paragraph blocks-engine-propagated-link"} --><p class="blocks-engine-synthetic-paragraph blocks-engine-propagated-link"><a href="/x/"><mark class="copy" style="background-color:transparent;color:inherit" role="none"><strong>Title</strong><small>Sub</small></mark></a></p><!-- /wp:paragraph --></div><!-- /wp:group -->

The empty .overlay span is dropped here, as it already is when the graphic is a <div>. That behaviour is existing link-wrapper behaviour and is unchanged by this PR.

Tests

  • New tests/unit/role-img-image-graphic-native-group.php, picked up by tests/run.php discovery. It is one table with four cases: an unnamed graphic, a named single image, a named mosaic beside text, and a named mosaic in a link tile. Each case asserts:

    • no core/html and no core/button
    • one core/image per <img>
    • one group holding exactly those images, with ariaLabel and the saved aria-label
    • the "04" text is preserved
    • block validity passes

    Fails on trunk for all four cases (src stashed) and passes with the fix.

  • composer test ran locally on the rebased branch: 1313 tests, and only the two Chrome-only contracts failed (artwork-slideshow.php and component-interactions.php); Chrome is not available locally. composer lint and composer test:lint are clean.


AI disclosure: Claude (claude-fable-5-1) running in Pi wrote the fix, the test and this PR as a subagent of the Data Liberation import-loop orchestrator. Finalized outside Homeboy with Chris's authorization because Homeboy cook finalization is blocked (homeboy#16292).

An element with role="img" that holds only images is one named graphic,
but it was classified as phrasing content. Beside a sibling text span the
parent took the inline-RichText paragraph route, the role-bearing wrapper
failed the inline image gate, and the whole parent became core/html.
Inside a link tile the anchor took the same RichText route, fell back to
core/html, and fallback reduction then rebuilt it as a core/button whose
label held the images.

Treat such a graphic as a flow child in the phrasing and link-wrapper
checks, and lower it to a core/group of core/image blocks that carries
the accessible name through the Group's ariaLabel support. Serialize the
Group's aria-label as core's block support saves it.

Fixes #5233.
@chubes4
chubes4 force-pushed the fix/issue-5233-blocks-engine branch from b101559 to f74a9c1 Compare November 9, 2026 17:42
@chubes4
chubes4 merged commit ad2509b into trunk Nov 9, 2026
24 of 25 checks passed
@chubes4
chubes4 deleted the fix/issue-5233-blocks-engine branch November 9, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant