From 83606c82f1d6cbdae5aa9d182b7adc521e7bbb84 Mon Sep 17 00:00:00 2001 From: Andrew Serong <14988353+andrewserong@users.noreply.github.com> Date: Tue, 21 Jul 2026 12:02:02 +1000 Subject: [PATCH 1/5] Global Styles: resolve per-level heading element styles in inspector controls Completes the heading-family portion of the element inheritance layer tracked in #80438 (follow-up to #77894). The block inspector now reflects styles set on a specific heading level (`styles.elements.h1`-`h6`) in addition to the shared `heading` element. Previously the resolver folded in only `styles.elements.heading` for `core/heading`, so anything written by the per-level tabs in Styles > Typography > Headings was invisible to inspector controls. Because `fontSize` is deliberately disabled at the "All headings" level, every Global Styles heading font size lands in `elements.hN` and never reached a control at all. - Engine: replace the hardcoded `BLOCK_TO_ROOT_ELEMENT` map with an optional, caller-supplied `elements` list on the resolve context, ordered low to high precedence so `h2` wins over `heading`. This keeps the engine agnostic of block-to-element mappings and removes core block names from the package. The element list joins the memo cache key so two heading levels no longer share an entry. - block-editor: map block name + `level` to element keys for the heading family (`core/heading`, `site-title`, `post-title`, `query-title`, `comments-title`, `term-name`, `site-tagline`). A level of 0 (a block rendered as `
`) folds
no heading layer. `clientId` is read from the block edit context, so
`useResolvedStyle`'s signature is unchanged and no new store subscription is
added.
Co-Authored-By: Claude Opus 4.8 `, so the caller maps it
+ // to no element keys and neither `heading` nor `hN` should fold.
+ const gs = {
+ styles: {
+ elements: {
+ heading: { typography: { fontWeight: '700' } },
+ h2: { typography: { fontWeight: '900' } },
+ },
+ },
+ };
+ const { value } = resolveStyle( gs, {
+ blockName: 'core/post-title',
+ elements: [],
+ } );
+ expect( value.typography?.fontWeight ).toBeUndefined();
+ } );
+
+ test( 'block-type styles still override the folded element layers', () => {
+ const gs = {
+ styles: {
+ elements: {
+ heading: { typography: { fontWeight: '700' } },
+ h2: { typography: { fontWeight: '900' } },
+ },
+ blocks: {
+ 'core/heading': {
+ typography: { fontWeight: '400' },
+ },
+ },
+ },
+ };
+ const { value, sources } = resolveStyle( gs, {
+ blockName: 'core/heading',
+ elements: [ 'heading', 'h2' ],
+ } );
+ expect( value.typography.fontWeight ).toBe( '400' );
+ expect( sources[ 'typography.fontWeight' ]?.layer ).toBe( 'block' );
+ } );
} );
describe( 'explicit-empty normalization', () => {
@@ -688,6 +760,38 @@ describe( 'resolveStyle – memoization', () => {
} );
expect( a ).toEqual( {} );
} );
+
+ test( 'different `elements` arrays key distinct cache entries', () => {
+ // Two heading levels share a block name and payload, so the element
+ // list must take part in the cache key or they would collide.
+ const gs = {
+ styles: {
+ elements: {
+ heading: { typography: { fontWeight: '700' } },
+ h1: { typography: { fontWeight: '900' } },
+ h2: { typography: { fontWeight: '400' } },
+ },
+ },
+ };
+ const h1 = resolveStyle( gs, {
+ blockName: 'core/heading',
+ elements: [ 'heading', 'h1' ],
+ } );
+ const h2 = resolveStyle( gs, {
+ blockName: 'core/heading',
+ elements: [ 'heading', 'h2' ],
+ } );
+ expect( h1 ).not.toBe( h2 );
+ expect( h1.value.typography.fontWeight ).toBe( '900' );
+ expect( h2.value.typography.fontWeight ).toBe( '400' );
+
+ // Identical element lists still return the memoized identity.
+ const h2Again = resolveStyle( gs, {
+ blockName: 'core/heading',
+ elements: [ 'heading', 'h2' ],
+ } );
+ expect( h2Again ).toBe( h2 );
+ } );
} );
describe( 'resolveStyle – non-cascading root drop', () => {
From ad93ca6c4b02dd8ef89f8a3c49428787eead2fce Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 21 Jul 2026 12:16:26 +1000
Subject: [PATCH 2/5] Update comment
---
.../global-styles/inherited-value-context.js | 31 +++++++++++++------
1 file changed, 22 insertions(+), 9 deletions(-)
diff --git a/packages/block-editor/src/components/global-styles/inherited-value-context.js b/packages/block-editor/src/components/global-styles/inherited-value-context.js
index 8d8c7432dd22b8..71678ee65079e4 100644
--- a/packages/block-editor/src/components/global-styles/inherited-value-context.js
+++ b/packages/block-editor/src/components/global-styles/inherited-value-context.js
@@ -45,20 +45,33 @@ function useRawGlobalStyles() {
}
/**
- * Blocks whose canvas rendering is driven by root-level Global Styles
- * *element* styles rather than (or in addition to) the block's own class.
+ * Maps a block to the root-level Global Styles *element* layers that paint it
+ * on the canvas, in addition to (or instead of) the block's own class — e.g. a
+ * Button renders `.wp-element-button`, a level-2 Heading renders ``.
*
- * Each entry returns the element keys that paint the block, ordered low to
- * high precedence, so a level-specific `h2` correctly wins over the generic
- * `heading`.
+ * Element keys are returned ordered low to high precedence, so a level-specific
+ * `h2` correctly wins over the generic `heading`. A level of `0` (e.g. a Site
+ * or Post Title rendered as a paragraph) renders no heading tag at all, so
+ * neither `heading` nor any `h1`-`h6` styles reach the block and no element
+ * layer applies.
*
- * A level of `0` (e.g. Site Title or Post Title rendered as a paragraph)
- * renders no heading tag at all, so neither the generic `heading` nor any
- * `h1`-`h6` element styles reach the block and no element layer applies.
+ * This list is hand-maintained by necessity: the block-to-element relationship
+ * is not inferable from block metadata. `supports.color.link` and friends mark
+ * blocks that *contain* a stylable element, not blocks that *are* one (Button
+ * declares no `color.button`, Heading no `color.heading`), and the system only
+ * stores the inverse element-to-selector map (`__EXPERIMENTAL_ELEMENTS`); the
+ * correspondence otherwise lives only in each block's rendered markup.
+ * Standardizing it as a declarative block property is tracked in
+ * https://github.com/WordPress/gutenberg/issues/80438.
+ *
+ * The link-bearing heading blocks (Site Title, Post Title, Term Name) are
+ * deliberately not given a `link` layer here: their inner-link color control
+ * reads the `inheritedValue.elements.link` passthrough, so folding `link` into
+ * the block's own layers would bleed link color into the heading's text.
*
* @param {string} blockName Block name.
* @param {?number} headingLevel Block's `level` attribute, when it has one.
- * @return {string[]} Ordered element keys.
+ * @return {string[]} Ordered element keys, low to high precedence.
*/
function getElementLayers( blockName, headingLevel ) {
switch ( blockName ) {
From 8451e7ecdcd3ca62887c057875087444dbce30ce Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Tue, 21 Jul 2026 14:26:11 +1000
Subject: [PATCH 3/5] Try to fix tests
---
.../global-styles/test/inherited-value-context.js | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/packages/block-editor/src/components/global-styles/test/inherited-value-context.js b/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
index 5ab733823739c7..38215e19efca37 100644
--- a/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
+++ b/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
@@ -46,9 +46,10 @@ jest.mock( '../../../store', () => ( {
} ) );
// `inherited-value-context.js` imports `store as blocksStore` from
-// `@wordpress/blocks` for `useOwnVariation`. The blocks store's transitive
-// import chain fails under this file's `@wordpress/data` mock (missing
-// `createSelector`), so stub the blocks module with just the shape needed.
+// `@wordpress/blocks` for `useVariationAndElements`. The blocks store's
+// transitive import chain fails under this file's `@wordpress/data` mock
+// (missing `createSelector`), so stub the blocks module with just the shape
+// needed.
jest.mock( '@wordpress/blocks', () => ( {
store: { name: 'core/blocks' },
getBlockType: ( blockName ) =>
@@ -96,6 +97,10 @@ describe( 'useResolvedStyle hook', () => {
[ globalStylesDataKey ]: rawGlobalStyles,
} ),
getBlockStyles: () => blockStyles,
+ // The hook reads the block's `level` attribute to pick the
+ // heading element layers; no block edit context is provided
+ // here, so `clientId` is undefined and attributes are empty.
+ getBlockAttributes: () => ( {} ),
} ) )
);
}
@@ -226,6 +231,7 @@ describe( 'useResolvedStyle – production bare-tree shape', () => {
[ globalStylesDataKey ]: rawGlobalStyles,
} ),
getBlockStyles: () => [],
+ getBlockAttributes: () => ( {} ),
} ) )
);
return render( ui );
From fb652035ae716c1c2c1dedfb0a7dbd343fe27a4a Mon Sep 17 00:00:00 2001
From: Andrew Serong <14988353+andrewserong@users.noreply.github.com>
Date: Wed, 22 Jul 2026 11:46:26 +1000
Subject: [PATCH 4/5] Handle accordion heading
---
packages/block-editor/CHANGELOG.md | 2 +-
.../global-styles/inherited-value-context.js | 93 +++++++++++++++----
.../test/inherited-value-context.js | 82 +++++++++++++++-
3 files changed, 152 insertions(+), 25 deletions(-)
diff --git a/packages/block-editor/CHANGELOG.md b/packages/block-editor/CHANGELOG.md
index 34a253f68706af..5d2327e993424a 100644
--- a/packages/block-editor/CHANGELOG.md
+++ b/packages/block-editor/CHANGELOG.md
@@ -5,7 +5,7 @@
### Enhancements
- Inspector controls in the standard block-supports panels (Typography, Dimensions, Border, Color, Background, Filters) now reflect the value a block inherits from Global Styles when no local override is set. Inherited controls show that value at rest (as a placeholder, preselected option, or resolved value) and mark the label with a dotted underline; setting a local override reveals a reset affordance that clears the override back to the inherited value ([#77894](https://github.com/WordPress/gutenberg/pull/77894)).
-- Inherited Global Styles now resolve per-level heading element styles for the heading-family blocks (`core/heading`, `core/site-title`, `core/post-title`, `core/query-title`, `core/comments-title`, `core/term-name`, `core/site-tagline`), so inspector controls reflect values set on a specific heading level (`styles.elements.h1`–`h6`) in addition to the shared `heading` element. A block rendered at level 0 (a paragraph) folds no heading element styles.
+- Inherited Global Styles now resolve per-level heading element styles for the heading-family blocks (`core/heading`, `core/site-title`, `core/post-title`, `core/query-title`, `core/comments-title`, `core/term-name`, `core/site-tagline`, `core/accordion-heading`), so inspector controls reflect values set on a specific heading level (`styles.elements.h1`–`h6`) in addition to the shared `heading` element. A block rendered at level 0 (a paragraph) folds no heading element styles; `core/accordion-heading` has no level-0 state and takes its level from the parent Accordion's block context ([#80495](https://github.com/WordPress/gutenberg/pull/80495)).
### Bug Fixes
diff --git a/packages/block-editor/src/components/global-styles/inherited-value-context.js b/packages/block-editor/src/components/global-styles/inherited-value-context.js
index 71678ee65079e4..d35ff102715539 100644
--- a/packages/block-editor/src/components/global-styles/inherited-value-context.js
+++ b/packages/block-editor/src/components/global-styles/inherited-value-context.js
@@ -1,7 +1,7 @@
/**
* WordPress dependencies
*/
-import { useMemo } from '@wordpress/element';
+import { useContext, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { store as blocksStore } from '@wordpress/blocks';
import { privateApis as globalStylesEnginePrivateApis } from '@wordpress/global-styles-engine';
@@ -16,6 +16,7 @@ import {
} from '../../store/private-keys';
import { getVariationNameFromClass } from '../../hooks/block-style-variation';
import { useBlockEditContext } from '../block-edit/context';
+import BlockContext from '../block-context';
import { unlock } from '../../lock-unlock';
const { resolveStyle } = unlock( globalStylesEnginePrivateApis );
@@ -44,39 +45,86 @@ function useRawGlobalStyles() {
);
}
+/**
+ * Blocks whose heading level is handed down by a parent through block context
+ * rather than held in their own `level` attribute, mapped to that context key
+ * and the level they fall back to.
+ *
+ * Accordion Heading is the only one: the parent Accordion owns the level, and
+ * the child's `level` attribute has no default and is absent on pattern-,
+ * template- and paste-inserted blocks.
+ */
+const CONTEXT_HEADING_LEVEL_BLOCKS = {
+ 'core/accordion-heading': {
+ contextKey: 'core/accordion-heading-level',
+ fallbackLevel: 3,
+ },
+};
+
+/**
+ * Resolves the heading level that selects a block's `h1`-`h6` element layer.
+ *
+ * @param {string} blockName Block name.
+ * @param {?number} levelAttribute The block's own `level` attribute, if it has one.
+ * @param {?number} contextLevel Level supplied by a parent through block context.
+ * @return {?number} Resolved heading level, or `undefined` when the block has none.
+ */
+function getHeadingLevel( blockName, levelAttribute, contextLevel ) {
+ const contextOwned = CONTEXT_HEADING_LEVEL_BLOCKS[ blockName ];
+ if ( ! contextOwned ) {
+ return levelAttribute;
+ }
+ // Mirrors the block's own chain (Accordion Heading saves `h${ level || 3 }`),
+ // so `0` coerces to the fallback instead of reading as a level-0 state.
+ return levelAttribute || contextLevel || contextOwned.fallbackLevel;
+}
+
+/**
+ * Reads the heading level a parent supplies through block context.
+ *
+ * Only the blocks in `CONTEXT_HEADING_LEVEL_BLOCKS` read a key: the value
+ * reaches every descendant, so an ordinary Heading inside an Accordion Panel
+ * must not pick it up. Returns a primitive so callers can use it as a
+ * `useSelect` dependency.
+ *
+ * @param {?string} blockName Block name.
+ * @return {?number} Level from block context, or `undefined`.
+ */
+function useContextHeadingLevel( blockName ) {
+ const blockContext = useContext( BlockContext );
+ const contextKey = CONTEXT_HEADING_LEVEL_BLOCKS[ blockName ]?.contextKey;
+ return contextKey ? blockContext[ contextKey ] : undefined;
+}
+
/**
* Maps a block to the root-level Global Styles *element* layers that paint it
* on the canvas, in addition to (or instead of) the block's own class — e.g. a
* Button renders `.wp-element-button`, a level-2 Heading renders `
`.
*
- * Element keys are returned ordered low to high precedence, so a level-specific
- * `h2` correctly wins over the generic `heading`. A level of `0` (e.g. a Site
- * or Post Title rendered as a paragraph) renders no heading tag at all, so
- * neither `heading` nor any `h1`-`h6` styles reach the block and no element
- * layer applies.
+ * Keys are ordered low to high precedence, so a level-specific `h2` wins over
+ * the generic `heading`. A level of `0` (e.g. a Site or Post Title rendered as
+ * a paragraph) renders no heading tag, so no element layer applies.
*
- * This list is hand-maintained by necessity: the block-to-element relationship
- * is not inferable from block metadata. `supports.color.link` and friends mark
- * blocks that *contain* a stylable element, not blocks that *are* one (Button
- * declares no `color.button`, Heading no `color.heading`), and the system only
- * stores the inverse element-to-selector map (`__EXPERIMENTAL_ELEMENTS`); the
- * correspondence otherwise lives only in each block's rendered markup.
- * Standardizing it as a declarative block property is tracked in
+ * Hand-maintained by necessity: the block-to-element relationship is not
+ * inferable from block metadata. `supports.color.link` and friends mark blocks
+ * that *contain* an element, not blocks that *are* one, and only the inverse
+ * element-to-selector map exists (`__EXPERIMENTAL_ELEMENTS`). Standardizing it
+ * as a block property is tracked in
* https://github.com/WordPress/gutenberg/issues/80438.
*
- * The link-bearing heading blocks (Site Title, Post Title, Term Name) are
- * deliberately not given a `link` layer here: their inner-link color control
- * reads the `inheritedValue.elements.link` passthrough, so folding `link` into
- * the block's own layers would bleed link color into the heading's text.
+ * The link-bearing heading blocks get no `link` layer: their inner-link control
+ * reads the `inheritedValue.elements.link` passthrough, so folding `link` in
+ * would bleed link color into the heading's own text.
*
* @param {string} blockName Block name.
- * @param {?number} headingLevel Block's `level` attribute, when it has one.
+ * @param {?number} headingLevel Resolved heading level, from `getHeadingLevel`.
* @return {string[]} Ordered element keys, low to high precedence.
*/
function getElementLayers( blockName, headingLevel ) {
switch ( blockName ) {
case 'core/button':
return [ 'button' ];
+ case 'core/accordion-heading':
case 'core/heading':
case 'core/post-title':
case 'core/site-title':
@@ -117,6 +165,7 @@ function getElementLayers( blockName, headingLevel ) {
*/
function useVariationAndElements( blockName, className ) {
const { clientId } = useBlockEditContext();
+ const contextHeadingLevel = useContextHeadingLevel( blockName );
return useSelect(
( select ) => {
if ( ! blockName ) {
@@ -132,10 +181,14 @@ function useVariationAndElements( blockName, className ) {
variationName: className
? getVariationNameFromClass( className, registeredStyles )
: null,
- headingLevel: level,
+ headingLevel: getHeadingLevel(
+ blockName,
+ level,
+ contextHeadingLevel
+ ),
};
},
- [ blockName, className, clientId ]
+ [ blockName, className, clientId, contextHeadingLevel ]
);
}
diff --git a/packages/block-editor/src/components/global-styles/test/inherited-value-context.js b/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
index 38215e19efca37..c9ff7c4507dad8 100644
--- a/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
+++ b/packages/block-editor/src/components/global-styles/test/inherited-value-context.js
@@ -12,6 +12,7 @@ import { useSelect } from '@wordpress/data';
* Internal dependencies
*/
import { useResolvedStyle } from '../inherited-value-context';
+import { BlockContextProvider } from '../../block-context';
import { globalStylesDataKey } from '../../../store/private-keys';
@@ -84,7 +85,10 @@ describe( 'useResolvedStyle hook', () => {
// The hook issues two `useSelect` reads: the merged Global Styles
// payload (block-editor store) and the block's registered styles
// (blocks store). Feed both from one stub.
- function mockStores( rawGlobalStyles, blockStyles = [] ) {
+ function mockStores(
+ rawGlobalStyles,
+ { blockStyles = [], attributes = {} } = {}
+ ) {
useSelect.mockImplementation( ( mapSelect ) =>
mapSelect( () => ( {
// `globalStylesDataKey` holds the BARE merged styles tree
@@ -98,13 +102,83 @@ describe( 'useResolvedStyle hook', () => {
} ),
getBlockStyles: () => blockStyles,
// The hook reads the block's `level` attribute to pick the
- // heading element layers; no block edit context is provided
- // here, so `clientId` is undefined and attributes are empty.
- getBlockAttributes: () => ( {} ),
+ // heading element layers. No block edit context is provided
+ // here, so `clientId` is undefined; the stub returns whatever
+ // attributes the test supplies regardless.
+ getBlockAttributes: () => attributes,
} ) )
);
}
+ // Resolves a block's styles as rendered inside an Accordion, which supplies
+ // the heading level to every descendant through block context.
+ function resolveInAccordion( blockName, level = 3 ) {
+ render(
+