Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b58dce5
Try adding optional infinite scroll to dataviews
tellthemachines Jul 29, 2025
a8042d9
try infinite scroll on all view types
tellthemachines Jul 30, 2025
5ef7214
make `infiniteScrollHandler` a property of `paginationInfo`
tellthemachines Jul 30, 2025
8b6f5f4
Add some more moons to storybook fixtures
tellthemachines Aug 4, 2025
61736d3
Add infinite scroll story
tellthemachines Aug 4, 2025
c8f47ab
update tests referring to data fixture
tellthemachines Aug 4, 2025
83c5644
Add aria roles for feed pattern
tellthemachines Aug 5, 2025
6269827
add feed aria roles to list and table layouts
tellthemachines Aug 5, 2025
afc4ac7
try toggle group control
tellthemachines Aug 5, 2025
c7b4fba
Changelog entry
tellthemachines Aug 5, 2025
c86bb1c
Fix storybook example
tellthemachines Aug 7, 2025
47dec0d
Fix styling issues with story
tellthemachines Aug 7, 2025
ac60b59
Hide pagination when infinite scroll is enabled
tellthemachines Aug 7, 2025
9a7d7aa
Remove memoed const
tellthemachines Aug 11, 2025
bf8010e
Make posinset optional prop and remove semantics from list layout
tellthemachines Aug 11, 2025
55302da
add aria attribs to div wrapper in list layout
tellthemachines Aug 11, 2025
fd50cfa
update changelog
tellthemachines Aug 11, 2025
be34116
Add loading more text
tellthemachines Aug 11, 2025
fd42a2c
Disable when data by group
tellthemachines Aug 11, 2025
b1ecc53
move infiniteScroll to base view
tellthemachines Aug 13, 2025
074c8ae
Try simple toggle and hiding per page values
tellthemachines Aug 13, 2025
3d54442
Center-align "loading more results"
tellthemachines Aug 13, 2025
1a9d90a
Change loading more text to spinner
tellthemachines Aug 13, 2025
07b49dc
Remove post list handler meant for testing
tellthemachines Aug 14, 2025
0cb6ad2
Update property name and clean up
tellthemachines Aug 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Features

