close
Skip to content

Widgets: carry a declarative icon through the widget pipeline - #80969

Merged
retrofox merged 24 commits into
trunkfrom
update/widget-declarative-icon
Jul 31, 2026
Merged

Widgets: carry a declarative icon through the widget pipeline#80969
retrofox merged 24 commits into
trunkfrom
update/widget-declarative-icon

Conversation

@retrofox

@retrofox retrofox commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

What

Adds icon to widget.json: a declarative reference to a registered icon (collection/icon-name).

The reference travels through the build manifest, the PHP registry, and the wp/v2/widget-modules REST record, and resolves client-side into a renderable element at the useWidgetTypes boundary.

All in-tree widgets now declare their icon in widget.json; none keep a React element in widget.ts. Icons that are not public in the core collection register server-side under a widget-owned dashboard-widgets collection.

Part of #80938 (steps 1 and 2).

Why

The icon was the only identity field that could not travel as data: every surface had to import and execute the widget module to draw it, and a widget without a JS module could not declare one at all.

With the Icons API in place (#75715, #77260), an icon is addressable by a collection/icon-name string, resolvable server-side with wp_get_icon() and client-side through the icon core-data entity. It can flow through the same pipeline as every other identity field.

How

  • @wordpress/build
    Carries icon from widget.json into build/widgets/registry.php.

  • WP_Widget_Type
    Gains $icon. Registration validates the collection/icon-name shape (gutenberg_sanitize_widget_icon()) and drops anything else silently, so future authoring forms degrade instead of warning.

  • REST
    WP_REST_Widget_Modules_Controller exposes icon (string|null, readonly) with schema.

  • @wordpress/widget-primitives
    Adds a single-slot resolver registry: registerIconResolver, first registration wins. useWidgetTypes resolves record references through it. The resolved element wins over a module element; the module element stands when the reference is absent or unresolvable. Non-elements are dropped, so WidgetType.icon is always renderable.

  • @wordpress/dashboard-init
    Registers the resolver at init, before the page renders: it reads the icon core-data entity and converts the SVG content to an element with safeHTML + html-react-parser, the Icon block precedent.

  • widget-icons.php
    Registers the dashboard-widgets collection for the core-private icons the widgets reference (drafts, wordpress, site-logo), sourced from the @wordpress/icons library files.

  • Welcome widget
    Its module loses the dead apiVersion, the non-functional string icon, and the duplicated category.

  • Docs
    An Icons page beside the field-types one, a WithIconReference story, and the README and architecture doc updated to the declarative form.

Testing

  1. Enable the gutenberg-dashboard-widgets experiment and build:
npm install && npm run build
  1. Open Dashboard (Beta). Site Health, WordPress events, and Quick draft render their header icons, now resolved from the icon registry. Full-bleed widgets (Welcome, Hello World, Site preview) stay unchanged, since they draw no header.
image
  1. Check the records carry the reference:
/wp/v2/widget-modules
image

Each record exposes icon (core/shield, core/calendar, dashboard-widgets/drafts, ...).

  1. Check a widget-owned icon is served:
/wp/v2/icons/dashboard-widgets/drafts
image
  1. Storybook: the WidgetRender / With Icon Reference story shows the resolver seam in isolation, and the Widget Primitives / Icons page documents the mechanism.
npm run storybook:dev
image

Follow-ups

retrofox added 11 commits July 30, 2026 13:12
carry widget.json icon into the generated PHP registry
validate the collection/icon-name shape at registration
resolve record icon references in useWidgetTypes
resolve references against the icon entity as elements
temporary dashboard-widgets collection for core-private icons
replace module icon elements with registered references
cover icon in the architecture doc field lists and README
demonstrate registerIconResolver in the WidgetRender stories
@retrofox retrofox added [Type] Enhancement A suggestion for improvement. [Package] wp-build /packages/wp-build [Feature] Dashboard WordPress admin dashboard, widget framework, and layout customization [Package] Widget primitives labels Jul 30, 2026
@retrofox retrofox self-assigned this Jul 30, 2026
@retrofox
retrofox requested review from chihsuan and simison July 30, 2026 13:51
@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: retrofox <retrofox@git.wordpress.org>
Co-authored-by: chihsuan <chihsuan@git.wordpress.org>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown

Size Change: +11.1 kB (+0.14%)

Total Size: 7.78 MB

📦 View Changed
Filename Size Change
build/modules/dashboard-init/index.min.js 11.5 kB +10.9 kB (+1836.7%) 🆘
build/modules/widget-primitives/index.min.js 1.33 kB +174 B (+15.01%) ⚠️

compressed-size-action

"content": "Results come from your site's latest health checks.",
"links": [ { "label": "Visit Site Health", "href": "site-health.php" } ]
},
"icon": "core/shield",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is nice

