Repository navigation
Global Styles: explore the inheritance cascade in the style panels' options menu - #81044
JosVelasco wants to merge 3 commits into
Conversation
`resolveStyle` collapsed every contributing layer down to the winning one, keeping only that layer in `sources`. An origin view needs the layers it beat in order to explain why a value is what it is, so record them. `cascade` is a new key on the returned object, ordered low to high precedence per leaf path, with the winner flagged. `sources` and `value` are unchanged, so nothing downstream is affected. Layers carry structural metadata (block name, element, variation) rather than a pre-built string, leaving the engine framework- and i18n-agnostic. Root-level non-cascading paths are dropped from `cascade` alongside `value` and `sources`; where that removes the winner, the highest remaining layer is promoted so each path keeps exactly one. Ported unchanged from WordPress#80993.
Adds `__experimentalMenuItemDescription`, a `( label ) => ReactNode` callback that the panel's options dropdown calls per generated menu item. Whatever it returns is rendered under that item. The content is `aria-hidden` and referenced by the item's `aria-describedby` rather than nested inside it. That keeps `role="menu"` holding only menu items, and keeps the description out of the item's accessible *name* — nesting would fold it in and make each control unqueryable by its own label. Assistive technology still reaches it, as that item's description. A described item is bracketed by a rule above and below, matching the one above `Reset all`. The rule is drawn as a pseudo-element so it reaches the dropdown edges while the item's own background and focus ring keep their inset width; back-to-back described items suppress the duplicate. The callback is invoked within the panel's own React tree, so it can read context provided around the `ToolsPanel`.
…ions menu
Replaces the per-control override indicator with the cascade itself, shown in
each style panel's options ("ellipsis") menu under the control it belongs to.
Nothing is rendered at rest — not in the control rows, not on the panel's menu
toggle. Inherited values read as values. Opening the menu shows, for every
control holding a local value over an inherited one, the layers behind it: the
value in effect, then each layer it covers, struck through, with the origin that
set it. The `Reset` the menu already offers that control is the action that puts
it back, so no new action is introduced.
Layer labels name scope, so reading down narrows it: Site-wide, All Paragraph
blocks, Subtitle style, This block, Custom CSS. Values are shortened to fit a
menu row — colour presets resolve to their hex, and fluid typography's `clamp()`
collapses to the range it moves between.
The cascade resolves on the panel *item* side: the block inspector renders its
panels outside the selected block's edit context, so only the items, which are
slot fills in the block's tree, know which block they belong to. They publish
into a registry the panel reads, forwarded across the `bubblesVirtually` slot
boundary the way `ToolsPanelContext` already is. It is only computed while a
control actually overrides something, and propagates on a value signature rather
than array identity, so typing does not re-render the panel.
Because the placement replaces rather than supplements, `InheritanceResetButton`
and its styles are removed, and the four controls that swapped their ordinary
reset for the dot — colour/gradient, shadow, duotone, background image — get
that reset back unconditionally.
Everything stays behind the existing `gutenberg-global-styles-inheritance-ui`
experiment.
|
Thanks for this PR, for following up, and for the easily reviewable video. Absent a few details, there's something interesting here in that the footprint is small at rest, but in the dropdown there's room to be verbose which is good. We'd want to fiddle with the details, get the typography, listing, visuals just right, and I don't think the underline is needed here either: just show the inherited value, no indicators. But all that is secondary to getting broader validation: how does the broader group feel about this pattern? CC: @WordPress/gutenberg-design. The main question that comes to mind for me is: if the ToolsPanel dropdown will become the (sole?) interface for showing inheritance context, are there any design tools that can inherit values that are at present not shown inside a toolspanel? Nothing comes to mind at a glance, but this is key: if that's indeed the case, it feels an obvious place to show expanded context because there's room in the dropdown. |
|
It’s interesting that the approach places the reset action next to the block’s values, but I don’t see it scaling well when multiple elements display their values. Even when it shows a single value and its origin, it already overloads the menu, in addition to the current enable/disable tool and reset actions. I like the idea of showing the current and overridden values, but a rich popover, like the one here on GitHub when hovering over the profile image, could convey the info more clearly. Basically what you did in #80993 but behaving as a tooltip that only provides information. |
Important note
This is an exploration, opened as a draft to keep the conversation going. It is not proposed for merge as-is — see Known gaps, one of which is architectural and one of which is a deliberate behaviour change. It builds on #80649, #80993 and @jasmussen's comment. Take it over, cherry-pick from it, or close it.
What?
@jasmussen, on #80993:
This is that, and it replaces the per-control dot rather than sitting alongside it. Nothing is rendered at rest — not on the control rows, not on the panel's options toggle. Inherited values simply read as values. Open a style panel's options menu and each control that overrides an inherited value shows its cascade under its own row, beside the
Resetthat already puts the value back:global-styles-inheritance-panels.mp4
Uses the existing
gutenberg-global-styles-inheritance-uiexperiment — no new flag, and no PHP changes at all. To compare against the per-control dot, compare againsttrunk.Why this shape
No new action. The menu already lists every control and already offers each one a
Reset— which for a control holding a local value over an inherited one is the "put it back" action. So nothing is added to the menu; the cascade slots in under the row that already exists. An earlier revision had a bulkRestore inherited stylesand it became redundant the moment the rows moved under their controls, taking a staged-restore queue with it.Values, not a marker. A one-bit indicator can say overridden but not what was overridden, what the value would fall back to, or where that comes from — the questions that send people to the browser inspector. A menu row has space for all three; a control row does not. That is the argument for this placement, more than the mobile one.
Nothing at rest is a deliberate trade. @jasmussen: "I'd personally also be fine with—to start—landing with nothing but the actual inheritance shown as values: no indicators at all." The cost is discoverability: nothing announces that a block overrides anything, and you find out by opening the menu. Every at-rest marker so far has had to answer "where does it go on a row that is entirely a flyout toggle, and how is it tapped on mobile" — a placement with no marker doesn't have to.
How?
Engine.
resolveStylekeeps the full per-path cascade (cascade: Record<path, CascadeEntry[]>) instead of collapsing to the winning layer.sourcesis unchanged. Layers carry structural metadata rather than pre-built strings, so the UI composes and translates the labels. Ported unchanged from #80993 — this is its second consumer, and an argument for landing it as its own PR.Labels name scope, so reading down narrows it: Site-wide → All Paragraph blocks → Subtitle style → This block → Custom CSS. Deliberately not "From block settings" and friends: that names where a value is stored rather than how far it reaches, a different question from the one every other row answers.
Values are shortened to fit a menu row. Colour presets resolve to their hex — a slug names the preset but not what it paints. Fluid typography's
clamp()collapses to1.5 – 1.75remrather than a 50-character viewport formula.Paths.
InheritanceToolsPanelItemtakes astylePathsprop (Appearance passes two, element colours three), wired at all 26 call sites.Cascade resolves on the item side. The block inspector renders panels outside the selected block's edit context, so only the items — slot fills in the block's tree — know which block they belong to. They publish into a registry the panel reads, forwarded across the
bubblesVirtuallyboundary the wayToolsPanelContextalready is. Only computed while a control actually overrides something, and propagated on a value signature rather than array identity, so typing doesn't re-render the panel.What this removes
Because the placement replaces rather than supplements,
InheritanceResetButtonand its styles are deleted, and the four controls that swapped their ordinary reset for the dot — colour/gradient, shadow, duotone, background image — get that reset back unconditionally. That incidentally resolves @t-hamano's point on #80649 that colour controls lost their reset in order to gain an indicator.has-local-override-from-global-stylesis kept as a class hook but now paints nothing — it is where a future at-rest treatment would attach.Accessibility
Audited in the browser, not assumed:
aria-hiddensibling referenced by the item'saria-describedby.role="menu"keeps only menu items as children, and the accessible name staysReset Size— nesting would have folded the cascade into the name and made every overriding control unqueryable by its own label.opacity: 0.7put these at 2.67:1 and was removed; the strikethrough carries the meaning and is not a colour-only encoding.RESETtext.Testing instructions
Reseton a row returns it to the next layer down, not to the bottom of the stack.trunkfor the per-control dot this replaces.Known gaps
ToolsPanelAPI change, and Properly support Global Styles inheritance indicators UI #80652 says not to make one. That issue states abstractions should stay local to Global Styles and that higher-level form component APIs should not gain support for this pattern while the design is unsettled. This adds__experimentalMenuItemDescriptiontoToolsPanelin@wordpress/components— a shared component used by every block's inspector. I could not find a way to do it locally: the menu is generated from ToolsPanel's internal item registry,dropdownMenuPropscannot reach its children, and the header is not replaceable. That cost is itself a finding: the per-control indicator lives in the control row, which Global Styles already owns; this menu belongs to ToolsPanel. If this direction is pursued it needs either a sanctioned extension point or a local reconstruction of the panel header.1.5 – 1.75remis swallowed by the strikethrough on losing layers.useToolsPanelDropdownMenuPropsdrops its popover placement and the menu positions differently. Needs a real device pass — which matters, since mobile reachability is this exploration's premise.CHANGELOG.mdentries, deliberately.AI disclosure
bubblesVirtuallyslot boundary, a separator overlapped a focused item's ring, and the contrast failure above. Treat the code as exploratory.