Skip to content

Commit f5c8b94

Browse files
committed
docs(dataviews): add registerLayout POC design
Design document describing a proof-of-concept `registerLayout` API for the DataViews package. The current layouts array is hardcoded and closed, forcing plugins that want a different visual shape to fall back to CSS overrides against internal class names and prop-shape hacks. Documents the render-only scope, API shape, the files that need to change, a Storybook-based demo, unit tests, and validation against a real downstream migration that currently uses those fallbacks. Refs: none
1 parent e0a171a commit f5c8b94

1 file changed

Lines changed: 169 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# DataViews: `registerLayout` API (POC)
2+
3+
**Status:** Proof of concept
4+
**Package:** `@wordpress/dataviews`
5+
**Date:** 2026-04-16
6+
7+
## Context
8+
9+
Today the set of DataViews layouts (`table`, `grid`, `list`, `activity`, `pickerGrid`, `pickerTable`) is a closed, hardcoded array at
10+
[`packages/dataviews/src/components/dataviews-layouts/index.ts`](../../packages/dataviews/src/components/dataviews-layouts/index.ts). The package exposes no
11+
`register*` function, no filter hook, and no slot/fill for layouts. Plugins wanting to render data in a shape the built-ins don't
12+
support fall back to CSS overrides and prop-shape hacks against internal class names.
13+
14+
A real example of that fallback pattern is the ciab-admin "Replace custom offline payments UI with DataForm" change, which needed to:
15+
16+
- Hide `thead` with a screen-reader-only CSS trick so column headers wouldn't render.
17+
- Set `view.layout.styles.{title,actions}.width` to fake a "primary content fills / actions shrink-wrap" row shape.
18+
- Neuter the view-switcher, search, sort, column-reorder, and pagination via stub props and `search={ false }`.
19+
- Override `.dataviews-view-table td`, `.dataviews-view-table__row:first-child`, and `.components-card__body` in route-level SCSS.
20+
21+
Every one of those except the last is something the layout component itself should decide. A registration API lets plugins ship
22+
their own layout component and opt out of the built-ins' shape entirely, instead of papering over it with CSS.
23+
24+
## Goal
25+
26+
Enable a plugin to register a DataViews layout type such that `view.type === '<customType>'` routes to the plugin's component.
27+
28+
**Render-only POC.** The custom layout renders when selected. Nothing else integrates: no view-switcher entry, no view-config menu,
29+
no type validation, no persistence affordances. Those are deliberate follow-ups.
30+
31+
## Non-goals
32+
33+
- `unregisterLayout` / cleanup on plugin deactivation — not needed for a proof of concept.
34+
- View-switcher icon integration.
35+
- View-config menu integration (density picker, column visibility, etc.).
36+
- A `@wordpress/data` store for the registry. A plain module-level `Map` is sufficient; no subscriptions are required.
37+
- Dynamic TypeScript narrowing per custom layout type. We widen `View` with one permissive `ViewCustom` variant.
38+
39+
## Public API
40+
41+
A new module at `packages/dataviews/src/components/dataviews-layouts/registry.ts`:
42+
43+
```ts
44+
import type { ComponentType, ReactElement } from 'react';
45+
import type { ViewBaseProps } from '../types';
46+
47+
export interface LayoutDefinition< Item = any > {
48+
type: string;
49+
label: string;
50+
component: ComponentType< ViewBaseProps< Item > >;
51+
icon?: ReactElement;
52+
}
53+
54+
export function registerLayout( layout: LayoutDefinition ): void;
55+
export function getRegisteredLayout( type: string ): LayoutDefinition | undefined;
56+
export function getRegisteredLayouts(): LayoutDefinition[];
57+
```
58+
59+
Re-exported from `packages/dataviews/src/index.ts` alongside the existing `VIEW_LAYOUTS` export.
60+
61+
### Semantics
62+
63+
- `registerLayout` throws if `type` collides with a built-in (`table`, `grid`, `list`, `activity`, `pickerGrid`, `pickerTable`) or
64+
with a previously-registered type. This matches `registerBlockType`'s duplicate-registration behavior.
65+
- Registry state is module-level: a single `Map<string, LayoutDefinition>` shared across the process. Same lifetime model as
66+
`@wordpress/blocks`' block type registry.
67+
- An internal `__clearRegisteredLayouts()` is exported for test teardown only, not re-exported from the package index.
68+
69+
## Implementation
70+
71+
Four files change:
72+
73+
### 1. `packages/dataviews/src/components/dataviews-layouts/registry.ts` — new, ~40 lines
74+
75+
Implements `registerLayout`, `getRegisteredLayout`, `getRegisteredLayouts`, `__clearRegisteredLayouts`. Module-level `Map`.
76+
77+
### 2. `packages/dataviews/src/components/dataviews-layout/index.tsx` — lookup change
78+
79+
Current lookup (line 43):
80+
81+
```ts
82+
const ViewComponent = VIEW_LAYOUTS.find(
83+
( v ) => v.type === view.type && defaultLayouts[ v.type ]
84+
)?.component;
85+
```
86+
87+
Becomes:
88+
89+
```ts
90+
const ViewComponent =
91+
VIEW_LAYOUTS.find(
92+
( v ) => v.type === view.type && defaultLayouts[ v.type ]
93+
)?.component
94+
?? getRegisteredLayout( view.type )?.component;
95+
```
96+
97+
Built-ins keep their `defaultLayouts[ v.type ]` gate — changing that would alter existing behavior for consumers that rely on it.
98+
Registered layouts skip the gate: a plugin registers globally, so requiring every consumer to add the custom type to
99+
`defaultLayouts` would defeat the point.
100+
101+
### 3. `packages/dataviews/src/types/dataviews.ts` — widen `View`
102+
103+
Add one permissive variant:
104+
105+
```ts
106+
export interface ViewCustom extends ViewBase {
107+
type: string; // anything not matching a built-in
108+
layout?: Record< string, any >;
109+
}
110+
111+
export type View =
112+
| ViewList
113+
| ViewGrid
114+
| ViewTable
115+
| ViewPickerGrid
116+
| ViewPickerTable
117+
| ViewActivity
118+
| ViewCustom;
119+
```
120+
121+
Plugin code can typecheck against `ViewCustom` without casts. Existing typed call sites over built-ins are unaffected.
122+
123+
### 4. `packages/dataviews/src/index.ts` — exports
124+
125+
Add `registerLayout`, `getRegisteredLayout`, `getRegisteredLayouts`, and the `LayoutDefinition` type.
126+
127+
## Demo
128+
129+
### Storybook story
130+
131+
New file `packages/dataviews/src/dataviews/stories/register-layout-poc.story.tsx`. Registers a `pocCardRows` layout — a
132+
flex row per item, primary field on the left, secondary fields on the right, no column headers, no borders, styling owned by the
133+
layout's component. The fixture resembles the offline-payments-style use case that motivated the API so readers can see the
134+
1:1 replacement.
135+
136+
Accessible labels (`aria-labelledby` on each row) are baked into the layout component so the story doubles as a correct example —
137+
layouts that omit headers still need to announce column context to screen readers.
138+
139+
The story uses the existing Free Composition entry point (`<DataViews.Layout />`) to render layout-only output.
140+
141+
### Unit tests
142+
143+
New file `packages/dataviews/src/dataviews-layouts/test/registry.ts` with four cases:
144+
145+
1. `registerLayout` then `getRegisteredLayout` returns the definition.
146+
2. `getRegisteredLayouts` returns all registered definitions.
147+
3. `registerLayout` throws on built-in collision and on duplicate registration.
148+
4. `<DataViewsLayout>` with `view.type === 'pocCardRows'` renders the registered component (verifies the lookup wire-up).
149+
150+
Suite calls `__clearRegisteredLayouts()` in `afterEach` to isolate tests.
151+
152+
## Validation
153+
154+
Against the ciab-admin offline-payments migration, a `pocCardRows`-style registered layout removes:
155+
156+
- The `thead { clip: rect(0 0 0 0) … }` CSS hack.
157+
- The `view.layout.styles.{title,actions}.width` column-width hack.
158+
- `enableMoving: false` (column-reorder UI isn't rendered at all).
159+
- All `.dataviews-view-table *` targeting in route-level SCSS.
160+
161+
What still stays because it's outside this POC's scope: stub `onChangeView`, `paginationInfo`, `isLoading`; DataForm-as-card-chrome.
162+
163+
## Future work (not in this POC)
164+
165+
- View-switcher and view-config-menu integration for registered layouts.
166+
- `unregisterLayout`, with subscriber notification so live-mounted DataViews instances react.
167+
- Filter hook on `VIEW_LAYOUTS` for layered composition (multiple plugins adding layouts).
168+
- TypeScript variance so each registered layout can narrow `view.layout` to its own shape.
169+
- Documentation in the DataViews handbook.

0 commit comments

Comments
 (0)