@github-actions

Copy link
Copy Markdown

Flaky tests detected in 8f32383.
Some tests passed with failed attempts. The failures may not be related to this commit but are still reported for visibility. See the documentation for more information.

🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/30551147884
📝 Reported issues:

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice work @retrofox The direction looks good, and most of the pipeline appears consistent!

I left two comments that may represent user-visible regressions:

  • whether fill="currentColor" survives icon sanitization;
  • whether icon lookup should be allowed to hold the entire dashboard in its resolving state.

The remaining inline comments are nit notes.

* Whitespace around the root `<svg>` makes `parse()` return an
* array; take the element.
*/
const parsed = parse( safeHTML( record.content.trim() ) );

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found that <svg> element now doesn't have fill="currentColor" anymore. It's near-invisible on white (#1e1e1e vs#000000), but I believe the icons have stopped responding to the token entirely.

I think the underlying fix is to allow fill on <svg>.

Before:

Image

After:

Image

@retrofox retrofox Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch.

The sanitizer only allows fill on path/polygon, and on WP >= 7.0 it's Core's copy (class_exists guard), so allowing it on <svg> takes a Core patch; worth raising with #75715.

Meanwhile, the resolver restores it client-side: parsed roots without fill are cloned with fill="currentColor" (ef50796).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#75715 (comment) and #75715 (comment) for more context

* resolve. Non-elements are dropped.
*/
const resolvedIcon = record.icon
? await resolveIcon( record.icon )

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this await would now cause the dashboard shows only loading cards until every icon request settles. Worth moving this off the loading gate?

@retrofox retrofox Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right.

Icon resolution now runs off the gate (2f7e628): widget types emit as soon as their modules land and each resolved icon patches in, at the cost of a small header pop-in.

Added a regression test with a never-settling resolver.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, would it be worth reserving the icon slot now that resolution happens off the gate? Currently, the title jumps right when each icon land.

Screen.Recording.2026-07-31.at.5.43.56.PM.mov

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes... Not sure about that. Probably yes; we should reserve an empty space for it.

@retrofox retrofox Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the video; that made it easy to pin down. Fixed in 4ae8c4a: while a reference resolves, the hook fills icon with a transparent stand-in that holds the slot, so the title no longer shifts.

When the icon lands, it swaps in place; if the reference never resolves, the stand-in clears, and the slot collapses, which is the degraded path.

The resolver registration also moved to @wordpress/dashboard-init (fd1e5c4), so it is in place before the page renders.

Comment thread packages/widget-primitives/src/types.ts Outdated
retrofox added 3 commits July 31, 2026 08:50
the wire reference lives on the record only
widget types emit when modules land; icons patch in
the registry sanitizer strips fill from the svg root
@retrofox
retrofox force-pushed the update/widget-declarative-icon branch from 594e86a to f9022d7 Compare July 31, 2026 09:41

@chihsuan chihsuan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing feedback! @retrofox

I just observed a UI issue. Icon and title shift now that cons arrive after first paint, also it maybe worth updating icons.md to match the new two-phase timing but could be follow-ups.

@retrofox

Copy link
Copy Markdown
Contributor Author

Let's address it in this PR

@retrofox

Copy link
Copy Markdown
Contributor Author

Answered here #80969 (comment)

@retrofox
retrofox merged commit 0e03429 into trunk Jul 31, 2026
60 of 61 checks passed
@retrofox
retrofox deleted the update/widget-declarative-icon branch July 31, 2026 14:41
@github-actions github-actions Bot added this to the Gutenberg 23.8 milestone Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Feature] Dashboard WordPress admin dashboard, widget framework, and layout customization [Package] Widget primitives [Package] wp-build /packages/wp-build [Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants