Skip to content

Commit b10c412

Browse files
committed
Allow dataviews empty state to be customised
1 parent 543a2d5 commit b10c412

File tree

12 files changed

+236
-6
lines changed

12 files changed

+236
-6
lines changed

packages/dataviews/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,28 @@ A list of numbers used to control the available item counts per page.
421421
422422
It's optional. Defaults to `[10, 20, 50, 100]`.
423423
424+
#### `empty`: `Object`
425+
426+
The empty object configures what should be shown when the `data` is empty.
427+
428+
Example:
429+
430+
```js
431+
const empty = {
432+
heading: 'No pages found',
433+
description: 'Your search did not match any pages.',
434+
illustration: 'https://example.com/illustration.svg',
435+
actions: <Button>Create a new page</Button>,
436+
};
437+
```
438+
439+
Properties:
440+
441+
- `heading`: short message describing the empty state. If this is the only property provided then the empty state will render as plain text without any additional styling.
442+
- `description`: a longer description of the empty state, providing more context or instructions.
443+
- `illustration`: either a URL to an image or a React component that renders an illustration. This should be purely illustrative as it does not render with any alt text.
444+
- `actions`: a `<Button>` (or list of `<Button>`s rendered in a fragment) which provide the user with next steps.
445+
424446
### Composition modes
425447
426448
The `DataViews` component supports two composition modes:
@@ -451,6 +473,7 @@ The following components are available directly under `DataViews`:
451473
- `DataViews.Pagination`
452474
- `DataViews.BulkActionToolbar`
453475
- `DataViews.ViewConfig`
476+
- `DataViews.Empty`
454477
455478
#### example
456479

packages/dataviews/src/components/dataviews-context/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { ComponentProps, ReactElement } from 'react';
77
* WordPress dependencies
88
*/
99
import { createContext, createRef } from '@wordpress/element';
10+
import { __ } from '@wordpress/i18n';
1011

