Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e675c92
add info property to widget type metadata
retrofox Jul 2, 2026
0a575ae
surface widget info as header tooltip
retrofox Jul 2, 2026
64d088f
add info notes to bundled widgets
retrofox Jul 2, 2026
8b85a9b
derive widget record overrides from metadata
retrofox Jul 2, 2026
16e7cd4
replace info note button with plain tooltip
retrofox Jul 2, 2026
6d029f2
reimplement info note as an infotip popover
retrofox Jul 3, 2026
c48cd69
adjust infotip position and stacking
retrofox Jul 3, 2026
f721622
open infotip on click, anchor bottom-start
retrofox Jul 3, 2026
086da25
allow renderable info in widget metadata
retrofox Jul 3, 2026
3b49706
link site health from its info note
retrofox Jul 3, 2026
9728d3a
keep tab focus inside the info popover
retrofox Jul 3, 2026
8df9c67
replace info with a declarative help schema
retrofox Jul 3, 2026
9e3d39a
render the help note in the header infotip
retrofox Jul 3, 2026
a36b0d5
declare widget help in widget.json
retrofox Jul 3, 2026
daecbea
render help emphasis via createInterpolateElement
retrofox Jul 3, 2026
e3242ec
mute the help icon and drop the z-index override
retrofox Jul 3, 2026
5e311d3
rename help actions to links
retrofox Jul 3, 2026
de9d394
Merge branch 'trunk' into update/widget-info-property
retrofox Jul 3, 2026
4232d9a
rename help text to content
retrofox Jul 3, 2026
a8aedd9
extract the header infotip into a component
retrofox Jul 3, 2026
4f16520
divide adjacent infotip links
retrofox Jul 3, 2026
5c67135
trim help types from the public API
retrofox Jul 3, 2026
53c811d
remove testing widget link
retrofox Jul 3, 2026
cf24dcd
fix help section for health widget
retrofox Jul 3, 2026
42b3a2a
fix tooltip max width and position
retrofox Jul 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,10 @@ public function prepare_item_for_response( $item, $request ) {
$data['description'] = $widget_type->description;
}

if ( rest_is_field_included( 'info', $fields ) ) {
$data['info'] = $widget_type->info;
}

if ( rest_is_field_included( 'keywords', $fields ) ) {
$data['keywords'] = $widget_type->keywords;
}
Expand Down Expand Up @@ -283,6 +287,13 @@ public function get_item_schema() {
'readonly' => true,
),

