Skip to content

Commit 2c195be

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 2c195be

1 file changed

Lines changed: 234 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)