-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Dashboard: Add experimental WidgetDashboard rendering engine
#77770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 27 commits
5008343
1977c8c
93231cb
0823aa4
c10f94e
c7554cd
bd45200
4f79925
2b9b42e
ac57d43
bccaa15
458d9b9
5e524c3
ad867a4
14c3ac4
614f9bb
49d27c7
29389fe
1adef5f
6a668cb
aa4e868
1c06638
b145a60
31e4181
6664291
968caa4
97b0159
e501807
e61ffbe
0c5c9fc
e57573d
30167a1
722aaba
bd56878
d5509d3
984b63b
e913665
3ea30d3
0dcffb4
dbb4e50
9458708
906c988
013f00f
f1607bc
e957a01
f785320
3b852ee
bf19859
fb84b0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| { | ||
| "$schema": "https://json.schemastore.org/tsconfig.json", | ||
| "extends": "../../tsconfig.base.json", | ||
| "compilerOptions": { | ||
| "jsx": "react-jsx", | ||
| "rootDir": ".", | ||
| "noEmit": true, | ||
| "emitDeclarationOnly": false, | ||
| "composite": false, | ||
| "types": [ "jest", "style-imports" ] | ||
| }, | ||
| "include": [ "**/*.ts", "**/*.tsx" ], | ||
| "exclude": [ "build", "node_modules" ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| # `WidgetDashboard` | ||
|
|
||
| Stateless rendering engine for widget dashboards. Renders an editable grid of widget instances, with drag-to-reorder and resize when edit mode is on. | ||
| Widget types flow in as a prop and every layout mutation fires `onLayoutChange` with the fully updated array. | ||
| The engine owns no data of its own. | ||
|
|
||
| ## Usage | ||
|
|
||
| ```tsx | ||
| import { useState } from '@wordpress/element'; | ||
| import { WidgetDashboard } from './widget-dashboard'; | ||
|
|
||
| function Dashboard() { | ||
| const [ layout, setLayout ] = useState( defaultLayout ); | ||
|
|
||
| return ( | ||
| <WidgetDashboard | ||
| layout={ layout } | ||
| onLayoutChange={ setLayout } | ||
| widgetTypes={ widgetTypes } | ||
| /> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| `<WidgetDashboard>` renders `<WidgetDashboard.Widgets />` by default. Pass `children` to compose the surface — header, empty state, footer — around the grid: | ||
|
|
||
| ```tsx | ||
| <WidgetDashboard | ||
| layout={ layout } | ||
| onLayoutChange={ setLayout } | ||
| widgetTypes={ widgetTypes } | ||
| > | ||
| <WidgetDashboard.NoWidgetsState> | ||
| <p>{ __( 'No widgets yet.' ) }</p> | ||
| </WidgetDashboard.NoWidgetsState> | ||
| <WidgetDashboard.Widgets /> | ||
| </WidgetDashboard> | ||
| ``` | ||
|
|
||
| ## Properties | ||
|
|
||
| #### `layout`: `DashboardWidget[]` | ||
|
|
||
| Widget instances to render. Each instance carries a stable `uuid`, a `type` reference, optional `attributes`, and a `placement` describing its slot in the grid. | ||
|
|
||
| #### `onLayoutChange`: `( layout: DashboardWidget[] ) => void` | ||
|
|
||
| Called on every mutation — reorder, resize, or `setAttributes` from a widget render module. Receives the fully updated array; the consumer owns the storage. | ||
|
|
||
| #### `widgetTypes`: `WidgetType[]` | ||
|
|
||
| The widget types available to the dashboard. | ||
|
|
||
| #### `editMode`: `boolean` | ||
|
|
||
| When `true`, the grid enables drag and resize. Defaults to `false`. | ||
|
|
||
| #### `onEditChange`: `( next: boolean ) => void` | ||
|
|
||
| Optional. Called when edit mode toggles via a future `WidgetDashboard.Actions` compound. | ||
|
|
||
| #### `resolveWidgetModule`: `( moduleId: string ) => Promise< { default: ComponentType } >` | ||
|
|
||
| Optional. Maps a `WidgetType.renderModule` id to the React component that renders the widget. Defaults to a dynamic `import( /* webpackIgnore */ moduleId )`. Override for tests, Storybook, or remote-URL loading. | ||
|
|
||
| #### `gridSettings`: `WidgetGridSettings` | ||
|
|
||
| Optional. Configures the underlying grid. | ||
|
|
||
| #### `children`: `ReactNode` | ||
|
|
||
| Optional. Composition slot for arbitrary surface markup. When omitted, the engine renders `<WidgetDashboard.Widgets />` directly. | ||
|
|
||
| ## Compound components | ||
|
|
||
| #### `<WidgetDashboard.Widgets />` | ||
|
|
||
| Iterates `layout`, renders each entry through `<WidgetDashboard.Widget />`, and feeds the resulting tree into the underlying grid (`@wordpress/grid`). | ||
|
|
||
| #### `<WidgetDashboard.Widget />` | ||
|
|
||
| Per-instance wrapper. Provides widget identity to the render tree via context and hosts the widget's render module under a `Suspense` boundary and an error boundary. The instance is read from `layout`; consumers don't pass it manually. | ||
|
|
||
| #### `<WidgetDashboard.NoWidgetsState>` | ||
|
|
||
| Renders its children only when `layout` is empty. Pair it with `<WidgetDashboard.Widgets />` so the empty state shows up in place of the grid until widgets are added. | ||
|
|
||
| ## Authoring widgets | ||
|
|
||
| Widget render modules receive only what they need to render and edit: | ||
|
|
||
| ```ts | ||
| interface WidgetRenderProps< Item = unknown > { | ||
| attributes: Item; | ||
| setAttributes?: ( next: Partial< Item > ) => void; | ||
| } | ||
| ``` | ||
|
|
||
| `setAttributes` flows back through `onLayoutChange` on the dashboard. Removal, badges, and error chrome are not part of this contract — those belong to the surface. | ||
|
|
||
| ## Types | ||
|
|
||
| - `DashboardWidget` — a placement of a widget on the dashboard. Carries `uuid`, `type`, `attributes`, `placement`. | ||
| - `WidgetType` — runtime widget type. Extends the `widget.json` shape with `renderModule`. | ||
| - `WidgetRenderProps` — widget render contract. | ||
| - `ResolveWidgetModule` — module resolver signature. | ||
| - `WidgetGridSettings` — grid configuration. | ||
|
|
||
| `WidgetName`, `WidgetTypeMetadata`, and `WidgetType` are declared locally in `types.ts` until `@wordpress/widget-types` lands in trunk; at that point those three collapse into a re-export from the package. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { NoWidgetsState } from './no-widgets-state'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| .root { | ||
| padding-block-start: calc(var(--wpds-dimension-padding-3xl) * 3.5); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| /** | ||
| * External dependencies | ||
| */ | ||
| import type { ReactNode } from 'react'; | ||
|
|
||
| /** | ||
| * WordPress dependencies | ||
| */ | ||
| import { __ } from '@wordpress/i18n'; | ||
| import { widget } from '@wordpress/icons'; | ||
| // eslint-disable-next-line @wordpress/use-recommended-components -- EmptyState promotion tracked separately. | ||
| import { EmptyState, Stack } from '@wordpress/ui'; | ||
|
|
||
| /** | ||
| * Internal dependencies | ||
| */ | ||
| import { useDashboardInternalContext } from '../../context/dashboard-context'; | ||
| import styles from './no-widgets-state.module.css'; | ||
|
|
||
| export interface NoWidgetsStateProps { | ||
| children?: ReactNode; | ||
| } | ||
|
|
||
| function NoWidgetsStateImpl( { children }: NoWidgetsStateProps ) { | ||
| const { layout } = useDashboardInternalContext(); | ||
| if ( layout.length > 0 ) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <Stack justify="center" align="center" className={ styles.root }> | ||
| { children ?? ( | ||
| <EmptyState.Root> | ||
| <EmptyState.Icon icon={ widget } /> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually copy and icons might be something we want to allow configure later on when in a package, similarly to how DataViews allows passing empty state.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already supported. // Default placeholder (home icon + built-in copy)
<WidgetDashboard.NoWidgetsState />
// Full override
<WidgetDashboard.NoWidgetsState>
<EmptyState.Root>
<EmptyState.Icon icon={ myIcon } />
<EmptyState.Title>…</EmptyState.Title>
<EmptyState.Description>…</EmptyState.Description>
</EmptyState.Root>
</WidgetDashboard.NoWidgetsState>The
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| <EmptyState.Title> | ||
| { __( 'Your dashboard is empty' ) } | ||
| </EmptyState.Title> | ||
| <EmptyState.Description> | ||
| { __( | ||
| 'Add widgets to start customizing your dashboard.' | ||
| ) } | ||
| </EmptyState.Description> | ||
| </EmptyState.Root> | ||
| ) } | ||
| </Stack> | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Renders an empty-state placeholder when the dashboard's `layout` has no | ||
| * widgets. Pair with `WidgetDashboard.Widgets` inside `WidgetDashboard` so | ||
| * the placeholder shows up in place of the grid until widgets are added. | ||
| * Without children, falls back to a built-in placeholder; pass children to | ||
| * override. | ||
| */ | ||
| export const NoWidgetsState = NoWidgetsStateImpl; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { WidgetRender } from './widget-render'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| .loading, | ||
| .error { | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
|
retrofox marked this conversation as resolved.
Outdated
|
||
| height: 100%; | ||
| padding: var(--wpds-dimension-padding-md); | ||
| color: var(--wpds-color-fg-content-neutral-weak); | ||
| } | ||
|
|
||
| .error { | ||
| flex-direction: column; | ||
| text-align: center; | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.