Skip to content

fix(php-transformer): preserve associated label hosts and their static attributes - #2637

Merged
chubes4 merged 5 commits into
trunkfrom
fix/1617-associated-label-hosts
Oct 8, 2026
Merged

chubes4 merged 5 commits into
trunkfrom
fix/1617-associated-label-hosts

Conversation

@chubes4

@chubes4 chubes4 commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Preserves explicitly associated external <label for> hosts as editable native label blocks instead of lowering them to core/paragraph, and keeps their passive static attributes so authored selectors still match their real subject.

  • New AuthoredLabelBlockGenerator (consumer-namespaced companion block) owns external labels with a nonempty for and no nested controls. Static save keeps the <label> host, for, id, classes and inline declarations with no extra wrapper; the editor is standard RichText + useBlockProps.
  • Label hosts carry a sourceAttributes map for passive metadata (data-*/aria-* in the canonical name pattern, lang, dir, hidden, role, title, tabindex). Support/SourceAttribute centralizes the selector-identity names and declared-event vocabulary shared by DocumentRootAttributes, SourceDom::eventMetadata, PHP label projection and the generated editor/save filter. Event handlers, data-wp-* directives, URL/executable attributes and attempts to override owned fields are rejected in both PHP and the registered JS save filter.
  • SourceAttributeSubjects proves when data predicates resolve to a retained label/document-root host, so the author-selector preparer keeps the original predicates instead of minting captured-state blocks-engine-attribute-* markers.
  • External label admission is shared via FormControlClassifier::isExternalAssociatedLabel. Unassociated prose labels keep their paragraph role; wrapping labels and form-owned controls keep their existing contracts.

Fixes #1617.

Root cause

ReadableFormControlBlockConverter emitted external labels as an escaped core/paragraph, so neither the <label> host nor its for association survived. Type, adjacent-sibling, checked-state and pseudo-element selectors lost their subject. Core can't host this natively: core/paragraph always saves <p>, and core/group accepts a label tag but drops for/htmlFor from its attribute schema. A follow-up review also found that the first label host saved only for/id/classes/style, so label[data-role=caption]{…} lost its subject when data-role was discarded.

Before / after

  • Before: the parent converter fails the first association assertion (input + label association and adjacency survive compilation). The first label host fails reviewer reproduction preserves the actual data-role subject.
  • After: across 2 label orders × 390/768/1440 × 6 states (rest, live data-role mutation, expanded/negated predicates, native label click, body context mutation, opposite-order click), Gutenberg-saved markup and the real WordPress document canvas exactly match the source. The comparison covers attribute names/values, association, checked state, label/input boxes, painted text-range boxes, font metrics, paint, pseudo-element attr() content and outline. At rest the label is red 20px/24px at 390, red 22px/28px at 768 and green 24px/30px at 1440. Clicking each label toggles its intended native checkbox.
  • New regressions: tests/unit/associated-label.php (24 assertions), tests/unit/associated-label-static-attributes.php (41 assertions), and packages/blocks-engine/src/wp/__tests__/associated-label-save-validity.test.ts (Core capability gap, registered companion parse/save/reopen validity, hostile save maps, browser/native parity).

UBC evidence

This came from the fresh UBC residual-owner audit, where form/filter labels lost their source subject and checked-state paint. The fixtures reproduce that source structure in a neutral form. Full 21-route UBC acceptance is not claimed by this PR.

Verification (rebased on current trunk)

  • composer validate --strict: pass
  • composer test (PHP 8.3, 2G): pass
  • composer test:packaging: pass
  • tools/visual-parity npm test: pass
  • Package suite (typecheck, focused label test, test:bundle, test:svg-decode) passed on the candidate before the rebase. CI re-runs it here.

AI disclosure

AI-assisted under Chris Huber's direction. OpenAI GPT-6.1-sol, working through OpenCode, did the diagnosis, implementation and verification of both commits. Anthropic Claude (Opus), also through OpenCode, rebased onto current trunk, re-verified, and wrote this PR. Chris Huber directed the work and authorized landing.

@chubes4
chubes4 merged commit abb9f47 into trunk Oct 8, 2026
13 checks passed
@chubes4
chubes4 deleted the fix/1617-associated-label-hosts branch October 8, 2026 22:07
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.

Preserve visible labels and editable native blocks in imported HTML controls

1 participant