'info' => array(
'description' => __( 'Brief contextual note about the widget type, meant for compact surfaces such as tooltips. Translatable.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),

'keywords' => array(
'description' => __( 'Alternative terms used to match the widget type when searching, e.g. "calendar" for an events widget. Translatable.', 'gutenberg' ),
'type' => array( 'array', 'null' ),
Expand Down
11 changes: 11 additions & 0 deletions lib/experimental/dashboard-widgets/class-wp-widget-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ class WP_Widget_Type {
*/
public $description = null;

/**
* Brief contextual note about the widget type, meant for compact
* surfaces such as tooltips. Translated at registration time using
* the widget's text domain.
*
* Null when the widget did not declare the field.
*
* @var string|null
*/
public $info = null;

/**
* Alternative terms used to match the widget type when searching,
* e.g. "calendar" for an events widget. Translated at registration
Expand Down
1 change: 1 addition & 0 deletions lib/experimental/dashboard-widgets/widget-i18n.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"title": "widget title",
"description": "widget description",
"info": "widget info",
"keywords": [ "widget keyword" ]
}
9 changes: 5 additions & 4 deletions lib/experimental/dashboard-widgets/widget-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ function gutenberg_get_widget_metadata_i18n_schema() {
/**
* Translates a widget's user-facing metadata strings.
*
* Runs `title`, `description`, and `keywords` through the widget i18n schema
* using the widget's `textdomain`, leaving every other key untouched. A no-op
* when the widget declares no `textdomain`.
* Runs `title`, `description`, `info`, and `keywords` through the widget
* i18n schema using the widget's `textdomain`, leaving every other key
* untouched. A no-op when the widget declares no `textdomain`.
*
* @param array $widget Widget data from the build manifest.
* @return array Widget data with its translatable strings localized.
Expand All @@ -52,7 +52,7 @@ function gutenberg_translate_widget_metadata( $widget ) {

$i18n_schema = gutenberg_get_widget_metadata_i18n_schema();

foreach ( array( 'title', 'description', 'keywords' ) as $field ) {
foreach ( array( 'title', 'description', 'info', 'keywords' ) as $field ) {
if ( isset( $widget[ $field ], $i18n_schema[ $field ] ) ) {
$widget[ $field ] = translate_settings_using_i18n_schema( $i18n_schema[ $field ], $widget[ $field ], $textdomain );
}
Expand Down Expand Up @@ -93,6 +93,7 @@ function gutenberg_register_widget_types() {
'category' => $widget['category'] ?? null,
'title' => $widget['title'] ?? null,
'description' => $widget['description'] ?? null,
'info' => $widget['info'] ?? null,
'keywords' => $widget['keywords'] ?? null,
)
);
Expand Down
5 changes: 5 additions & 0 deletions packages/widget-dashboard/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Enhancements

- `WidgetHeader`: surface the widget type's `info` note as a tooltip
affordance beside the title.

## 0.2.0 (2026-07-01)

## 0.1.0 (2026-06-24)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
min-width: 0;
}

.icon {
.icon,
.info {
display: inline-flex;
color: var(--wpds-color-foreground-content-neutral);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import type { ReactNode } from 'react';
/**
* WordPress dependencies
*/
import { Card, Icon, Stack } from '@wordpress/ui';
import { info } from '@wordpress/icons';
import { Card, Icon, Stack, Tooltip, VisuallyHidden } from '@wordpress/ui';
import type { WidgetType } from '@wordpress/widget-primitives';

/**
Expand All @@ -17,7 +18,8 @@ import styles from './widget-header.module.css';

export interface WidgetHeaderProps {
/**
* Widget type, source of the icon and title shown as identity.
* Widget type, source of the icon, title, and info note shown as
* identity.
*/
widgetType?: WidgetType;

Expand Down Expand Up @@ -83,8 +85,28 @@ export function WidgetHeader( {
<Card.Title id={ titleId } render={ <h2 /> }>
{ widgetType.title }
</Card.Title>

{ widgetType.info && (
<Tooltip.Root>
<Tooltip.Trigger
render={
<span className={ styles.info }>
<Icon
icon={ info }
aria-hidden="true"
/>
<VisuallyHidden>
{ widgetType.info }
</VisuallyHidden>
</span>
}
/>
<Tooltip.Popup>{ widgetType.info }</Tooltip.Popup>
</Tooltip.Root>
) }
</Stack>
) }

{ children && <div className={ styles.toolbar }>{ children }</div> }
</Card.Header>
);
Expand Down
11 changes: 11 additions & 0 deletions packages/widget-dashboard/src/test/widget-dashboard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const widgetTypes: WidgetType[] = [
apiVersion: 1,
name: 'test/greet',
title: 'Greet',
info: 'Greetings at a glance.',
renderModule: 'test-greet-module',
},
];
Expand Down Expand Up @@ -101,6 +102,16 @@ describe( 'WidgetDashboard', () => {
);
} );

it( 'surfaces the widget type info note in the header', async () => {
render( <Harness /> );

// The visually hidden copy inside the tooltip trigger; the popup
// itself only mounts on hover/focus.
expect(
await screen.findByText( 'Greetings at a glance.' )
).toBeInTheDocument();
} );

it( 'threads setAttributes into onLayoutChange on commit with merged attributes', async () => {
const onChange = jest.fn();
render( <Harness onLayoutChange={ onChange } /> );
Expand Down
3 changes: 3 additions & 0 deletions packages/widget-primitives/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
- `WidgetModuleRecord`: add optional `title`, `description`, and
`keywords`, overlaid onto the metadata module's values. Lets a host
supply metadata translated server-side.
- `WidgetTypeMetadata`: add optional `info`, a brief contextual note
meant for compact surfaces such as tooltips. Also carried by
`WidgetModuleRecord`, overlaid onto the metadata module's value.

## 0.2.0 (2026-07-01)

Expand Down
1 change: 1 addition & 0 deletions packages/widget-primitives/src/hooks/use-widget-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export function useWidgetTypes(
...( record.description
? { description: record.description }
: {} ),
...( record.info ? { info: record.info } : {} ),
...( record.keywords
? { keywords: record.keywords }
: {} ),
Expand Down
2 changes: 1 addition & 1 deletion packages/widget-primitives/src/stories/anatomy.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The three layers are what a widget _is_, how it _asks_ to be framed, and what it

## Identity

What the widget is: `name`, `title`, `description`, `icon`, `category`, `keywords`.
What the widget is: `name`, `title`, `description`, `info`, `icon`, `category`, `keywords`.

The host shows this almost verbatim, in pickers, headers, and help. It decides _where_ identity appears and _whether_ to show it, never _what it says_. Identity is copied, not interpreted: two hosts displaying the same widget show the same title.

Expand Down
51 changes: 25 additions & 26 deletions packages/widget-primitives/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ export interface WidgetTypeMetadata< Item = unknown > {
*/
description?: string;

/**
* Brief contextual note about the widget type, meant for compact
* surfaces such as tooltips. Translatable.
*/
info?: string;

/**
* Visual identifier for the widget type; hosts decide where, and
* whether, to render it.
Expand Down Expand Up @@ -192,12 +198,30 @@ export type ResolveWidgetModule = (
moduleId: string
) => Promise< WidgetModule >;

/**
* The `WidgetTypeMetadata` subset a record may carry, resolved server-side
* (already translated) and overriding the metadata module's values. Every
* field is optional and nullable; `null`/absent means the module's value
* stands.
*/
type WidgetModuleRecordOverrides = {
[ K in keyof Pick<
WidgetTypeMetadata,
| 'title'
| 'description'
| 'info'
| 'category'
| 'presentation'
| 'keywords'
> ]?: WidgetTypeMetadata[ K ] | null;
};

/**
* Per-widget record a host feeds to `useWidgetTypes`, in snake_case wire
* format. The host fetches these however it likes; only the field shape is
* part of the contract.
*/
export interface WidgetModuleRecord {
export interface WidgetModuleRecord extends WidgetModuleRecordOverrides {
/**
* Stable widget type identifier.
*/
Expand All @@ -212,29 +236,4 @@ export interface WidgetModuleRecord {
* Script-module id dynamically imported for the widget's live metadata.
*/
widget_module?: string | null;

/**
* Authoring presentation hint; overrides the metadata module's value.
*/
presentation?: WidgetTypeMetadata[ 'presentation' ] | null;

/**
* Grouping category; overrides the metadata module's value.
*/
category?: WidgetTypeMetadata[ 'category' ] | null;

/**
* Translated title; overrides the metadata module's value.
*/
title?: WidgetTypeMetadata[ 'title' ] | null;

/**
* Translated description; overrides the metadata module's value.
*/
description?: WidgetTypeMetadata[ 'description' ] | null;

/**
* Translated search aliases; override the metadata module's value.
*/
keywords?: WidgetTypeMetadata[ 'keywords' ] | null;
}
5 changes: 5 additions & 0 deletions packages/wp-build/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

## Unreleased

### Enhancements

- Widgets: carry the optional `info` field from `widget.json` into the
generated PHP registry.

## 0.18.0 (2026-07-01)

## 0.17.0 (2026-06-24)
Expand Down
5 changes: 4 additions & 1 deletion packages/wp-build/lib/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2016,7 +2016,7 @@ async function buildAllWidgets() {
* Discover all widgets and collect their registry-facing data.
* Widgets without a valid widget.json are skipped.
*
* @return {Array<{ name: string, dirName: string, title: string | null, description: string | null, hasRender: boolean, hasWidget: boolean, presentation: string | null, category: string | null, keywords: string[] | null, textdomain: string | null }>} Array of widget objects.
* @return {Array<{ name: string, dirName: string, title: string | null, description: string | null, info: string | null, hasRender: boolean, hasWidget: boolean, presentation: string | null, category: string | null, keywords: string[] | null, textdomain: string | null }>} Array of widget objects.
*/
function collectWidgets() {
return getAllWidgets( ROOT_DIR ).flatMap( ( widgetName ) => {
Expand All @@ -2037,6 +2037,7 @@ function collectWidgets() {
dirName: widgetName,
title: metadata.title ?? null,
description: metadata.description ?? null,
info: metadata.info ?? null,
hasRender: widgetFiles.hasRender,
hasWidget: widgetFiles.hasWidget,
presentation: metadata.presentation ?? null,
Expand Down Expand Up @@ -2104,13 +2105,15 @@ async function generateWidgetRegistry( widgets, replacements ) {
const categoryStr = toPhpStringLiteral( widget.category );
const titleStr = toPhpStringLiteral( widget.title );
const descriptionStr = toPhpStringLiteral( widget.description );
const infoStr = toPhpStringLiteral( widget.info );
const keywordsStr = toPhpStringArrayLiteral( widget.keywords );
const textdomainStr = toPhpStringLiteral( widget.textdomain );
return `\tarray(
'name' => '${ widget.name }',
'dir_name' => '${ widget.dirName }',
'title' => ${ titleStr },
'description' => ${ descriptionStr },
'info' => ${ infoStr },
'has_render' => ${ hasRenderStr },
'has_widget' => ${ hasWidgetStr },
'presentation' => ${ presentationStr },
Expand Down
1 change: 1 addition & 0 deletions packages/wp-build/lib/widget-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export function getAllWidgets( rootDir ) {
* @property {string} name Widget namespaced identifier.
* @property {string} [title] Human-readable title.
* @property {string} [description] Short description.
* @property {string} [info] Brief contextual note for compact surfaces such as tooltips.
* @property {string} [category] Grouping category.
* @property {'framed' | 'content-bleed' | 'full-bleed'} [presentation] Authoring intent about how the widget wants to render.
* @property {string[]} [keywords] Search aliases used to match the widget.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ private function register_sample_widgets() {
'category' => 'dashboard',
'title' => 'Widget A',
'description' => 'The first test widget.',
'info' => 'Everything about A, briefly.',
'keywords' => array( 'alpha', 'first' ),
)
);
Expand Down Expand Up @@ -131,6 +132,7 @@ public function test_get_item_returns_widget_by_name() {
$this->assertSame( 'dashboard', $data['category'] );
$this->assertSame( 'Widget A', $data['title'] );
$this->assertSame( 'The first test widget.', $data['description'] );
$this->assertSame( 'Everything about A, briefly.', $data['info'] );
$this->assertSame( array( 'alpha', 'first' ), $data['keywords'] );
}

Expand Down Expand Up @@ -180,13 +182,15 @@ public function test_schema_exposes_expected_properties() {
$this->assertArrayHasKey( 'category', $properties );
$this->assertArrayHasKey( 'title', $properties );
$this->assertArrayHasKey( 'description', $properties );
$this->assertArrayHasKey( 'info', $properties );
$this->assertArrayHasKey( 'keywords', $properties );
$this->assertSame( 'string', $properties['name']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['render_module']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['widget_module']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['category']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['title']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['description']['type'] );
$this->assertSame( array( 'string', 'null' ), $properties['info']['type'] );
$this->assertSame( array( 'array', 'null' ), $properties['keywords']['type'] );
}
}
4 changes: 4 additions & 0 deletions phpunit/experimental/widget-types-test.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function test_i18n_schema_declares_translatable_fields() {

$this->assertSame( 'widget title', $schema['title'] );
$this->assertSame( 'widget description', $schema['description'] );
$this->assertSame( 'widget info', $schema['info'] );
$this->assertSame( array( 'widget keyword' ), $schema['keywords'] );
}

Expand All @@ -37,6 +38,7 @@ public function test_translate_widget_metadata_localizes_strings() {
'name' => 'core/welcome',
'title' => 'Welcome',
'description' => 'Displays a welcome panel.',
'info' => 'Welcome at a glance.',
'keywords' => array( 'start' ),
'category' => 'dashboard',
'textdomain' => 'default',
Expand All @@ -45,6 +47,7 @@ public function test_translate_widget_metadata_localizes_strings() {

$this->assertSame( 'Bienvenido', $widget['title'] );
$this->assertSame( 'Muestra un panel de bienvenida.', $widget['description'] );
$this->assertSame( 'Bienvenida de un vistazo.', $widget['info'] );
$this->assertSame( array( 'inicio' ), $widget['keywords'] );
$this->assertSame( 'dashboard', $widget['category'] );
}
Expand Down Expand Up @@ -78,6 +81,7 @@ public function translate_to_spanish( $translation, $text, $context, $domain ) {
$messages = array(
'widget title' => array( 'Welcome' => 'Bienvenido' ),
'widget description' => array( 'Displays a welcome panel.' => 'Muestra un panel de bienvenida.' ),
'widget info' => array( 'Welcome at a glance.' => 'Bienvenida de un vistazo.' ),
'widget keyword' => array( 'start' => 'inicio' ),
);

Expand Down
Loading
Loading