- Add `enableMoving` option to the `table` layout to allow or disallow column moving left and right. [#71120](https://github.com/WordPress/gutenberg/pull/71120)
- Add infinite scroll support across all layout types (grid, list, table). Enable infinite scroll by providing an `infiniteScrollHandler` function in the `paginationInfo` prop and toggling the feature in the view configuration. ([#70955](https://github.com/WordPress/gutenberg/pull/70955))

### Enhancements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ type DataViewsContextType< Item > = {
setIsShowingFilter: ( value: boolean ) => void;
perPageSizes: number[];
empty?: ReactNode;
hasInfiniteScrollHandler: boolean;
};

const DataViewsContext = createContext< DataViewsContextType< any > >( {
Expand All @@ -82,6 +83,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( {
isShowingFilter: false,
setIsShowingFilter: () => {},
perPageSizes: [],
hasInfiniteScrollHandler: false,
} );

export default DataViewsContext;
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export function DataViewsPagination() {
paginationInfo: { totalItems = 0, totalPages },
} = useContext( DataViewsContext );

if ( ! totalItems || ! totalPages ) {
if ( ! totalItems || ! totalPages || view.infiniteScrollEnabled ) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { SORTING_DIRECTIONS, sortIcons, sortLabels } from '../../constants';
import { VIEW_LAYOUTS } from '../../dataviews-layouts';
import type { NormalizedField, View } from '../../types';
import DataViewsContext from '../dataviews-context';
import InfiniteScrollToggle from './infinite-scroll-toggle';
import { unlock } from '../../lock-unlock';

const { Menu } = unlock( componentsPrivateApis );
Expand Down Expand Up @@ -102,12 +103,11 @@ export function ViewTypeMenu() {
if ( 'layout' in viewWithoutLayout ) {
delete viewWithoutLayout.layout;
}
// @ts-expect-error
return onChangeView( {
...viewWithoutLayout,
type: e.target.value,
...defaultLayouts[ e.target.value ],
} );
} as View );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird that this error disappeared when I added infiniteScroll into layout in the several views, and reappeared when I moved it out. But I think this solution is nicer than the expect error comment.

}
warning( 'Invalid dataview' );
} }
Expand Down Expand Up @@ -215,7 +215,12 @@ function SortDirectionControl() {

function ItemsPerPageControl() {
const { view, perPageSizes, onChangeView } = useContext( DataViewsContext );
if ( perPageSizes.length < 2 || perPageSizes.length > 6 ) {
const { infiniteScrollEnabled } = view;
if (
perPageSizes.length < 2 ||
perPageSizes.length > 6 ||
infiniteScrollEnabled
) {
return null;
}

Expand Down Expand Up @@ -801,6 +806,7 @@ export function DataviewsViewConfigDropdown() {
{ !! activeLayout?.viewConfigOptions && (
<activeLayout.viewConfigOptions />
) }
<InfiniteScrollToggle />
<ItemsPerPageControl />
Comment on lines +809 to 810
Copy link
Contributor

@andrewserong andrewserong Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edit: Dan raised a good point that my suggestion below should not be followed!


Just thinking out loud, but the toggle control looks quite wide in its current implementation. Would it be worth conditionally wrapping these two controls in an HStack (like the sort and direction controls) when infinite scroll is present? (I imagine we'd need to add a context.hasInfiniteScrollHandler check here if that's the case?)

Here's how it's looking now:

image

But I was wondering if it'd be a bit more efficient use of space if we did something like this:

image

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO that makes Items Per Page look too confined. It can also have up to 6 items specified, so while it has 4 in that screenshot it's not a true reflection of the worst case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point! Ignore me then 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I'm thinking that looks a bit squished 😄
@jameskoster @jasmussen do you have opinions?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The appearance dropdown works decently as is, and yet it can benefit from a revisit given how it's organically grown. But part of that is also reflecting on: what should be here? IMO, and I feel this somewhat strongly, this dropdown should not hold a toggle for infinite scroll.

Infinite scroll has pros and cons. If implemented right, for extremely large amounts of data, such as your entire media library archive of tens of thousands, it can be an excellent way to provide you access to the entire silo of content in one view. When paired with other date-scrolling or letter-jumping tools, it can be one of the best ways to find exactly what you're looking for, in a way that maintains performance.

It's also got plenty of downsides. You can't do an in-page search for something that hasn't loaded yet, and there are some assistive tech challenges as well. I recognize those are largely mitigated by the presence of the pagination control, as well as the "Items per page" choice. In most cases, however, those controls are mutually exclusive: either you have infinite scroll, or you have n items per page.

I'm not saying infinite scroll can't be combined with an items per page choice, but it is unusual. And unlike posts per page which is commonly a per-view relevant choice, infinite scroll feels more like a set it and forget it choice; presumably you either want it everywhere, or nowhere. To frame it as an exercise, if we presumed all of WordPress used dataviews for list views, would you set infinite scroll for some, but not other screens?

For that reason, I'd put infinite scroll as a choice in a global preferences screen, and have it apply to all. Such as Settings > General.

I recognize that's non-trivial as far as progressing the dataviews component, so I could see it temporarily living in this dropdown, if we were to agree to eventually move it to a global place.

Although I feel somewhat strongly about this, it's also not a hill I'll do battle on, and if there's consensus otherwise that infinite scroll should be a per-view setting that's tied to or adjacent to posts per page, I would approach it as a toggle button not quite unlike how Figma does it for their auto-layout wrap button:

wrap

That is, keep the segmented control (or dropdown if need be) for posts per pages, and attach a toggle button to the right of it with a tooltip. This is in part to reduce the prominence of the button.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two problems I have with the above is that 1) it gives huge prominence to something I'm not sure should have prominence, and secondarily that the panel as a whole is already huge:

image

The design can probably work if it moves to a Settings > General page eventually. There's space to be verbose and contextualize there.

I don't want to block this work from moving forward, but the two pieces of followups to look into are just that: moving this to a settings page at some point, and overall looking at whether we can make this list appearance control more dense/compact/glancable. Those two tasks may in fact be the same, I have this vague feeling that most of the "set and forget" settings can live elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design can probably work if it moves to a Settings > General page eventually.

One thing we need to be aware of regarding infinite scroll as a global setting is that it has to be implemented on a per-screen basis. Dataviews only provides an event listener that the screen using it can attach a handler to. Even if we add infinite scroll handlers on all the core screens using dataviews, it's up to plugin developers to do it on their screens, so the global setting may only work for a subset of admin screens. Is that acceptable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK I've updated the PR to try out what Jay suggested above:

Screenshot 2025-08-13 at 3 16 27 pm

I've also centered the "loading more results" text:

Screenshot 2025-08-13 at 3 25 04 pm

(that text should only appear over a slow connection)

Does this look OK for now?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For me it’s an acceptable starting point from which to refine.

I agree the view options dialog is beginning to feel cumbersome in some circumstances, but I see that as a separate issue that we should approach systematically.

A global setting could makes sense, but ultimately this is a property of each individual dataview so the local option would likely need to remain regardless. I could see a global setting working similarly to ‘Default post category’.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree we can approach this separately and certainly systematically, and not necessarily urgently.

A global setting could makes sense, but ultimately this is a property of each individual dataview so the local option would likely need to remain regardless. I could see a global setting working similarly to ‘Default post category’.

Honestly this depends for me. In both Proton Drive, Dropbox, and Google Drive, there's not a choice to turn off or on infinite scroll, or even choose items per page. And it feels plenty, honestly. I recognize that doesn't map 1:1 with what DataViews is doing, because it's also handling column visibility properties. So I'm not suggesting we remove these. Perhaps it's just that the cog feels very prominent, and the dropdown it opens is substantial, so perhaps when we get to it changing the cog to an ellipsis or something ligther, and working on the density of the controls inside, is plenty.

</SettingsSection>
<SettingsSection title={ __( 'Properties' ) }>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* WordPress dependencies
*/
import { ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import DataViewsContext from '../dataviews-context';

export default function InfiniteScrollToggle() {
const context = useContext( DataViewsContext );
const { view, onChangeView } = context;
const infiniteScrollEnabled = view.infiniteScrollEnabled ?? false;

// Only render the toggle if an infinite scroll handler is available
if ( ! context.hasInfiniteScrollHandler ) {
return null;
}

return (
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Enable infinite scroll' ) }
help={ __(
'Automatically load more content as you scroll, instead of showing pagination links.'
) }
checked={ infiniteScrollEnabled }
onChange={ ( newValue ) => {
onChangeView( {
Copy link
Member

@oandregal oandregal Aug 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When switching it off, we also need to set the page we are in and make sure we only list the perPage number of items (not all of them)? Otherwise, it's like nothing happened:

Screen.Recording.2025-08-06.at.13.20.52.mov

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was an issue with the storybook implementation, not the toggle control 😅
It should be fixed in 8804ac9.

...view,
infiniteScrollEnabled: newValue,
} );
} }
/>
);
}
34 changes: 32 additions & 2 deletions packages/dataviews/src/components/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import type { ReactNode, ComponentProps, ReactElement } from 'react';
import { __experimentalHStack as HStack } from '@wordpress/components';
import {
useContext,
useEffect,
useMemo,
useRef,
useState,
useEffect,
} from '@wordpress/element';
import { useResizeObserver } from '@wordpress/compose';
import { useResizeObserver, throttle } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -51,6 +51,7 @@ type DataViewsProps< Item > = {
paginationInfo: {
totalItems: number;
totalPages: number;
infiniteScrollHandler?: () => void;
};
defaultLayouts: SupportedLayouts;
selection?: string[];
Expand Down Expand Up @@ -143,6 +144,7 @@ function DataViews< Item >( {
perPageSizes = [ 10, 20, 50, 100 ],
empty,
}: DataViewsProps< Item > ) {
const { infiniteScrollHandler } = paginationInfo;
const containerRef = useRef< HTMLDivElement | null >( null );
const [ containerWidth, setContainerWidth ] = useState( 0 );
const resizeObserverRef = useResizeObserver(
Expand Down Expand Up @@ -193,6 +195,33 @@ function DataViews< Item >( {
}
}, [ hasPrimaryOrLockedFilters, isShowingFilter ] );

// Attach scroll event listener for infinite scroll
useEffect( () => {
Copy link
Contributor

@talldan talldan Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useRefEffect could be an option here instead of useEffect, but tbh, it's a very minor difference. Only advantage is you wouldn't need to check containerRef.current is set.

if ( ! view.infiniteScrollEnabled || ! containerRef.current ) {
return;
}

const handleScroll = throttle( ( event: unknown ) => {
const target = ( event as Event ).target as HTMLElement;
const scrollTop = target.scrollTop;
const scrollHeight = target.scrollHeight;
const clientHeight = target.clientHeight;

// Check if user has scrolled near the bottom
if ( scrollTop + clientHeight >= scrollHeight - 100 ) {
infiniteScrollHandler?.();
}
}, 100 ); // Throttle to 100ms

const container = containerRef.current;
container.addEventListener( 'scroll', handleScroll );

return () => {
container.removeEventListener( 'scroll', handleScroll );
handleScroll.cancel(); // Cancel any pending throttled calls
};
}, [ infiniteScrollHandler, view.infiniteScrollEnabled ] );

return (
<DataViewsContext.Provider
value={ {
Expand Down Expand Up @@ -221,6 +250,7 @@ function DataViews< Item >( {
setIsShowingFilter,
perPageSizes,
empty,
hasInfiniteScrollHandler: !! infiniteScrollHandler,
} }
>
<div className="dataviews-wrapper" ref={ containerRef }>
Expand Down
105 changes: 98 additions & 7 deletions packages/dataviews/src/components/dataviews/stories/fixtures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,58 @@ export const data: SpaceObject[] = [
},
{
id: 4,
title: 'Ganymede',
description: 'Largest moon of Jupiter',
image: 'https://live.staticflickr.com/7816/33436473218_a836235935_k.jpg',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Jupiter', 'Moon' ],
satellites: 0,
date: '2022-01-04',
datetime: '2022-01-04T12:30:00Z',
email: '[email protected]',
},
{
id: 5,
title: 'Callisto',
description: 'Outermost Galilean moon of Jupiter',
image: 'https://live.staticflickr.com/804/27604150528_4512448a9c_c.jpg',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Jupiter', 'Moon' ],
satellites: 0,
date: '2021-01-05',
datetime: '2021-01-05T14:15:30Z',
email: '[email protected]',
},
{
id: 6,
title: 'Amalthea',
description: 'Small irregular moon of Jupiter',
image: 'https://upload.wikimedia.org/wikipedia/commons/6/62/Amalthea.gif',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Jupiter', 'Moon' ],
satellites: 0,
date: '2020-01-06',
datetime: '2020-01-06T10:45:15Z',
email: '[email protected]',
},
{
id: 7,
title: 'Himalia',
description: 'Largest irregular moon of Jupiter',
image: 'https://upload.wikimedia.org/wikipedia/commons/c/c2/Cassini-Huygens_Image_of_Himalia.png',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Jupiter', 'Moon' ],
satellites: 0,
date: '2019-01-07',
datetime: '2019-01-07T16:20:45Z',
email: '[email protected]',
},
{
id: 8,
title: 'Neptune',
description: 'Ice giant in the Solar system',
image: 'https://live.staticflickr.com/65535/29523683990_000ff4720c_z.jpg',
Expand All @@ -92,7 +144,46 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 5,
id: 9,
title: 'Triton',
description: 'Largest moon of Neptune',
image: 'https://live.staticflickr.com/65535/50728384241_02c5126c30_h.jpg',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Neptune', 'Moon' ],
satellites: 0,
date: '2021-02-01',
datetime: '2021-02-01T11:30:00Z',
email: '[email protected]',
},
{
id: 10,
title: 'Nereid',
description: 'Irregular moon of Neptune',
image: 'https://upload.wikimedia.org/wikipedia/commons/b/b0/Nereid-Voyager2.jpg',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Neptune', 'Moon' ],
satellites: 0,
date: '2020-02-02',
datetime: '2020-02-02T15:45:30Z',
email: '[email protected]',
},
{
id: 11,
title: 'Proteus',
description: 'Second-largest moon of Neptune',
image: 'https://live.staticflickr.com/65535/50727825808_bf427e007b_c.jpg',
type: 'Satellite',
isPlanet: false,
categories: [ 'Solar system', 'Satellite', 'Neptune', 'Moon' ],
satellites: 0,
date: '2019-02-03',
datetime: '2019-02-03T09:20:15Z',
email: '[email protected]',
},
{
id: 12,
title: 'Mercury',
description: 'Terrestrial planet in the Solar system',
image: 'https://live.staticflickr.com/813/40199101735_e5e92ffd11_z.jpg',
Expand All @@ -105,7 +196,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 6,
id: 13,
title: 'Venus',
description: 'La planète Vénus',
image: 'https://live.staticflickr.com/8025/7544560662_900e717727_z.jpg',
Expand All @@ -118,7 +209,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 7,
id: 14,
title: 'Earth',
description: 'Terrestrial planet in the Solar system',
image: 'https://live.staticflickr.com/3762/9460163562_964fe6af07_z.jpg',
Expand All @@ -131,7 +222,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 8,
id: 15,
title: 'Mars',
description: 'Terrestrial planet in the Solar system',
image: 'https://live.staticflickr.com/8151/7651156426_e047f4d219_z.jpg',
Expand All @@ -144,7 +235,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 9,
id: 16,
title: 'Jupiter',
description: 'Gas giant in the Solar system',
image: 'https://staging-jubilee.flickr.com/2853/9458010071_6e6fc41408_z.jpg',
Expand All @@ -157,7 +248,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 10,
id: 17,
title: 'Saturn',
description: 'Gas giant in the Solar system',
image: 'https://live.staticflickr.com/5524/9464658509_fc2d83dff5_z.jpg',
Expand All @@ -170,7 +261,7 @@ export const data: SpaceObject[] = [
email: '[email protected]',
},
{
id: 11,
id: 18,
title: 'Uranus',
description: 'Ice giant in the Solar system',
image: 'https://live.staticflickr.com/65535/5553350875_3072df91e2_c.jpg',
Expand Down
Loading
Loading