Preserve auto-fit grids, inline flex containers, and authored button font-family - #886
Merged
Conversation
## Summary Adds two deliberately RED contracts, one per confirmed defect: `repeat(auto-fit, …)` track lists must not be converted to a native WordPress grid layout attribute, and a flex container demoted to a css-owned core/group must keep its authored `display:flex`. ## Why `StyleResolutionTrait::autoRepeatMinimumColumnWidth()` accepts both `auto-fit` and `auto-fill` and converts both to `layout.minimumColumnWidth`. `wp-includes/block-supports/layout.php` hardcodes `auto-fill` in every branch that renders that attribute, so an authored `auto-fit` grid renders with empty tracks retained and the content crammed into part of the measure. `cssOwnedGroupAttributes()` drops the native `layout` attribute on demotion. Grids survive because `CSS_OWNED_GRID_CARRIER_PROPERTIES` rides their inline declarations to the generated stylesheet; there is no flex equivalent, so the block is marked `blocks-engine-css-owned-flow` while the declaration that makes it a flex container is dropped and the children stack. ## How `tests/unit/auto-fit-grid-carrier.php` covers inline and class-owned `auto-fit` track lists plus `auto-fill` controls proving the native conversion is retained, not abandoned. `tests/unit/css-owned-flex-carrier.php` covers inline row and column flex containers plus controls for a container with no authored display and for a class-owned flex declaration the author stylesheet already owns. ## Testing RED evidence at this commit: `php tests/unit/auto-fit-grid-carrier.php` exits 1 with 6 failed and 5 passed assertions; `php tests/unit/css-owned-flex-carrier.php` exits 1 with 6 failed and 6 passed assertions.
… intact Restricts the native grid-layout conversion to `auto-fill`, and carries the inline flex declarations of a container demoted to a css-owned core/group. `autoRepeatMinimumColumnWidth()` accepted `auto-fit` and `auto-fill` alike and converted both to `layout.minimumColumnWidth`. `wp-includes/block-supports/layout.php` hardcodes `auto-fill` in every branch that renders that attribute, so an authored `auto-fit` grid rendered with the empty tracks `auto-fit` collapses still retained, squeezing the content into part of the measure. Measured against a design, delivered tracks came back `267 267 267 267` where the design resolved `592 592 0 0`. `cssOwnedGroupAttributes()` drops the native `layout` attribute on demotion. Grids survive because `CSS_OWNED_GRID_CARRIER_PROPERTIES` rides their inline declarations to the generated stylesheet. There was no flex equivalent, so a container marked `blocks-engine-css-owned-flow` lost the declaration that made it a flex container and its children stacked. `auto-fit` now returns '' from the conversion and falls through to the same css-owned-grid carrier that already holds every other track list WordPress cannot express, so the author rule stays the single owner of the exact authored track list. `auto-fill` keeps converting natively. `CSS_OWNED_FLEX_CARRIER_PROPERTIES` plus `isCssOwnedFlexElement()`/`cssOwnedFlexAttributes()` mirror the grid carrier for flex containers. An inline display that overrides class-owned layout is skipped: #883 already carries those properties complete, at the non-important specificity tier, and forcing them would move them to the important tier. - [ ] `composer test:unit` — 0 failures across 37 scripts, including the two new contracts at 11 and 12 assertions. - [ ] `composer test` — 0 failures, 275 parity fixtures. Seven parity fixtures moved, each because it encoded one of the two defects: three auto-fit fixtures asserted the native conversion (one renamed, since `html-autofit-grid-maps-to-minimum-column-width` named the buggy contract), and four flex fixtures asserted output in which the authored `display:flex` had been dropped.
## Summary Adds a deliberately RED contract proving an authored `font-family` on a button-mapped rule must reach the core/button block attributes, and that the other carried declarations keep mapping exactly as they do today. ## Why `ButtonStyleResolver::BUTTON_TYPOGRAPHY` projects `fontSize`, `fontWeight`, `letterSpacing`, `lineHeight` and `textTransform` onto buttons and drops `font-family`. core/button registers a `fontFamily` attribute, which core injects only when the typography fontFamily support is enabled, so this is a dropped declaration rather than an unsupported one. The drop is not benign. The authored `.btn` class is consumed into block attributes, so the surviving author rule is rewritten behind `:where()` at zero specificity and matches nothing that can win. theme.json's `styles.elements.button.typography.fontFamily` then substitutes the design-direction typeface, which widens the measure enough to wrap hero CTAs onto two lines. ## How `tests/unit/button-font-family-carry.php` asserts the support exists before relying on it, pins the resolver's complete attribute output so no other carried declaration can drift, covers a rule that authors no font-family, and repeats the end-to-end shape from the report where the class is consumed. ## Testing RED evidence at this commit: `php tests/unit/button-font-family-carry.php` exits 1 with 5 failed and 4 passed assertions. The passing four include the support check, which must hold before the fix is safe to write.
## Summary Adds `fontFamily` to the typography subset projected onto core/button, and to the save-markup serializer that renders those attributes. ## Why `ButtonStyleResolver` carried a button rule's background, text color, border, radius, font-size, weight, letter-spacing and text-transform, but dropped `font-family`. core/button registers a `fontFamily` attribute, which core injects only when the typography fontFamily support is enabled, so the declaration was expressible all along. Because the authored class is consumed into block attributes, its surviving rule is rewritten behind `:where()` at zero specificity and cannot win. theme.json's `styles.elements.button.typography.fontFamily` therefore substituted the design-direction typeface, widening hero CTAs enough to wrap them onto two lines. ## How A raw authored family is a custom value rather than a preset slug, so it rides in `style.typography.fontFamily`. `BlockFactory` did not map that attribute either, so the block attribute alone would not have reached the rendered link; the serializer now emits `font-family` inline the way core's style engine does, ahead of the other typography declarations. Inline style outranks theme.json's element styles, so no `!important` bridge is needed and none was added. ## Testing - [ ] `composer test:unit` — 0 failures across 39 scripts. - [ ] `composer test` — 0 failures across 51 scripts, 275 parity fixtures, none edited for this defect. - [ ] `php tests/unit/button-font-family-carry.php` — 9 assertions, up from 4 passing and 5 failing at the RED commit.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Three defects that all change rendered geometry. Each was confirmed against real renders before any code changed, and each has a committed RED test that failed at the test-only commit.
Problem 1:
auto-fitis converted to a grid layout WordPress renders asauto-fillStyleResolutionTrait::autoRepeatMinimumColumnWidth()acceptedrepeat(auto-fit|auto-fill, minmax(W, 1fr))and converted BOTH to a native"layout":{"type":"grid","minimumColumnWidth":"W"}.wp-includes/block-supports/layout.phphardcodesauto-fillin every branch that rendersminimumColumnWidth, so the attribute cannot expressauto-fitat all. The two differ in rendered geometry:auto-fitcollapses tracks left empty,auto-fillretains them. An authoredauto-fitgrid therefore rendered with the empty tracks kept and the real content squeezed into part of the measure.Measured on a design fixture with three affected sections, design tracks versus delivered tracks:
minmax(260px), 2 itemsrepeat(auto-fit,minmax(260px,1fr))= 592 592 0 0minimumColumnWidth:260px, rendered asauto-fill= 267 267 267 267minmax(270px), 3 itemsrepeat(auto-fit,minmax(270px,1fr))= 389 389 389 0minimumColumnWidth:270px= 284 284 284 284minmax(280px), 2 itemsrepeat(auto-fit,minmax(280px,1fr))= 586 586 0minimumColumnWidth:280px= 366 366 366Across that whole page: native
minimumColumnWidthattributes 5 to 0,is-layout-gridclasses 10 to 0, deliveredauto-fittrack lists 0 to 5. A fourth grid on the page has 4 items, whereauto-fitandauto-fillagree, which is why only three sections were visibly wrong.Control group: a second design fixture authors
minmax(min(100%,14rem),1fr), which never matched the conversion regex. Its 6auto-fittrack lists are byte-identical before and after this change.Problem 2: inline
display:flexis dropped while the block stays marked css-owned-flowcssOwnedGroupAttributes()drops the nativelayoutattribute when an author flex or grid container is demoted to a css-ownedcore/group. Grids survive becauseCSS_OWNED_GRID_CARRIER_PROPERTIESrides their inline declarations to the generated stylesheet. There was no flex equivalent, so a block markedblocks-engine-css-owned-flowlost the declaration that made it a flex container, and its children stacked.A footer in one design fixture, before and after, same carrier class:
No
displayrule existed anywhere in the generated stylesheet before. Three such containers on that page alone.Problem 3:
core/buttondrops authoredfont-familyButtonStyleResolver::BUTTON_TYPOGRAPHYprojectedfontSize,fontWeight,letterSpacing,lineHeightandtextTransformonto buttons and droppedfont-family.core/buttonregisters afontFamilyattribute (packages/blocks-engine/src/core-block-attrs.json, generated from@wordpress/block-library9.42.0), which core injects only when the typography fontFamily support is enabled, so this was a dropped declaration rather than an unsupported one.The authored
.btnclass is consumed into block attributes, so its surviving rule is rewritten behind:where()at zero specificity and matches nothing that can win.theme.json'sstyles.elements.button.typography.fontFamilythen substituted the theme typeface, widening hero CTAs enough to wrap them onto two lines.Buttons on one design fixture, before and after:
This has to be fixed per authored declaration rather than by a blanket downstream change: across seven design fixtures the
.btnfamily matches the body family in three, and differs in four. One fixture whose buttons are correct today only because the theme preset happens to match now carries its authored stack explicitly ("Archivo", ..., its own heading stack, resolved), so it stays correct rather than being correct by coincidence.Fix
autoRepeatMinimumColumnWidth()now matchesauto-fillonly.auto-fitreturns''and falls through to the same css-owned-grid carrier that already holds every other track list WordPress cannot express, so the author rule stays the single owner of the exact authored track list.auto-fillkeeps converting natively.CSS_OWNED_FLEX_CARRIER_PROPERTIES,isCssOwnedFlexElement()andcssOwnedFlexAttributes()mirror the grid carrier for flex containers. An inline display that overrides class-owned layout is skipped, because Preserve inline display overrides through the geometry carrier #883 already carries those properties complete at the non-important specificity tier; forcing them would move them to the important tier and change that output.fontFamilyjoins the button typography subset. A raw authored family is a custom value rather than a preset slug, so it rides instyle.typography.fontFamily.BlockFactory's typography map did not map that attribute either, so the block attribute alone never reached the rendered link; the serializer now emitsfont-familyinline the way core's style engine does. This part is deliberately broad rather than button-scoped: the map is the generic serializer, and the drop affected any block that mapped the declaration.Inline style outranks theme.json element styles, so no
!importantbridge was added for the font-family path.Verification
Local, on this branch rebased onto
c0d66eafe:composer test:unitexit 0, 39 scripts, 0 failures.composer testexit 0, 51 scripts, 0 failures, 275 parity fixtures.auto-fit-grid-carrier.php11 assertions,css-owned-flex-carrier.php12,button-font-family-carry.php9.block-style-support-conversion.php130 assertions after resolving the three-way append collision as a union. Trunk's own copy of that file reports 129 assertions, so the union is trunk's 129 plus the one assertion added here, with neither side dropped.Seven parity fixtures changed, each because it encoded one of the defects rather than to make a test pass. Three asserted the
auto-fitto native conversion, one of which was renamed because its name asserted the buggy contract. Four asserted flex output in which the authoreddisplay:flexhad already been dropped, including one whose own description claimed a horizontal layout it was not getting. No parity fixture changed for the font-family fix.Rendered-class inventory across the 89-site corpus, comparing this branch against
c0d66eafe: zerohas-*,is-layout-*,wp-block-*orwp-element-*class tokens are ADDED anywhere. Every class-token change is a removal ofis-layout-grid/wp-block-group-is-layout-gridon containers that should never have had core grid layout. This is the specific failure shape from #885, wherehas-border-colortriggered core's all-sidesborder-style:solidand painted phantom borders; nothing here emits a class that makes WordPress paint something the author never wrote.Solved-gate fixtures, transformed output compared against
c0d66eafe:89-static-site-importer-architecture: byte-identical.15-saas: differs only by 8 renderedfont-familydeclarations on buttons, value'IBM Plex Mono', 'Courier New', monospace. That fixture's source authors.btn { font-family: var(--font-mono) }with--font-mono: 'IBM Plex Mono', 'Courier New', monospace, so the delivered typeface now equals the source's. Noauto-fit, flex-carrier oris-layout-gridchange on either fixture.The Solved Site Promotion Gate cannot be run locally, since it lives in another repository and needs a WordPress rendering rig. It is the gate that matters most here, because all three defects change rendered geometry and static parity compares output structure rather than pixels. It is left to CI on this PR to arbitrate at the 0.00% threshold.
Static style parity, this branch against
c0d66eafeRun directly rather than through composer, against a baseline worktree whose
vendor/was copied rather than symlinked, since a symlinked vendor makes Composer's$baseDirresolve to the other tree and silently invalidates the comparison. Both trees were checked by reflection before the runs to confirm they resolved to their own sources.Not byte-identical. Across 87 fixtures: mean score 0.664049 to 0.667159 (+0.003109), total findings 12385 to 12313 (-72), no status transitions. 23 fixtures moved: 16 improved, largest
69-markdown-editor0.2704 to 0.5039 with findings 173 to 116, and 7 regressed between -0.0028 and -0.0012.All 7 regressions are
font-familyfindings, and all are comparator artifacts rather than fidelity losses:var(--X)while the candidate reads the literal value of--X. Each variable was resolved by hand and is byte-equal, for example--sans: 'Space Grotesk', system-ui, sans-serifagainst a candidate value of'Space Grotesk', system-ui, sans-serif. The comparator does a string comparison and cannot resolvevar().<span>inside a button. Each of those fixtures'.btnrule does author the family the candidate now carries. The comparator resolves the inner span's inherited family to the document default instead of inheriting from.btn, so the candidate is the more faithful side.The pixel-level check that this signal cannot provide is the Solved Site Promotion Gate, which is why that gate is the one that decides here.