Widgets: carry a declarative icon through the widget pipeline - #80969
Conversation
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
|
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
Size Change: +11.1 kB (+0.14%) Total Size: 7.78 MB 📦 View Changed
|
| "content": "Results come from your site's latest health checks.", | ||
| "links": [ { "label": "Visit Site Health", "href": "site-health.php" } ] | ||
| }, | ||
| "icon": "core/shield", |
|
Flaky tests detected in 8f32383. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/30551147884
|
chihsuan
left a comment
There was a problem hiding this comment.
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() ) ); |
There was a problem hiding this comment.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
#75715 (comment) and #75715 (comment) for more context
| * resolve. Non-elements are dropped. | ||
| */ | ||
| const resolvedIcon = record.icon | ||
| ? await resolveIcon( record.icon ) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Yes... Not sure about that. Probably yes; we should reserve an empty space for it.
There was a problem hiding this comment.
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.
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
594e86a to
f9022d7
Compare
|
Let's address it in this PR |
a transparent stand-in prevents the title shift
init modules run before the page renders
|
Answered here #80969 (comment) |



What
Adds
icontowidget.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-modulesREST record, and resolves client-side into a renderable element at theuseWidgetTypesboundary.All in-tree widgets now declare their icon in
widget.json; none keep a React element inwidget.ts. Icons that are not public in thecorecollection register server-side under a widget-owneddashboard-widgetscollection.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-namestring, resolvable server-side withwp_get_icon()and client-side through theiconcore-data entity. It can flow through the same pipeline as every other identity field.How
@wordpress/buildCarries
iconfromwidget.jsonintobuild/widgets/registry.php.WP_Widget_TypeGains
$icon. Registration validates thecollection/icon-nameshape (gutenberg_sanitize_widget_icon()) and drops anything else silently, so future authoring forms degrade instead of warning.REST
WP_REST_Widget_Modules_Controllerexposesicon(string|null, readonly) with schema.@wordpress/widget-primitivesAdds a single-slot resolver registry:
registerIconResolver, first registration wins.useWidgetTypesresolves 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, soWidgetType.iconis always renderable.@wordpress/dashboard-initRegisters the resolver at init, before the page renders: it reads the
iconcore-data entity and converts the SVG content to an element withsafeHTML+html-react-parser, the Icon block precedent.widget-icons.phpRegisters the
dashboard-widgetscollection for the core-private icons the widgets reference (drafts,wordpress,site-logo), sourced from the@wordpress/iconslibrary files.Welcome widget
Its module loses the dead
apiVersion, the non-functional stringicon, and the duplicatedcategory.Docs
An Icons page beside the field-types one, a
WithIconReferencestory, and the README and architecture doc updated to the declarative form.Testing
gutenberg-dashboard-widgetsexperiment and build:npm install && npm run buildEach record exposes
icon(core/shield,core/calendar,dashboard-widgets/drafts, ...).Follow-ups
iconthrough the widget pipeline #80938: accept inline SVG markup and widget-local.svgfiles, sanitized at registration likesanitize_icon_content().icontoschemas/json/widget.jsononce the schema file lands (Widgets: Addwidget.jsonmetadata schema #77640).dashboard-widgetscollection: promotedrafts,wordpress, andsite-logoto the public core set, or keep them widget-owned.WidgetTypeMetadata.apiVersionis required by the type but no widget declares it; settle the field.