Block Editor: Reflect inherited Global Styles values in block inspector controls#77894
Block Editor: Reflect inherited Global Styles values in block inspector controls#77894aaronrobertshaw wants to merge 39 commits into
Conversation
|
Thanks for the early feedback @ramonjd, appreciate it 🙇
This is on the cards to investigate but ensuring we can accurately trace where the inheritance comes from and then also pass that data along to the tooltip being injected into the DOM was another. So I left that to follow-up iterations that I'm in the process of exploring. |
There was a problem hiding this comment.
I was playing around with this and noticed how, the underlined/purple text now appears everywhere. It's the default state to have your style inherited from somewhere.
What's good about the bullet icon for overridden styles
is that the default shows nothing special, and it's the override that changes up the UI. This inverts that. Now the default behaviour looks special and the override looks less special. You could just have a tooltip and no visual style change, but then it becomes very hard to discover.
What I'm imagining is that if the hit box for the tooltip was much bigger, like the entire panel was a hitbox, then any hover over the panel would show the detailed info.
This works with a mouse which isn't ideal. The panels which have a click which expands another side panel are better for showing extra info.
I'm sure you have plenty of UI proposals to choose from :)
| helper returns `{ className, title }` to spread onto the wrapping | ||
| `ToolsPanelItem` (or another wrapping element). The class hook drives the | ||
| label-level visual treatment described above; the `title` attribute carries | ||
| the i18n string `__( 'Inherited from Global Styles' )` for native browser |
There was a problem hiding this comment.
The tooltip says things other than "Inherited from Global Styles" too, so I think just needs updated to "e.g. Inherited from ..."
There was a problem hiding this comment.
Thanks, I missed going back over all this when I iterated on the PoC adding the style breadcrumbs to the tooltip.
|
Thanks so much for the thoughtful review and feedbacjk @p-jackson 🙇
For the controls highlighted, you're right there are values inherited, in this case the theme's theme.json file acting as the base for Global Styles. So the purple and underlined labels are "correct" in that regard. Whether it is "too much", that is a question I might direct to @jameskoster and his expertise. For now it is inline with the original design issue and recent feedback.
Initially, I looked at something like this too but wrapping more than the label meant that the tooltip there swallowed all tooltips on subcomponents which are as or more important to end users. So I deliberately kept the scope for this narrow. |
|
Perhaps the underline could be more subtle ( |
9bfb3d7 to
9313738
Compare
|
Size Change: +11.4 kB (+0.15%) Total Size: 7.73 MB 📦 View Changed
|
|
Quick update: I'll be away for a little while but I'll address any feedback collected over the next two weeks as well as the failing tests when i'm back. It is worth keeping in mind this is a proof of concept at this stage and the code and approach hasn't yet been polished to a production ready state. That said, please don't be shy in leaving feedback, especially on the design and UX side of things. |
|
I think I'd lean toward omitting the purple colorisation (we can easily add it back later if the underline proves inadequate). Not a strong feeling though. |
|
Thanks for the continued design direction here. I'll address the current feedback in the next couple of days once I wrangle the new wave of conflicts after other recent global styles projects landed. |
9313738 to
68fda90
Compare
|
It's been a while coming but I think I have this successfully rebased onto trunk. The state-based / responsive styling work that landed on trunk reshaped basically every panel and hook this PR touches, so just about all of the old code was in conflict. As this PR stands now, the inherited global styles display has been relayed and updated on top of trunk and the state/responsive styles feature. A few notes:
Unit and e2es are green locally but treat this as an updated PoC. With as much conflict as the previous working version had, this needs proper manual testing to confirm the inherited value UX actually makes sense across every panel and every state. I'll continue to give this some testing and polish before flagging for review in the near future. |
68fda90 to
1f96f54
Compare
|
Flaky tests detected in 4e4ff76. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/29065169599
|
1f96f54 to
93fe5df
Compare
… inspector controls
…on the duotone control
…ilder Make the inherited-value builder the single source of truth for the inspector's display of inherited Global Styles — it now holds the full Root › Block › Variation cascade plus all resolution, so the panels stay pure UI and the canvas stays on trunk. - Builder: drop non-cascading root leaves (background, spacing, dimensions, border, shadow, filter, colour background/gradient) from value and sources together, and resolve theme-file `_links` image pointers inline. Typography, `color.text` and the `elements` passthrough still cascade. - Close the dimensions gap — root minHeight/minWidth/aspectRatio are non-cascading and are now dropped like the other groups. - Revert the background/style canvas `useBlockProps` to trunk: the merged value is display-only, and the CSS cascade already renders inherited styles. - Panels: remove the in-panel `useSelect`/`getResolvedValue` and the `dropRootSourced`/`isRootSourced` filtering; `inheritedValue` now arrives fully resolved and cascade-correct. - Remove the now-dead `root-source` module and canvas glue (`useInheritedStyleValue`, `getEffectiveBackgroundStyle`, `mergeInheritedBackgroundStyle`).
Replace the per-panel InheritedValueProvider / context / bridge chain with one arg-taking `useInheritedValue( blockName, className, selectedState )` hook. Each inspector wrapper now calls it directly and passes the resolved value to the shared panel as props, so the panels stay presentational and the store access stays in the wrapper where it already lived. The context indirection deduped nothing (one consumer per Provider), so removing it shrinks the wiring and drops InheritedValueProvider, InheritedValueContext and useOwnVariation from the private API surface — they are consumed only within block-editor. The only remaining private-API addition is the backwards-compatible showInheritanceLabelIndicators prop. Tests retargeted from Provider-mounting to the hook contract; the subscription-dedup test (only reachable by nesting probes under one Provider, which production never does) is removed.
Assert the inherited-styles feature through stable, user-facing signals instead of brittle CSS class hooks. - Query the reset affordance by its accessible role and name rather than the `.has-local-override-from-global-styles` class. - Drop the `.is-inherited-from-global-styles` node-access assertions and the helpers that wrapped them, along with the now-unused eslint-disable directives for testing-library node access. - Keep every behavioural assertion: value shown at rest, local override wins, display without commit, zero preserved, single-undo reset. - Add coverage of the background image reset button via its accessible name. The inherited-at-rest state has no accessible representation yet, so that coverage is deferred to the accessibility pass that follows the label adornment decision.
…olvedStyles
Behaviour-preserving readability refactor of the block-editor global styles
cascade builder:
- Rename buildInheritedValue -> resolveStyles, computeInheritedValue ->
computeResolvedStyles, and useInheritedValue -> useResolvedStyles, updating
the six inspector hook consumers and tests.
- De-abbreviate internal variable names (sVal, v, gs, acc, obj).
- Cull/condense comments: drop trivial JSDoc on self-evident helpers, keep and
condense the domain 'why' notes and the public-entry JSDoc.
The { value, sources } output shape and the inheritedValue/inheritedSources
prop wiring are unchanged.
…nt-backed blocks Element-backed blocks (core/button, core/heading) are painted on the canvas by Global Styles element rules (e.g. .wp-element-button), but the inherited- value builder only folded the block-scoped layers, so their inspector controls read as empty while the canvas was styled. Resolve the matching root-level styles.elements[ element ] layer as a block contribution at root < element < block-type < variation precedence, mirroring the same folding in the pseudo/viewport state pass. Block-type styles override root element styles per the agreed inspector model. Scoped to core/button and core/heading for now; other element-backed blocks can follow up.
The builder's public entry point was renamed to resolveStyles/useResolvedStyles, so align the filenames. Uses git mv to preserve history and keep rebases clean. - build-inherited-value.js -> resolve-styles.js - test/build-inherited-value.js -> test/resolve-styles.js Updates the import in inherited-value-context.js and a doc comment in the typography-panel test.
…trol is present
The visible Border label was gated on showInheritanceLabelIndicators, so it disappeared in Global Styles (which passes showInheritanceLabelIndicators={ false }). Widen the gate to also render when a Shadow control is present, matching trunk's hideLabelFromVision={ ! hasShadowControl } and the symmetric Shadow label guard.
…g is set
The panel passed placeholder={ undefined } to LineHeightControl when there was no local or inherited value, which clobbered the control's own BASE_DEFAULT_VALUE (1.5) placeholder (spread via ...otherProps). Conditionally spread the placeholder prop so it is only supplied when an inherited value is being surfaced, letting the built-in default apply otherwise.
The Content width and Wide width UnitControls passed the full inherited value (with unit) as the placeholder, duplicating the control's unit selector, and used value={ local ?? '' } so the inherited value only ever showed greyed-out. Align them with the Minimum height/width pattern: render the inherited value with unit as the control value at rest and use getNumericPlaceholder for a numeric-only placeholder. Values are still only written to local on user change.
… set ColorGradientTab passed the inherited preset's slug to ColorPalette unconditionally. Since ColorPalette selects strictly by selectedSlug when set, the inherited preset swatch stayed highlighted even after the user chose a local custom colour. Only pass inheritedSlug while displaying the inherited value (placeholder mode); otherwise pass undefined so selection falls back to matching the displayed colour value.
deepMergeDroppingEmpties recursed into every plain object, so a higher-precedence layer's background.backgroundImage was merged key-by-key onto a lower layer's, mixing fields (e.g. one layer's url with another's id) into an invalid value. Add an ATOMIC_OBJECT_KEYS exception so backgroundImage is replaced wholesale (shallow-cloned to keep the merged tree fresh), mirroring the backgroundImage exception in the Global Styles engine's mergeGlobalStyles. Add focused tests.
… merge
getValueFromObjectPath does not resolve nested refs, so a resolved envelope could itself be a { ref } object (an invalid, unresolved pointer) and slip through into the merged value. Add an isRefObject( resolved ) guard to the skip block, matching getResolvedRefValue in the Global Styles engine.
Each of the six block-support panels wraps the shared raw Global Styles payload in its own { styles } object, and resolveStyles memoized on that wrapper's identity, so the full cascade merge recomputed once per consumer on every block selection. Key the resolveStyles memo on the raw styles payload (a single stable reference already shared by every consumer) instead of the throwaway wrapper, so all consumers collapse to one merge per selection.
Align the dimensions and typography panel tests with the review fixes: the inherited layout content/wide widths now render as the control value with a numeric-only placeholder (matching the Minimum height/width pattern), and the line-height control restores its BASE_DEFAULT_VALUE (1.5) placeholder when no inherited value is surfaced. Update the assertions that still encoded the pre-fix placeholder-only behaviour.
…rols The block inspector now renders the global-styles Typography/Dimensions panels with resolved inherited values, which changed the Button block's inspector behaviour and broke three e2e tests: - 'can apply named colors': the link-color sync only fired when the inherited text and link colors matched. A fresh button inherits a text color but no link color, so the sync was skipped. Sync when the inherited link color is unset, only skipping when a distinct link color was explicitly set. - 'can apply custom colors': the color control now carries an inherited value, so the 'Custom color picker' swatch's accessible name gains a '. The currently selected color is…' suffix. Match by substring. - 'can resize width': the Font Size control now opens in custom mode and renders a 'Select unit' combobox above the Width control, so the panel-wide '.first()' matched the wrong control. Scope the lookup to the Width group.
Rename `resolveStyles` to the singular `resolveStyle` and move the pure cascade resolver from `block-editor` into `@wordpress/global-styles-engine`, exposed via the engine's private API. The `useResolvedStyle` hook stays in `block-editor` and owns all store access. Also relocate `getVariationStylesWithRefValues` into the engine so `resolveStyle` resolves the applied block style variation internally from its slug. This collapses the earlier interim signature (a separate pre-resolved `variationStyles` argument plus a slug used only for memoization) into a single `ownVariation` argument, keeping the engine self-contained. `block-editor` consumers (`block-style-variation`, `switch-section-style`) now unlock the helper from the engine. Relocated modules are authored in TypeScript to match engine conventions, and their unit tests move alongside them.
40dd5f6 to
6997026
Compare



Closes: #77595
What?
Inspector controls now reflect the Global Styles values a block inherits, so the panels line up closer to what you actually see on the block in the canvas.
Inherited global styles values only come from the merged Global Styles data:
There are no computed styles involved, so custom CSS, enqueued stylesheets, and ancestor variations still can't be reflected even though they paint the canvas.
Scoped to an MVP
To land the core of this in 7.1 without first having to extend heavily-used
@wordpress/components(likeBaseControl) to accept injected label nodes and settle the related design and portal questions that come with it. Two things are deliberately out of scope here and moved to follow-ups:BaseControlnorInputControlexposes one today. Rather than push that public-API change through now, this PR keeps only the dotted underline at rest and moves the portaled adornment + path tooltip to a follow-up, where the approach and design can be settled properly.Why?
Right now a control only reflects what's set on the block, never what it inherits.
A Paragraph block renders with the theme's colour, size, and padding on the canvas while the controls sit empty, reading as "unset" when they're anything but. You can't tell a local override apart from an inherited value.
It's a long-standing rough edge (#37752, #43082, #49278), tracked under #77595.
How?
buildInheritedValue, merges the value a block inherits in precedence order; theme.json/user defaults ‹ block type ‹ applied block style variation. It's pure: all value resolution ({ ref }envelopes and theme-file image links) happens here.useInheritedValue, resolve the cascade for the selected block, and pass the result to the shared inspector panels as a prop.block-editor.0is preserved, inherited values aren't saved until a user picks them, and reset stays a single undo.Architecture
The flow is a straight line from the store to the control:
useInheritedValue, they read the merged Global Styles data (and theme-file links) from the store in a singleuseSelect, then callbuild InheritedValueto resolve the Root ‹ Block ‹ Variation cascade for the selected block, dropping root-level values that don't actually cascade to it (e.g. a root background).So store access lives only in the hooks, cascade resolution lives only in the pure function, and the panels stay presentational. No data dependency and liftable out of
block-editor.APIs
@wordpress/components(e.g.BaseControl) to wrap control labels in tooltips. This PR doesn't need that, and the follow-up avoids it too by portaling the adornment instead. Neither branch touches@wordpress/components.useInheritedValueandbuildInheritedValueare internal toblock-editor, not re-exported from the package entry orprivateApis.showInheritanceLabelIndicators, which flags whether the inheritance labels and indicators should be shown. TheinheritedValueprop is pre-existing.Notes
Testing Instructions
Inherited values
Local overrides
Edge cases
0value is treated as a real value, not an empty one.Example theme.json
To help with defining global style values to check different controls the following theme.json might be of use. It is a modified version of the emptytheme theme.json, so applying this there and switching to emptytheme is a quick way to get up and running.
Example theme.json
{ "$schema": "../../schemas/json/theme.json", "version": 3, "settings": { "appearanceTools": true, "typography": { "writingMode": true, "fontFamilies": [ { "slug": "georgia", "name": "Georgia", "fontFamily": "Georgia, 'Times New Roman', serif" }, { "slug": "system", "name": "System", "fontFamily": "system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif" } ] }, "layout": { "contentSize": "840px", "wideSize": "1100px" } }, "styles": { "blocks": { "core/paragraph": { "color": { "text": "#1e1e1e", "background": "#f6f7f7", "gradient": "linear-gradient(135deg, #fdfbfb 0%, #ebedee 100%)" }, "typography": { "fontFamily": "var:preset|font-family|georgia", "fontSize": "1.125rem", "fontStyle": "italic", "fontWeight": "600", "lineHeight": "1.7", "letterSpacing": "0.01em", "textAlign": "justify", "textColumns": "2", "textDecoration": "underline", "textIndent": "1.5em", "textTransform": "capitalize" }, "spacing": { "margin": { "top": "0", "right": "0", "bottom": "1.5rem", "left": "0" }, "padding": { "top": "0.5rem", "right": "1rem", "bottom": "0.5rem", "left": "1rem" } }, "border": { "color": "#e0e0e0", "style": "solid", "width": "1px", "radius": "4px" }, "elements": { "link": { "color": { "text": "#0073aa" }, ":hover": { "color": { "text": "#005177" } } } } }, "core/group": { "background": { "backgroundImage": { "url": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScyMDAnIGhlaWdodD0nMjAwJz48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9J2cnIHgxPScwJyB5MT0nMCcgeDI9JzEnIHkyPScxJz48c3RvcCBvZmZzZXQ9JzAnIHN0b3AtY29sb3I9JyNjOWU3ZmYnLz48c3RvcCBvZmZzZXQ9JzAuNScgc3RvcC1jb2xvcj0nI2VhZTRmZicvPjxzdG9wIG9mZnNldD0nMScgc3RvcC1jb2xvcj0nI2ZmZDZlOCcvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHdpZHRoPScyMDAnIGhlaWdodD0nMjAwJyBmaWxsPSd1cmwoI2cpJy8+PGNpcmNsZSBjeD0nNjAnIGN5PSc3MCcgcj0nMjgnIGZpbGw9JyNmZmZmZmYnIG9wYWNpdHk9JzAuMzUnLz48Y2lyY2xlIGN4PScxNTAnIGN5PScxNDAnIHI9JzQwJyBmaWxsPScjZmZmZmZmJyBvcGFjaXR5PScwLjI1Jy8+PC9zdmc+" }, "backgroundSize": "cover", "backgroundPosition": "center center", "backgroundRepeat": "no-repeat", "backgroundAttachment": "fixed" }, "color": { "text": "#1e1e1e", "background": "#ffffff", "gradient": "linear-gradient(135deg, #f6f7f7 0%, #e5f0f5 100%)" }, "typography": { "fontFamily": "var:preset|font-family|system", "fontSize": "1rem", "fontStyle": "italic", "fontWeight": "500", "lineHeight": "1.6", "letterSpacing": "0.02em", "textDecoration": "underline", "textTransform": "uppercase" }, "spacing": { "blockGap": "1.5rem", "margin": { "top": "2rem", "bottom": "2rem" }, "padding": { "top": "2rem", "right": "2rem", "bottom": "2rem", "left": "2rem" } }, "dimensions": { "minHeight": "120px", "minWidth": "200px" }, "border": { "color": "#d5d5d5", "style": "solid", "width": "1px", "radius": "8px" }, "elements": { "heading": { "color": { "text": "#0a1e2e" }, "typography": { "fontWeight": "700", "lineHeight": "1.2" } }, "button": { "color": { "text": "#ffffff", "background": "#0073aa" }, "border": { "radius": "4px" }, ":hover": { "color": { "background": "#005177" } } }, "link": { "color": { "text": "#0073aa" }, ":hover": { "color": { "text": "#005177" } } } } } } }, "customTemplates": [ { "name": "custom-template", "title": "Custom", "postTypes": [ "post" ] } ], "patterns": [ "short-text-surrounded-by-round-images", "partner-logos" ] }