Skip to content

Commit 9730df2

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 is a minimal settings-page "payment methods" list (title, description, toggle) 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 2b91bb5 commit 9730df2

1 file changed

Lines changed: 240 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)