Repository navigation
Global Styles: Resolve per-level heading element styles in block inspector controls #80495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
83606c8
ad93ca6
8451e7e
fb65203
f1a60ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ | |
|
|
||
| ### New Features | ||
|
|
||
| - Added a private `resolveStyle` API that resolves the merged Global Styles cascade (root, block type, and applied block style variation) for a block, returning the inherited value and a per-leaf source map. | ||
| - Added a private `resolveStyle` API that resolves the merged Global Styles cascade (root, root-level element layers, block type, and applied block style variation) for a block, returning the inherited value and a per-leaf source map. Element layers are supplied by the caller as an ordered `elements` list on the resolve context (low to high precedence, e.g. `[ 'heading', 'h2' ]`), keeping the engine agnostic of block-to-element mappings. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs a #80495 link I think
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added the PR to a separate changelog entry. I left the existing entries alone just in case it affects branch conflicts when we go to backport this (but I might be overthinking this one 😄) |
||
| - Added a private `getVariationStyle` API that returns a block style variation's styles, resolving `{ ref }` references against the Global Styles tree by default (pass `{ resolveRefs: false }` to keep them in place). | ||
|
|
||
| ## 1.18.0 (2026-07-14) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -29,6 +29,11 @@ interface ResolveStyleContext { | |
| // Slug of the active block style variation, if any. Its styles are resolved | ||
| // against `globalStyles` internally and folded in as the highest layer. | ||
| variationName?: string | null; | ||
| // Root-level `styles.elements.*` keys whose styles paint this block on the | ||
| // canvas, ordered low to high precedence (e.g. `[ 'heading', 'h2' ]` for a | ||
| // level-2 Heading). Folded in above the root defaults and below the block's | ||
| // own styles. The caller owns this mapping; the engine stays block-agnostic. | ||
| elements?: string[] | null; | ||
| // Responsive breakpoint (e.g. `@mobile`), or null for the default viewport. | ||
| viewport?: string | null; | ||
| // CSS pseudo-state (e.g. `:hover`), or null for the default state. Matches | ||
|
|
@@ -83,15 +88,6 @@ const SOURCE_DESCRIPTORS: Record< string, SourceDescriptor > = { | |
| blockVariation: { layer: 'blockVariation' }, | ||
| }; | ||
|
|
||
| // Blocks whose canvas rendering is driven by a Global Styles *element* | ||
| // selector (e.g. `.wp-element-button`, `h1`–`h6`) rather than the block's own | ||
| // class. For these, the matching root-level `styles.elements[ element ]` layer | ||
| // is folded in as its own inheritance layer. | ||
| const BLOCK_TO_ROOT_ELEMENT: Record< string, string > = { | ||
| 'core/button': 'button', | ||
| 'core/heading': 'heading', | ||
| }; | ||
|
|
||
| function createSourceDescriptor( type: string ): SourceDescriptor | null { | ||
| const descriptor = SOURCE_DESCRIPTORS[ type ]; | ||
| if ( ! descriptor ) { | ||
|
|
@@ -401,6 +397,7 @@ function resolveThemeFileBackgroundImage( | |
| * @param context Which slice to resolve. | ||
| * @param context.blockName Block name (e.g. `core/heading`). | ||
| * @param context.variationName Slug of the active block style variation, or null. | ||
| * @param context.elements Root-level `styles.elements.*` keys that paint this block, ordered low to high precedence. | ||
| * @param context.viewport Responsive breakpoint, or null for the default viewport. | ||
| * @param context.pseudoState CSS pseudo-state, or null for the default state. | ||
| * @return Merged panel-scoped payload and source map. | ||
|
|
@@ -410,6 +407,7 @@ function computeResolvedStyle( | |
| { | ||
| blockName, | ||
| variationName = null, | ||
| elements = null, | ||
| viewport = null, | ||
| pseudoState = null, | ||
| }: ResolveStyleContext = {} | ||
|
|
@@ -429,13 +427,14 @@ function computeResolvedStyle( | |
|
|
||
| const root = styles; | ||
| const block = styles.blocks?.[ blockName ] ?? null; | ||
| // For element-based blocks (e.g. `core/button`), the root-level element | ||
| // styles paint the block on the canvas, so fold them in as a layer just | ||
| // above the root defaults but below the block's own styles. | ||
| const rootElement = BLOCK_TO_ROOT_ELEMENT[ blockName ] ?? null; | ||
| const element = rootElement | ||
| ? styles.elements?.[ rootElement ] ?? null | ||
| : null; | ||
| // For element-based blocks (e.g. `core/button`, or a Heading at a given | ||
| // level), the root-level element styles paint the block on the canvas, so | ||
| // fold them in as layers just above the root defaults but below the | ||
| // block's own styles. `elements` is ordered low to high precedence, so a | ||
| // level-specific `h2` correctly wins over the generic `heading`. | ||
| const elementLayers = ( elements ?? [] ) | ||
| .map( ( elementName ) => styles.elements?.[ elementName ] ?? null ) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a limitation of the current approach for sure. Not sure how well it'll work to walk up the block hierarchy to see what's there 🤔 Will poke at the idea tomorrow and make a call on whether we should go down the rabbithole now, or defer it to a future voyage!
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
It is looking a bit hairy performing block lookups up the block hierarchy to make sure we're inheriting things correctly. My vote would be to leave blocks-within-blocks behaviour to a follow up and mark it as a task in the linked issue. What do you reckon?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agree. A follow up sounds best. Thanks for digging in. |
||
| .filter( ( layer ): layer is StyleTree => !! layer ); | ||
| // Resolve the active block style variation's styles (with `{ ref }` | ||
| // values resolved) against the Global Styles tree. | ||
| const variation = variationName | ||
|
|
@@ -452,12 +451,12 @@ function computeResolvedStyle( | |
| pickLayerRootContribution( root ), | ||
| createSourceDescriptor( 'root' ) | ||
| ), | ||
| element | ||
| ? createContribution( | ||
| pickLayerRootContribution( element ), | ||
| createSourceDescriptor( 'element' ) | ||
| ) | ||
| : null, | ||
| ...elementLayers.map( ( layer ) => | ||
| createContribution( | ||
| pickLayerRootContribution( layer ), | ||
| createSourceDescriptor( 'element' ) | ||
| ) | ||
| ), | ||
| block | ||
| ? createContribution( | ||
| pickLayerRootContribution( block ), | ||
|
|
@@ -485,14 +484,14 @@ function computeResolvedStyle( | |
| ), | ||
| createSourceDescriptor( 'root' ) | ||
| ), | ||
| element | ||
| ? createContribution( | ||
| pickLayerRootContribution( | ||
| getStateSlice( element, selectedState ) | ||
| ), | ||
| createSourceDescriptor( 'element' ) | ||
| ) | ||
| : null, | ||
| ...elementLayers.map( ( layer ) => | ||
| createContribution( | ||
| pickLayerRootContribution( | ||
| getStateSlice( layer, selectedState ) | ||
| ), | ||
| createSourceDescriptor( 'element' ) | ||
| ) | ||
| ), | ||
| block | ||
| ? createContribution( | ||
| pickLayerRootContribution( | ||
|
|
@@ -588,15 +587,17 @@ export function resolveStyle( | |
| inner = new Map(); | ||
| byLinks.set( linksKey, inner ); | ||
| } | ||
| const stateKey = `${ context.viewport ?? '' }:${ | ||
| context.pseudoState ?? '' | ||
| }`; | ||
| // Element layers vary per block instance (e.g. a Heading's level), so they | ||
| // must take part in the cache key alongside the state slice. | ||
| const sliceKey = `${ context.elements?.join( ',' ) ?? '' }:${ | ||
| context.viewport ?? '' | ||
| }:${ context.pseudoState ?? '' }`; | ||
| const key = | ||
| ( context.blockName || '' ) + | ||
| '\u0001' + | ||
| ( context.variationName || '' ) + | ||
| '\u0001' + | ||
| stateKey; | ||
| sliceKey; | ||
| if ( inner.has( key ) ) { | ||
| return inner.get( key ) as ResolvedStyle; | ||
| } | ||
|
|
||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think
core/accordion-headingis an h3. Should we add it?https://github.com/WordPress/gutenberg/blob/fix/global-styles-element-layers/packages/block-library/src/accordion/block.json#L77
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the accordion one might need a little more work, but I'll double-check tomorrow. Thanks for taking this for a spin!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I got it working for the accordion heading: it needed different handling as it derives its heading level via context rather than it being set on the individual accordion heading (so that all accordion headings in the set of accordion items receive the same heading level). It's a bit hard-coded and possibly smelly, but I tried to keep it neat where I could. Let me know if you think it's too much, and we can roll back and look at it in a follow-up if need be.
I'm a little on the fence about all this hard-coding, but at least it's fairly internal for now, and we could always look at improving it in the future... in any case, here's how it's looking now, inheriting the heading element style:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! I think it's okay. Is there an alternative in code?
My 2c is that this small fix makes headings consistent, and therefore better for the user. That to me is more important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here. I think I was just musing out loud about the idea that at some point we might codify this in block.json in some way and make it a bit more API-ey than hard-coded.
But agreed — for now I think focusing on improving the UX while keeping the code fairly self-contained is likely the right path forward.
I haven't looked at an alternative in code, mostly I wanted to confidence check that we're okay with the hard-coding. It seems we both are for now 😄