1112
/**
1213
* Internal dependencies
@@ -17,6 +18,7 @@ import type {
1718
NormalizedField,
1819
SupportedLayouts,
1920
NormalizedFilter,
21+
EmptyViewProps,
2022
} from '../../types';
2123
import type { SetSelection } from '../../private-types';
2224
import { LAYOUT_TABLE } from '../../constants';
@@ -52,6 +54,11 @@ type DataViewsContextType< Item > = {
5254
isShowingFilter: boolean;
5355
setIsShowingFilter: ( value: boolean ) => void;
5456
perPageSizes?: [ number, number, number, number ];
57+
empty: EmptyViewProps;
58+
};
59+
60+
export const DEFAULT_VIEW_EMPTY = {
61+
heading: __( 'No results' ),
5562
};
5663

5764
const DataViewsContext = createContext< DataViewsContextType< any > >( {
@@ -76,6 +83,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( {
7683
filters: [],
7784
isShowingFilter: false,
7885
setIsShowingFilter: () => {},
86+
empty: DEFAULT_VIEW_EMPTY,
7987
} );
8088

8189
export default DataViewsContext;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import {
5+
__experimentalHStack as HStack,
6+
__experimentalVStack as VStack,
7+
} from '@wordpress/components';
8+
import { useContext } from '@wordpress/element';
9+
10+
/**
11+
* Internal dependencies
12+
*/
13+
import DataViewsContext from '../dataviews-context';
14+
import type { EmptyViewProps } from '../../types';
15+
16+
function DataViewsEmptyContent( {
17+
heading,
18+
description,
19+
illustration,
20+
actions,
21+
}: EmptyViewProps ) {
22+
if ( ! description && ! illustration && ! actions ) {
23+
return heading;
24+
}
25+
26+
const illustrationElement =
27+
typeof illustration === 'string' ? (
28+
<img
29+
className="dataviews-empty__illustration"
30+
src={ illustration }
31+
alt=""
32+
/>
33+
) : (
34+
illustration
35+
);
36+
37+
return (
38+
<VStack spacing={ 8 } alignment="center" className="dataviews-empty">
39+
{ illustration && illustrationElement }
40+
<VStack spacing={ 1 } alignment="center">
41+
<div className="dataviews-empty__heading">{ heading }</div>
42+
{ description && (
43+
<div className="dataviews-empty__description">
44+
{ description }
45+
</div>
46+
) }
47+
</VStack>
48+
{ actions && (
49+
<HStack spacing={ 2 } justify="center">
50+
{ actions }
51+
</HStack>
52+
) }
53+
</VStack>
54+
);
55+
}
56+
57+
export function DataViewsEmpty() {
58+
const { empty } = useContext( DataViewsContext );
59+
return <DataViewsEmptyContent { ...empty } />;
60+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
.dataviews-empty {
3+
max-width: $modal-width-medium;
4+
text-align: center;
5+
}
6+
7+
.dataviews-empty__illustration {
8+
width: 100%;
9+
}
10+
11+
.dataviews-empty__heading {
12+
color: $gray-900;
13+
font-size: $font-size-large;
14+
font-weight: $font-weight-medium;
15+
line-height: $font-line-height-medium;
16+
}
17+
18+
.dataviews-empty__description {
19+
color: $gray-700;
20+
font-size: $font-size-medium;
21+
line-height: $font-line-height-small;
22+
text-wrap: balance;
23+
}

packages/dataviews/src/components/dataviews/index.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { useMergeRefs, useResizeObserver } from '@wordpress/compose';
1313
/**
1414
* Internal dependencies
1515
*/
16-
import DataViewsContext from '../dataviews-context';
16+
import DataViewsContext, { DEFAULT_VIEW_EMPTY } from '../dataviews-context';
1717
import {
1818
default as DataViewsFilters,
1919
useFilters,
@@ -23,13 +23,20 @@ import DataViewsLayout from '../dataviews-layout';
2323
import DataViewsFooter from '../dataviews-footer';
2424
import DataViewsSearch from '../dataviews-search';
2525
import { BulkActionsFooter } from '../dataviews-bulk-actions';
26+
import { DataViewsEmpty } from '../dataviews-empty';
2627
import { DataViewsPagination } from '../dataviews-pagination';
2728
import DataViewsViewConfig, {
2829
DataviewsViewConfigDropdown,
2930
ViewTypeMenu,
3031
} from '../dataviews-view-config';
3132
import { normalizeFields } from '../../normalize-fields';
32-
import type { Action, Field, View, SupportedLayouts } from '../../types';
33+
import type {
34+
Action,
35+
Field,
36+
View,
37+
SupportedLayouts,
38+
EmptyViewProps,
39+
} from '../../types';
3340
import type { SelectionOrUpdater } from '../../private-types';
3441
type ItemWithId = { id: string };
3542

@@ -60,6 +67,7 @@ type DataViewsProps< Item > = {
6067
getItemLevel?: ( item: Item ) => number;
6168
children?: ReactNode;
6269
perPageSizes?: [ number, number, number, number ];
70+
empty?: EmptyViewProps;
6371
} & ( Item extends ItemWithId
6472
? { getItemId?: ( item: Item ) => string }
6573
: { getItemId: ( item: Item ) => string } );
@@ -134,6 +142,7 @@ function DataViews< Item >( {
134142
header,
135143
children,
136144
perPageSizes,
145+
empty,
137146
}: DataViewsProps< Item > ) {
138147
const containerRef = useRef< HTMLDivElement | null >( null );
139148
const [ containerWidth, setContainerWidth ] = useState( 0 );
@@ -198,6 +207,7 @@ function DataViews< Item >( {
198207
isShowingFilter,
199208
setIsShowingFilter,
200209
perPageSizes,
210+
empty: empty ?? DEFAULT_VIEW_EMPTY,
201211
} }
202212
>
203213
<div
@@ -226,6 +236,7 @@ const DataViewsSubComponents = DataViews as typeof DataViews & {
226236
Pagination: typeof DataViewsPagination;
227237
Search: typeof DataViewsSearch;
228238
ViewConfig: typeof DataviewsViewConfigDropdown;
239+
Empty: typeof DataViewsEmpty;
229240
};
230241

231242
DataViewsSubComponents.BulkActionToolbar = BulkActionsFooter;
@@ -236,5 +247,6 @@ DataViewsSubComponents.LayoutSwitcher = ViewTypeMenu;
236247
DataViewsSubComponents.Pagination = DataViewsPagination;
237248
DataViewsSubComponents.Search = DataViewsSearch;
238249
DataViewsSubComponents.ViewConfig = DataviewsViewConfigDropdown;
250+
DataViewsSubComponents.Empty = DataViewsEmpty;
239251

240252
export default DataViewsSubComponents;

packages/dataviews/src/components/dataviews/stories/index.story.tsx

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
__experimentalText as Text,
2121
__experimentalHStack as HStack,
2222
__experimentalVStack as VStack,
23+
Button,
2324
} from '@wordpress/components';
2425
import { __, _n } from '@wordpress/i18n';
2526

@@ -110,6 +111,52 @@ export const Empty = () => {
110111
);
111112
};
112113

114+
export const CustomEmpty = () => {
115+
const [ view, setView ] = useState< View >( {
116+
...DEFAULT_VIEW,
117+
fields: [ 'title', 'description', 'categories' ],
118+
} );
119+
120+
return (
121+
<DataViews
122+
getItemId={ ( item ) => item.id.toString() }
123+
paginationInfo={ { totalItems: 0, totalPages: 0 } }
124+
data={ [] }
125+
view={ view }
126+
fields={ fields }
127+
onChangeView={ setView }
128+
actions={ actions }
129+
defaultLayouts={ defaultLayouts }
130+
empty={ {
131+
heading: view.search ? 'No sites found' : 'No sites',
132+
description: view.search
133+
? `Your search for “${ view.search }” did not match any sites. Try searching by the site title or domain name.`
134+
: 'Get started by creating a new site.',
135+
illustration:
136+
'https://pd.w.org/2025/05/9376819135a616da1.38400720-768x432.jpeg',
137+
actions: (
138+
<>
139+
{ view.search && (
140+
<Button
141+
variant="secondary"
142+
onClick={ () => {
143+
setView( ( oldView ) => ( {
144+
...oldView,
145+
search: '',
146+
} ) );
147+
} }
148+
>
149+
Clear search
150+
</Button>
151+
) }
152+
<Button variant="primary">Add new site</Button>
153+
</>
154+
),
155+
} }
156+
/>
157+
);
158+
};
159+
113160
export const FieldsNoSortableNoHidable = () => {
114161
const [ view, setView ] = useState< View >( {
115162
...DEFAULT_VIEW,
@@ -211,7 +258,16 @@ function PlanetOverview( { planets }: { planets: SpaceObject[] } ) {
211258
</VStack>
212259
</Grid>
213260

214-
<DataViews.Layout className="free-composition-dataviews-layout" />
261+
{ planets.length > 0 ? (
262+
<DataViews.Layout className="free-composition-dataviews-layout" />
263+
) : (
264+
<HStack
265+
justify="space-around"
266+
className="free-composition-dataviews-empty"
267+
>
268+
<DataViews.Empty />
269+
</HStack>
270+
) }
215271
</>
216272
);
217273
}
@@ -261,6 +317,15 @@ export const FreeComposition = () => {
261317
table: {},
262318
grid: {},
263319
} }
320+
empty={ {
321+
heading: 'No plants',
322+
description: `Try a different search because “${ view.search }” returned no results.`,
323+
illustration:
324+
'https://pd.w.org/2022/02/2486205e41de79b53.44778169-768x363.jpg',
325+
actions: (
326+
<Button variant="secondary">Create new planet</Button>
327+
),
328+
} }
264329
>
265330
<PlanetOverview planets={ planets } />
266331
</DataViews>

packages/dataviews/src/components/dataviews/stories/style.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@
2323
.free-composition-dataviews-layout thead {
2424
inset-block-start: 67px;
2525
}
26+
27+
.free-composition-dataviews-empty > * {
28+
border: 1px solid #000;
29+
border-radius: 8px;
30+
padding: 24px;
31+
}

packages/dataviews/src/dataviews-layouts/grid/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import type {
3838
import type { SetSelection } from '../../private-types';
3939
import { ItemClickWrapper } from '../utils/item-click-wrapper';
4040
import { useUpdatedPreviewSizeOnViewportChange } from './preview-size-picker';
41+
import { DataViewsEmpty } from '../../components/dataviews-empty';
4142
const { Badge } = unlock( componentsPrivateApis );
4243

4344
interface GridItemProps< Item > {
@@ -425,7 +426,7 @@ function ViewGrid< Item >( {
425426
'dataviews-no-results': ! isLoading,
426427
} ) }
427428
>
428-
<p>{ isLoading ? <Spinner /> : __( 'No results' ) }</p>
429+
<p>{ isLoading ? <Spinner /> : <DataViewsEmpty /> }</p>
429430
</div>
430431
)
431432
}

packages/dataviews/src/dataviews-layouts/list/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
*/
44
import clsx from 'clsx';
55

6+
/**
7+
* Internal dependencies
8+
*/
9+
import { DataViewsEmpty } from '../../components/dataviews-empty';
10+
611
/**
712
* WordPress dependencies
813
*/
@@ -488,7 +493,7 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) {
488493
} ) }
489494
>
490495
{ ! hasData && (
491-
<p>{ isLoading ? <Spinner /> : __( 'No results' ) }</p>
496+
<p>{ isLoading ? <Spinner /> : <DataViewsEmpty /> }</p>
492497
) }
493498
</div>
494499
);

packages/dataviews/src/dataviews-layouts/table/index.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import type { SetSelection } from '../../private-types';
3939
import ColumnHeaderMenu from './column-header-menu';
4040
import ColumnPrimary from './column-primary';
4141
import { useIsHorizontalScrollEnd } from './use-is-horizontal-scroll-end';
42+
import { DataViewsEmpty } from '../../components/dataviews-empty';
4243

4344
interface TableColumnFieldProps< Item > {
4445
fields: NormalizedField< Item >[];
@@ -464,7 +465,7 @@ function ViewTable< Item >( {
464465
id={ tableNoticeId }
465466
>
466467
{ ! hasData && (
467-
<p>{ isLoading ? <Spinner /> : __( 'No results' ) }</p>
468+
<p>{ isLoading ? <Spinner /> : <DataViewsEmpty /> }</p>
468469
) }
469470
</div>
470471
</>

0 commit comments

Comments
 (0)