Skip to content

Commit 0d01570

Browse files
committed
docs(dataviews): add registerLayout Storybook story
A render-only demo of the new registerLayout API. Registers a `pocCardRows` layout that renders each item as a flex row — primary field on the left, secondary field(s) on the right — with no table headers, no borders, and no column-reorder or sort affordances. Fixture resembles a settings-page "payment methods" list (title, description, status, actions) because that is the real-world pattern that motivated the API: an existing migration to DataViews was visually-hiding thead with CSS, faking column widths via view.layout.styles, and overriding internal .dataviews-view-table class names. A registered layout replaces every one of those hacks with a component that just renders what it wants. Accessibility: each row is wired with aria-labelledby so assistive tech still gets row context even though the visual column header is gone. Layout authors that omit headers are responsible for this — the story doubles as a correct example. Refs: none
1 parent d8a7e4d commit 0d01570

1 file changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import type { Meta } from '@storybook/react';
5+
6+
/**
7+
* WordPress dependencies
8+
*/
9+
import { useState } from '@wordpress/element';
10+
11+
/**
12+
* Internal dependencies
13+
*/
14+
import DataViews from '../components/dataviews';
15+
import {
16+
registerLayout,
17+
getRegisteredLayout,
18+
} from '../dataviews-layouts/registry';
19+
import type { Field, View, ViewBaseProps } from '../types';
20+
21+
/**
22+
* Fixture that mirrors the shape of a settings-page "payment methods" row:
23+
* title + description on the left, a status badge and a toggle control on
24+
* the right. Chosen to resemble the real-world case that motivated this
25+
* POC (see docs/plans/2026-04-16-dataviews-register-layout-design.md).
26+
*/
27+
type PaymentMethod = {
28+
id: string;
29+
title: string;
30+
description: string;
31+
needsSetup: boolean;
32+
enabled: boolean;
33+
};
34+
35+
const methods: PaymentMethod[] = [
36+
{
37+
id: 'bacs',
38+
title: 'Bank transfer',
39+
description: 'Accept payments via direct bank transfer.',
40+
needsSetup: true,
41+
enabled: true,
42+
},
43+
{
44+
id: 'cheque',
45+
title: 'Check',
46+
description: 'Accept payments via mailed checks.',
47+
needsSetup: false,
48+
enabled: false,
49+
},
50+
{
51+
id: 'cod',
52+
title: 'Cash on delivery',
53+
description: 'Let customers pay when they receive the order.',
54+
needsSetup: false,
55+
enabled: true,
56+
},
57+
];
58+
59+
const methodFields: Field< PaymentMethod >[] = [
60+
{
61+
id: 'title',
62+
label: 'Payment method',
63+
type: 'text' as const,
64+
render: ( { item } ) => (
65+
<div>
66+
<div style={ { fontWeight: 500 } }>
67+
{ item.title }
68+
{ item.enabled && item.needsSetup ? (
69+
<span
70+
style={ {
71+
marginLeft: 8,
72+
padding: '2px 8px',
73+
borderRadius: 10,
74+
background: '#fff3cd',
75+
color: '#664d03',
76+
fontSize: 11,
77+
fontWeight: 500,
78+
} }
79+
>
80+
Action needed
81+
</span>
82+
) : null }
83+
</div>
84+
<div style={ { color: '#777', fontSize: 13 } }>
85+
{ item.description }
86+
</div>
87+
</div>
88+
),
89+
},
90+
{
91+
id: 'actions',
92+
label: 'Actions',
93+
type: 'text' as const,
94+
render: ( { item } ) => (
95+
<div style={ { display: 'flex', alignItems: 'center', gap: 12 } }>
96+
{ item.enabled && (
97+
<a href={ `#/manage/${ item.id }` }>
98+
{ item.needsSetup ? 'Add details' : 'Manage' }
99+
</a>
100+
) }
101+
<span
102+
style={ {
103+
display: 'inline-flex',
104+
alignItems: 'center',
105+
gap: 6,
106+
} }
107+
>
108+
<input
109+
type="checkbox"
110+
defaultChecked={ item.enabled }
111+
aria-label={ `Enable ${ item.title }` }
112+
/>
113+
<span style={ { fontSize: 12, color: '#555' } }>
114+
{ item.enabled ? 'On' : 'Off' }
115+
</span>
116+
</span>
117+
</div>
118+
),
119+
},
120+
];
121+
122+
/**
123+
* The plugin-defined layout component. It is a plain function component of
124+
* shape `( props: ViewBaseProps< Item > ) => ReactElement`. It renders each
125+
* item as a flex row with the primary field on the left and the secondary
126+
* field(s) on the right — no table headers, no borders between rows.
127+
*
128+
* Accessibility: each row is labelled by the primary cell's content via
129+
* `aria-labelledby`, so assistive tech still gets meaningful row context
130+
* even though the visual table header is gone.
131+
*/
132+
function PocCardRowsLayout< Item >( {
133+
data,
134+
fields,
135+
getItemId,
136+
view,
137+
}: ViewBaseProps< Item > ) {
138+
const visibleFieldIds = view.fields ?? fields.map( ( f ) => f.id );
139+
const primaryId = visibleFieldIds[ 0 ];
140+
const secondaryIds = visibleFieldIds.slice( 1 );
141+
142+
return (
143+
<ul
144+
style={ {
145+
listStyle: 'none',
146+
margin: 0,
147+
padding: 0,
148+
display: 'flex',
149+
flexDirection: 'column',
150+
gap: 16,
151+
} }
152+
>
153+
{ data.map( ( item ) => {
154+
const id = getItemId( item );
155+
const primary = fields.find( ( f ) => f.id === primaryId );
156+
const primaryLabelId = `poc-row-${ id }-primary`;
157+
return (
158+
<li
159+
key={ id }
160+
aria-labelledby={ primaryLabelId }
161+
style={ {
162+
display: 'flex',
163+
justifyContent: 'space-between',
164+
alignItems: 'center',
165+
gap: 16,
166+
} }
167+
>
168+
<div id={ primaryLabelId } style={ { flex: 1 } }>
169+
{ primary?.render
170+
? primary.render( { item, field: primary } )
171+
: null }
172+
</div>
173+
<div
174+
style={ {
175+
display: 'flex',
176+
alignItems: 'center',
177+
gap: 12,
178+
} }
179+
>
180+
{ secondaryIds.map( ( fieldId ) => {
181+
const f = fields.find(
182+
( fld ) => fld.id === fieldId
183+
);
184+
if ( ! f?.render ) {
185+
return null;
186+
}
187+
return (
188+
<div key={ fieldId }>
189+
{ f.render( { item, field: f } ) }
190+
</div>
191+
);
192+
} ) }
193+
</div>
194+
</li>
195+
);
196+
} ) }
197+
</ul>
198+
);
199+
}
200+
201+
// Register once at module load. Guarded against duplicate registration so
202+
// Storybook's HMR doesn't throw on second evaluation.
203+
if ( ! getRegisteredLayout( 'pocCardRows' ) ) {
204+
registerLayout( {
205+
type: 'pocCardRows',
206+
label: 'POC card rows',
207+
component: PocCardRowsLayout as unknown as Parameters<
208+
typeof registerLayout
209+
>[ 0 ][ 'component' ],
210+
} );
211+
}
212+
213+
const meta = {
214+
title: 'DataViews/Register Layout (POC)',
215+
component: DataViews,
216+
parameters: { layout: 'fullscreen' },
217+
decorators: [
218+
( Story: React.ComponentType ) => (
219+
<div style={ { maxWidth: 660, margin: '1rem auto' } }>
220+
<h2 style={ { margin: '0 0 8px' } }>Offline payment methods</h2>
221+
<p
222+
style={ {
223+
margin: '0 0 16px',
224+
color: '#555',
225+
fontSize: 14,
226+
} }
227+
>
228+
Custom layout registered via <code>registerLayout()</code>.
229+
No table headers, no pagination chrome, no column-reorder
230+
affordances — the layout component decides what to render.
231+
</p>
232+
<Story />
233+
</div>
234+
),
235+
],
236+
} as Meta< typeof DataViews >;
237+
238+
export default meta;
239+
240+
export const PocCardRows = () => {
241+
const [ view, setView ] = useState< View >( {
242+
type: 'pocCardRows',
243+
fields: [ 'title', 'actions' ],
244+
} as View );
245+
246+
return (
247+
<DataViews< PaymentMethod >
248+
data={ methods }
249+
fields={ methodFields }
250+
view={ view }
251+
onChangeView={ setView }
252+
getItemId={ ( item ) => item.id }
253+
paginationInfo={ {
254+
totalItems: methods.length,
255+
totalPages: 1,
256+
} }
257+
defaultLayouts={ {} }
258+
search={ false }
259+
>
260+
<DataViews.Layout />
261+
</DataViews>
262+
);
263+
};

0 commit comments

Comments
 (0)