Skip to content

[6.x] Parts-only card event, with Craft 5 card HTML handled by the Yii adapter - #20354

Merged
brandonkelly merged 2 commits into
bugfix/element-card-html-resolvingfrom
card-parts-event
Nov 4, 2026
Merged

brandonkelly merged 2 commits into
bugfix/element-card-html-resolvingfrom
card-parts-event

Conversation

@brandonkelly

Copy link
Copy Markdown
Member

Description

Counter-proposal to #20335, based on its branch so the diff shows only what changes. Both card renderers still build the same parts and fire one event, but core no longer carries $html.

elementCardHtml()  ─┐                                         ┌─ elementCardPartsHtml(parts)
                    ├─ elementCardParts() → ElementCardParts ─┤
elementCardData()  ─┘   fires ElementCardPartsResolving       └─ card data array
                        (element, context, parts — no html)

Yii adapter listener (only if EVENT_DEFINE_ELEMENT_CARD_HTML has handlers):
  parts → HTML → Craft 5 handlers → changed? → split back into parts
  • ElementCardHtmlResolving is renamed to ElementCardPartsResolving, with no $html, so there's one way to change a card and listeners can't override each other's parts changes.
  • The Dom\HTMLDocument parser moves from ElementHtml to the legacy Cp helper. HTML that isn't a card keeps the original footer, which holds the field's hidden input.
  • Both renderers give listeners identical parts: the selection checkbox is added at render time rather than stored in actionsHtml, and withThumb means the same thing in both.
  • ElementCards.vue passes cardAttributes.class through to <craft-card>, so classes listeners add show up on Matrix, relation and index cards.
  • ElementCardParts is a plain, non-final class without Arrayable, and headerHtml is renamed to labelHtml.

Tradeoff: Craft 5 handlers' HTML is always parsed back into parts, including for elementCardHtml(), so a handler that wraps the card in another element ends up with the wrapper as the card's content.

Evidence

  • Before: cardAttributes: {class: ['card', 'listener-class']} → <craft-card class="">; listeners got actionsHtml with the checkbox from elementCardHtml() but not from elementCardData(); a non-card Craft 5 override dropped the field's hidden input.
    After: each of those cases is covered by a test that fails on [6.x] Fire ElementCardPartsResolving for every element card #20335's branch and passes here (ElementCards.test.ts, ElementHtmlTest, adapter CpElementCardHtmlTest). The ElementCards Storybook story is pixel-identical before and after.

Related issues

…pter

ElementCardHtmlResolving is renamed to ElementCardPartsResolving and only
carries the card's parts. Both card renderers build the parts and fire the
event in one place, so listeners see the same parts everywhere.

The Yii adapter renders the parts for Craft 5 EVENT_DEFINE_ELEMENT_CARD_HTML
handlers and splits changed HTML back into parts, keeping the original footer
when the result isn't a card.

ElementCards.vue now passes server card classes through to <craft-card>.

Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3
Co-authored-by: Amp <amp@ampcode.com>
@github-actions

github-actions Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

No changed components detected in this Storybook.

resources/js — open Storybook

Changed components:

@brandonkelly
brandonkelly requested review from i-just and riasvdv and a balanced review from Copilot November 4, 2026 18:51

@riasvdv riasvdv 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.

👏 all for moving more *html stuff into the adapter as it’s difficult to deal with in inertia

Copilot AI 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.

🟡 Changes recommended

The HTML-to-parts conversion loses wrapper attributes and sibling markup from Craft 5 overrides, while the fallback can duplicate field inputs and submit the same element ID twice.

3 open issues
What changed in this PR

This PR makes element-card customization parts-only in core and moves Craft 5 HTML override handling into the Yii adapter, keeping customization shared across HTML and component-rendered cards.

Changes:

  • Renames the card event to ElementCardPartsResolving and unifies parts generation, thumbnail behavior, and selection-checkbox rendering.
  • Moves legacy HTML parsing into the Yii adapter and preserves the original footer for non-card overrides.
  • Passes server-provided classes to Vue cards and adds regression coverage.
File Description
yii2-adapter/​tests-laravel/​Helpers/​CpElementCardHtmlTest.php Tests legacy overrides and footer preservation.
yii2-adapter/​legacy/​helpers/​Cp.php Converts legacy HTML overrides back into card parts.
yii2-adapter/​legacy/​events/​DefineElementHtmlEvent.php Updates the replacement-event documentation reference.
tests/​Feature/​Cp/​ElementHtmlTest.php Tests shared event parts and render-time selection controls.
src/​Cp/​Html/​ElementHtml.php Unifies parts generation and removes legacy HTML parsing.
src/​Cp/​Events/​ElementCardPartsResolving.php Renames the event and removes its HTML property.
src/​Cp/​Data/​ElementCardParts.php Simplifies the parts class and renames headerHtml to labelHtml.
resources/​js/​modules/​elements/​components/​ElementCards.vue Preserves server-provided card classes.
resources/​js/​modules/​elements/​components/​ElementCards.test.ts Tests card-class propagation.
CHANGELOG.md Documents the event rename and legacy override behavior.

Review effort: Balanced

Comment thread yii2-adapter/legacy/helpers/Cp.php Resolved
Comment thread yii2-adapter/legacy/helpers/Cp.php Resolved
Comment thread yii2-adapter/legacy/helpers/Cp.php Resolved
A handler that wraps the card already includes its footer, so the original
footer is only restored when the override lacks it. Markup a handler adds
next to the card root now goes in the footer instead of being dropped.

Amp-Thread-ID: https://ampcode.com/threads/T-06e10032-984e-4027-9c79-c92ca17a25e3

Copilot AI 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.

🔵 Needs a closer look

The server’s element class causes quick-edit to target the non-focusable card host instead of its focusable list item, breaking focus restoration after the editor closes.

No open issues 🎉

1 previously missed issue

In code that hasn't changed since last review

Medium severity Prevent quick-edit focus lookup from selecting non-focusable card host

resources/​js/​modules/​elements/​components/​ElementCards.vue:104

Passing through the server’s element class makes quick-edit’s closest('.element') lookup select <craft-card> instead of its focusable list item. After double-clicking a selectable card’s body and closing the editor, focus restoration targets that non-focusable host and fails. Keep listener classes, but filter out the structural element class here so quick-edit still finds the list item.

3 resolved since last review

Review effort: Balanced

@brandonkelly
brandonkelly merged commit c9edd68 into bugfix/element-card-html-resolving Nov 4, 2026
21 checks passed
@brandonkelly
brandonkelly deleted the card-parts-event branch November 4, 2026 19:37

Copy link
Copy Markdown
Member Author

Re: Copilot's follow-up review: agreed. Because the server's element class reached <craft-card>, quick-edit's closest('.element') found the card instead of its focusable <li>, so focus couldn't be restored after the editor closed. That was caused by this PR's class pass-through.

This PR is already merged, so the fix is on #20335 in 2f1ccc2. The element and error classes now stay only on the list item, which itemClass() already gives them. Other server classes still reach the card. The ElementCards test now checks that the card doesn't get element and that closest('.element') is the <li>. It fails without the fix.

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.

4 participants