Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -182,13 +182,19 @@ public function prepare_item_for_response( $item, $request ) {
if ( rest_is_field_included( 'name', $fields ) ) {
$data['name'] = $widget_type->name;
}

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

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

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

$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
$data = $this->add_additional_fields_to_object( $data, $request );
$data = $this->filter_response_by_context( $data, $context );
Expand Down Expand Up @@ -217,18 +223,28 @@ public function get_item_schema() {
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),

'render_module' => array(
'description' => __( 'Script-module handle for the widget render entry point.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),

'widget_module' => array(
'description' => __( 'Script-module handle for the widget metadata entry point.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),

'presentation' => array(
'description' => __( 'Authoring intent about how the widget wants to render.', 'gutenberg' ),
'type' => array( 'string', 'null' ),
'enum' => array_merge( WP_Widget_Type::PRESENTATION_VALUES, array( null ) ),
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
),
);

Expand Down
18 changes: 18 additions & 0 deletions lib/experimental/dashboard-widgets/class-wp-widget-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
#[AllowDynamicProperties]
class WP_Widget_Type {

/**
* Allowed values for the `presentation` field. Treated as the
* single source of truth across the registry, REST schema, and
* any consumer that needs to validate or enumerate the set.
*/
const PRESENTATION_VALUES = array( 'framed', 'full-bleed' );

/**
* Widget type key. Namespaced identifier, e.g. `core/hello-world`.
*
Expand Down Expand Up @@ -49,6 +56,17 @@ class WP_Widget_Type {
*/
public $widget_module = null;

/**
* Authoring intent about how the widget wants to render. Static
* and declarative; not a user-editable attribute.
*
* One of {@see self::PRESENTATION_VALUES} (first entry is the
* default). Null when the widget did not declare the field.
*
* @var string|null
*/
public $presentation = null;

/**
* Constructor.
*
Expand Down
1 change: 1 addition & 0 deletions lib/experimental/dashboard-widgets/widget-types.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function gutenberg_register_widget_types() {
array(
'render_module' => $widget['render_module'] ?? null,
'widget_module' => $widget['widget_module'] ?? null,
'presentation' => $widget['presentation'] ?? null,
)
);
}
Expand Down
15 changes: 10 additions & 5 deletions packages/wp-build/lib/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1952,7 +1952,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, hasRender: boolean, hasWidget: boolean }>} Array of widget objects.
* @return {Array<{ name: string, dirName: string, hasRender: boolean, hasWidget: boolean, presentation: string | null }>} Array of widget objects.
*/
function collectWidgets() {
return getAllWidgets( ROOT_DIR ).flatMap( ( widgetName ) => {
Expand All @@ -1973,6 +1973,7 @@ function collectWidgets() {
dirName: widgetName,
hasRender: widgetFiles.hasRender,
hasWidget: widgetFiles.hasWidget,
presentation: metadata.presentation ?? null,
},
];
} );
Expand All @@ -1998,11 +1999,15 @@ async function generateWidgetRegistry( widgets, replacements ) {
.map( ( widget ) => {
const hasRenderStr = widget.hasRender ? 'true' : 'false';
const hasWidgetStr = widget.hasWidget ? 'true' : 'false';
const presentationStr = widget.presentation
? `'${ widget.presentation }'`
: 'null';
return `\tarray(
'name' => '${ widget.name }',
'dir_name' => '${ widget.dirName }',
'has_render' => ${ hasRenderStr },
'has_widget' => ${ hasWidgetStr },
'name' => '${ widget.name }',
'dir_name' => '${ widget.dirName }',
'has_render' => ${ hasRenderStr },
'has_widget' => ${ hasWidgetStr },
'presentation' => ${ presentationStr },
)`;
} )
.join( ',\n' );
Expand Down
9 changes: 5 additions & 4 deletions packages/wp-build/lib/widget-utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ export function getAllWidgets( rootDir ) {

/**
* @typedef {Object} WidgetMetadata
* @property {string} name Widget namespaced identifier.
* @property {string} [title] Human-readable title.
* @property {string} [description] Short description.
* @property {string} [category] Grouping category.
* @property {string} name Widget namespaced identifier.
* @property {string} [title] Human-readable title.
* @property {string} [description] Short description.
* @property {string} [category] Grouping category.
* @property {'framed' | 'full-bleed'} [presentation] Authoring intent about how the widget wants to render.
*/

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,24 @@

.widgetChromeContent {
flex: 1;
height: 100%;

/*
* Probably remove the following padding
* once #77856 lands.
* @see https://github.com/WordPress/gutenberg/pull/77856
*/
padding-top: 0;
padding-bottom: 0;
}

/**
* Full-bleed widgets are rendered in a full-bleed container.
* Probably remove the following CSS once #77586 lands.
* @see https://github.com/WordPress/gutenberg/pull/77586
*/
.widgetChromeContentFullBleed {
height: 100%;
}

.loading {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { __ } from '@wordpress/i18n';
// Dashboard is still experimental.
// eslint-disable-next-line @wordpress/use-recommended-components
import { Card, Stack, Notice } from '@wordpress/ui';
import { Card, Stack, Notice, VisuallyHidden } from '@wordpress/ui';

/**
* Internal dependencies
Expand Down Expand Up @@ -126,6 +126,16 @@ export const WidgetChrome = forwardRef< HTMLDivElement, WidgetChromeProps >(
return null;
}

const isFullBleed = widgetType.presentation === 'full-bleed';
const header = <Header titleId={ titleId } widgetType={ widgetType } />;
const body = (
<WidgetErrorBoundary>
<Suspense fallback={ <LoadingOverlay /> }>
<WidgetRender widget={ widget } widgetType={ widgetType } />
</Suspense>
</WidgetErrorBoundary>
);

return (
<WidgetContextProvider value={ contextValue }>
<Card.Root
Expand All @@ -135,16 +145,24 @@ export const WidgetChrome = forwardRef< HTMLDivElement, WidgetChromeProps >(
aria-labelledby={ widgetType.title ? titleId : undefined }
{ ...( editMode ? { inert: '' } : {} ) }
>
<Header titleId={ titleId } widgetType={ widgetType } />
{ isFullBleed ? (
<VisuallyHidden>{ header }</VisuallyHidden>
) : (
header
) }

<Card.Content className={ styles.widgetChromeContent }>
<WidgetErrorBoundary>
<Suspense fallback={ <LoadingOverlay /> }>
<WidgetRender
widget={ widget }
widgetType={ widgetType }
/>
</Suspense>
</WidgetErrorBoundary>
{ isFullBleed ? (
<Card.FullBleed
className={
styles.widgetChromeContentFullBleed
}
>
{ body }
</Card.FullBleed>
) : (
body
) }
</Card.Content>
</Card.Root>
</WidgetContextProvider>
Expand Down
141 changes: 15 additions & 126 deletions routes/dashboard/widget-dashboard/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/**
* Widget type definitions.
* Widget type definitions for the dashboard engine.
*
* The widget identity types (`WidgetName`, `WidgetTypeMetadata`,
* `WidgetType`) live in `routes/dashboard/widget-types/types` and are
* re-exported here so dashboard internals can pull every type they need
* from a single module. The local declarations below cover the
* dashboard-specific surface area: `DashboardWidget`, render props,
* module resolver, grid settings, and the `WidgetDashboard` prop bag.
*/

/**
Expand All @@ -10,139 +17,21 @@ import type { ComponentType, ReactNode } from 'react';
/**
* WordPress dependencies
*/
import type { IconType } from '@wordpress/components';
import type { Field } from '@wordpress/dataviews';
import type {
DashboardGridLayoutItem,
DashboardLanesLayoutItem,
} from '@wordpress/grid';

/*
* MIGRATION: `WidgetName`, `WidgetTypeMetadata`, and `WidgetType` below
* are also defined in `@wordpress/widget-types` (currently on its own
* branch). When that package lands in trunk, replace the three
* declarations with:
*
* export type {
* WidgetName,
* WidgetTypeMetadata,
* WidgetType,
* } from '@wordpress/widget-types';
*
* The shapes are kept identical on purpose so the swap is mechanical —
* any change to the fields here must land in lockstep on the
* `@wordpress/widget-types` package to keep the cutover trivial.
*/

/**
* Widget type identifier, structured as `<widget-namespace>/<widget-name>`.
* Both segments are lowercase, kebab-case; the full character pattern is
* enforced by the `widget.json` schema at authoring time.
*/
export type WidgetName = `${ string }/${ string }`;

/**
* Literal contents of a widget's `widget.json` metadata file.
*
* Captures the *authoring* shape only — module entry points and style
* assets are discovered by convention from the widget directory
* (`render.*`, `widget.*`, `render.scss`), not declared here.
*
* Consumed by tooling (IDE autocomplete, validation, the build pipeline).
* The dashboard engine consumes the richer `WidgetType` below, which
* extends this shape with runtime-only fields produced by the build
* manifest.
* Internal dependencies
*/
export interface WidgetTypeMetadata {
/**
* Version of the Widget API used by the widget.
*/
apiVersion: number;

/**
* Stable type identifier. See `WidgetName` for the shape.
*/
name: WidgetName;

/**
* Display title; shown in the inserter.
*/
title: string;

/**
* Short description shown in the widget inspector.
*/
description?: string;

/**
* Visual identifier shown in the widget header; dashicon string, React node, or SVG component.
*/
icon?: IconType;

/**
* Grouping category. Core provides `dashboard`; plugins and themes may
* register custom categories.
*/
category?: string;

/**
* Search aliases used to surface the widget from the inserter.
*/
keywords?: string[];

/**
* Widget version — used for asset cache invalidation.
*/
version?: string;

/**
* Gettext text domain for translations.
*/
textdomain?: string;

/**
* Experiment gate — boolean `true`, or a specific experiment name.
*/
__experimental?: string | boolean;

/**
* Declarative attribute schema. Surfaces render forms straight from
* this list via `DataForm`, with no per-widget form wiring. `any` is
* used here because the array is heterogeneous — each widget narrows
* `Item` to its own attribute type at the point of registration.
*/
attributes?: Field< any >[];

/**
* Structured example data for the Inspector Help Panel preview, and
* the default attributes applied by `createDashboardWidget` when no
* initial attributes are supplied.
*/
example?: {
attributes?: Record< string, unknown >;
};
}
import type {
WidgetName,
WidgetTypeMetadata,
WidgetType,
} from '../widget-types/types';

/**
* Runtime widget type consumed by the dashboard engine.
*
* Extends `WidgetTypeMetadata` (the authoring shape of `widget.json`) with
* runtime-only fields produced by the build pipeline — notably
* `renderModule`, which maps each widget to its discovered script-module
* entry point.
*
* Surfaces consume `WidgetType[]` via the `widgetTypes` prop; the
* dashboard never reads the widget-types store directly.
*/
export interface WidgetType extends WidgetTypeMetadata {
/**
* Script-module identifier resolved to a React component at render
* time by `ResolveWidgetModule`. Produced by the build pipeline from
* the conventional `render.*` / `widget.*` entry points; not declared
* in `widget.json`.
*/
renderModule: string;
}
export type { WidgetName, WidgetTypeMetadata, WidgetType };

export type GridTilePlacement = Omit< DashboardGridLayoutItem, 'key' >;
export type MasonryTilePlacement = Omit< DashboardLanesLayoutItem, 'key' >;
Expand Down
Loading
Loading