-
Notifications
You must be signed in to change notification settings - Fork 4.6k
DataViews: add support for infinite scroll in table, grid, and list layouts #70955
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
Size Change: +1.23 kB (+0.06%) Total Size: 1.92 MB
ℹ️ View Unchanged
|
|
No particular reason apart from it being a more common UI pattern in that type of view, but given it's an opt-in it should be fine to add it for all views. |
|
Thanks for the feedback Riad! I've updated to add infiniteScrollHandler inside paginationInfo and allow infinite scroll on all view types. There's still a few things that need to be added before the PR is ready; I'll continue working on it in the next few days. |
|
It'd be great to update the stories in the storybook so that it's testable there as well. It should hopefully be pretty easy to update the views:
Possibly we might need a bigger data set in the stories. I thnk Jupiter and Saturn have some more moons that could be added 😄 |
50af36d to
6e25cd6
Compare
How do I get those image URLs from |
|
Good question. I found this gallery which is creative commons - https://www.flickr.com/photos/lightsinthedark/albums/72157694956141104/ But I'm not sure how you get the links. edit: Actually, maybe not every image is creative commons, it seems only some, might be ok for our usage. |
I've added an infinite scroll story, but it's looking a bit messy because I had to wrap it in a |
490a0f6 to
9457c81
Compare
| isInfiniteScroll | ||
| ? 'feed' | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm really not sure if the semantics here are correct: when groupByField is enabled, this means there will be multiple feed role groups on the page. Should we just disable infinite scroll when grouping by field?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I suppose semantically it'd be the containing VStack that would get the "feed" role if there should just be the one?
Should we just disable infinite scroll when grouping by field?
From my perspective it would make sense to start out not supporting "group by" for now, and return to adding it further down the track if it's needed? Aside from the semantics of the feed role, it does feel like the group by case is a slightly different UX from scrolling through a single list 🤔
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message. To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
|
I had a look at our recommendations regarding the accessibility of the infinite scroll pattern. One thing to bear in mind is that the event handler that actually implements the infinite scroll by loading in more data, etc, is created by the consumer, so there's nothing that can be done on the dataviews side to safeguard a good navigation experience or memory management. The example implementation in edit-site post list is very simplified and doesn't deal with these matters, mostly because that view doesn't handle URL changes for pagination either, and adding proper navigation would require that. This is why I suggest we don't merge that change in this PR, and follow up with a proper implementation later if/when we want to add infinite scroll to that view. |
| value={ infiniteScrollEnabled ? 'enabled' : 'disabled' } | ||
| onChange={ ( value ) => { | ||
| const newValue = value === 'enabled'; | ||
| onChangeView( { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
oandregal
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure if this is an artifact of the storybook, but I've noticed that the footer is not visible until you've scrolled down all the pages. This is problematic as you can't access bulk selection (in grid) and bulk actions (in all layouts).
Screen.Recording.2025-08-06.at.13.25.54.mov
| fields={ fields } | ||
| onChangeView={ setView } | ||
| actions={ actions } | ||
| isLoading={ isLoadingMore } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wanted to try the "loading" experience. Layouts need to combine it with "has data":
- when there is no data, display a spinner instead like they do (e.g., table)
- when there is actually data but is still loading: what do we do? display a "loading item" at the end of the list?
Screen.Recording.2025-08-06.at.13.35.36.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, what do we do about the existing pagination? The current state is that we display the last page that has been fetched. And interacting with it doesn't do anything. Perhaps we hide it if infinite scroll is on?
Screen.Recording.2025-08-06.at.13.37.14.mov
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And interacting with it doesn't do anything. Perhaps we hide it if infinite scroll is on?
Interacting does something, but I don't think it's expected. The page jumps when you select pages after all have loaded, and the n items changes.
Kapture.2025-08-07.at.14.40.23.mp4
I wonder if infinite scroll needs some sort of custom footer, e.g., how many have been loaded and/or selected.
Otherwise it's working nicely for me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Additionally, what do we do about the existing pagination? The current state is that we display the last page that has been fetched. And interacting with it doesn't do anything. Perhaps we hide it if infinite scroll is on?
Yeah I think we just hide the pagination bit.
how many have been loaded and/or selected
This bit is working fine so we can leave it as is for now.
when there is no data, display a spinner instead like they do (e.g., table)
When would we have no data? If the server doesn't respond, shouldn't we display an error message in that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I think I get it. When the data is slow to load on the page, the spinner displays. That'll happen for the initial data whether infinite scroll is enabled or not.
If we're loading additional data with infinite scroll, that's where we might display a "loading items...". I can have a play with that and test it on the Pages screen. It might need some design input though.
|
@tellthemachines direction looks good to me, thanks for working on this. I've left some comments about edge cases. |
7134ceb to
98a19ef
Compare
|
Thanks for the feedback, folks!
Yeah it's a storybook issue 😅 it doesn't happen on the Pages screen. I'll see if it's fixable, the story should really reflect how the feature actually works. Edit: this is now fixed, and I managed to get rid of the double scrollbar too by adding in a hacky |
a5f2cd1 to
b8e41d7
Compare
talldan
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's testing well for me so far, other reviewers already noted the same things that I would mention.
Code is looking pretty tidy. I left a few very minor review comments.
| const isInfiniteScroll = useMemo( () => { | ||
| if ( ! infiniteScrollHandler ) { | ||
| return false; | ||
| } | ||
| return !! view.layout?.infiniteScroll; | ||
| }, [ view, infiniteScrollHandler ] ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you could remove the useMemo here, and instead have the boolean calculated inline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can if we assume that view.layout?.infiniteScroll will only exist if the infiniteScrollHandler also exists. Which it should! I might have been overly cautious here.
| }, [ view, infiniteScrollHandler ] ); | ||
|
|
||
| // Attach scroll event listener for infinite scroll | ||
| useEffect( () => { |
There was a problem hiding this comment.
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.
| config={ { | ||
| sizes: size, | ||
| } } | ||
| posinset={ index + 1 } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if the value should only be passed value when isInfiniteScroll is true, because when regular pagination is active it'll be providing the incorrect value (e.g. on the second page it'll start from 1 again).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm if you're planning to use posinset when infinite scroll is disabled, then yes that would make sense to do. Otherwise in this PR posinset is only rendered when infinite scroll is on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Defo a nitpick. To me, invalid data is passed to GridItem, and then we're trusting that the component does the right thing with that data. It's not an issue with the current code, but code changes, and some future person might miss the subtlety.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've found a bit of a problem with the aria attributes on the list layout: they don't play well at all with the Composite component we're using. I've removed them from list for now, though I haven't disabled infinite scroll on it.
Composit is built on ariakit and the error occurs on that side. Basically any posinset value passed to Composite.Row makes it explode 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😬 What's the error?
Might be worth checking the issues for ariakit to see if anyone has had the same - https://github.com/ariakit/ariakit/issues. I searched for posinset and saw nothing though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if you tried it, but you could try setting the prop on the <div /> that the Composite.Row renders.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ReferenceError: Cannot access 'rowId' before initialization which is coming from the ariakit component. I haven't really looked into it.
Adding it to the div works 🎉 I also had to add aria-setsize to the div otherwise it ends up attached to the button inside.
I also had to update the CSS selectors directly targeting [role="row"] to target [role="article"] too.
| ); | ||
| } | ||
| } } | ||
| role={ isInfiniteScroll ? 'article' : undefined } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting. I think I'll need a different role for the data picker changes. I'm not sure whether article supports selection. Will need to figure out if infinite scroll can work for a picker or if that's an invalid combination.
I'll also need posinset so it's useful to establish it here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's good question. Aside from whether the roles will work or not, personally I think I'd prefer to infinite scroll through a large picker than to navigate pagination, but it's not a great solution for accessibility so it shouldn't be the default mode. Does it makes sense to have view config options in a picker scenario?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does it makes sense to have view config options in a picker scenario?
Hard to say, possibly a smaller subset of options. But maybe it's not worth differentiating in that case and instead providing all the options.
| <InfiniteScrollToggle /> | ||
| <ItemsPerPageControl /> |
There was a problem hiding this comment.
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:
But I was wondering if it'd be a bit more efficient use of space if we did something like this:
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
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:
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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’.
There was a problem hiding this comment.
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.
| isInfiniteScroll | ||
| ? 'feed' | ||
| : undefined | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question. I suppose semantically it'd be the containing VStack that would get the "feed" role if there should just be the one?
Should we just disable infinite scroll when grouping by field?
From my perspective it would make sense to start out not supporting "group by" for now, and return to adding it further down the track if it's needed? Aside from the semantics of the feed role, it does feel like the group by case is a slightly different UX from scrolling through a single list 🤔
e14cc29 to
fd42a2c
Compare
| type: e.target.value, | ||
| ...defaultLayouts[ e.target.value ], | ||
| } ); | ||
| } as View ); |
There was a problem hiding this comment.
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.
Never a bother. To specifically answer the question, use a spinner instead of the text, which there's every risk you won't be able to read in time. As far as the knobs to toggle between items per page and infinite scroll, I've registered my concerns as far as the prominence this receives. As also noted, it's not blocking, if we agree to circle back to this at a later time, for example when we'll be able to move the infinite scroll toggle to a global settings page. |
|
@jasmussen thanks, I was able to read it in time 😄 I've now updated to use a spinner on all layout types:
|
|
🚀 |
|
Ok peeps this is now ready for a final code review. I've removed the post list handler which I'd added just for testing purposes, but you can still test it in storybook: It might be good to look into building a generic infinite scroll handler hook that can be used by several screens, but that can be done as a follow-up at some point. |
|
I haven't had a chance to give this a proper code review, but functionally this is testing great for me! |
ramonjd
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tested in storybook (the description still says it can be tested in Pages?)
Works as described for me, and nothing stands out in the code.
If the design folks are happy with it, then LGTM too for what it's worth, and it can be iterated upon before adding it to the post list as well.
Just left a suggestion re: the boolean var name.
Good show!
packages/dataviews/src/types.ts
Outdated
| /** | ||
| * Whether infinite scroll is enabled. | ||
| */ | ||
| infiniteScroll?: boolean; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: infiniteScroll reads more like a noun (the feature itself) rather than a boolean state. I'm wondering if it'd aid readability to have a name that indicates a true/false condition?
Then assignments like const isInfiniteScroll = view.infiniteScroll; above would be semantically redundant.
E.g.,
infiniteScrollEnabled
hasInfiniteScroll
enableInfiniteScroll
infiniteScrollActive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
infiniteScrollEnabled is probably the most accurate because that's what the setting checks for.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, good discussion. This is the tiniest of tiny nits, but I see in this PR, there's usage of infiniteScrollEnabled and isInfiniteScrollEnabled. Would it be good for consistency to double-check that we're naming them the same in each place? I have a mild preference for prefixing with is for booleans, but don't feel too strongly about it.
| if ( ! totalItems || ! totalPages ) { | ||
| const isInfiniteScrollEnabled = view.infiniteScroll ?? false; | ||
|
|
||
| if ( ! totalItems || ! totalPages || isInfiniteScrollEnabled ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Idea for a follow up: what do folks think about a go to top navigation? If you've got 100s of images, it might be helpful to go back to the top. I don't know, maybe it's just me 😄
Another "me" thing is needing an idea of what "infinite" means, e.g., how many items are there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah we could have a play with that! I'm not sure how it might work if the handler implementation doesn't just straight up load all items but instead loads next and unloads previous (which it might do for performance reasons, especially when catering to mobile devices). But def something to explore.
andrewserong
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is looking great to me, too. I really like the API of infiniteScrollEnabled in the view, and infiniteScrollHandler in the pagination info — it feels like a good starting point to me and will allow consumers a lot of flexibility to explore a variety of different implementations.
LGTM! 🚀
|
Thanks for the reviews everyone! 🎉 |






What?
It's likely this will be needed as DataViews is adopted in more wp-admin screens.
This PR adds an optional
infiniteScrollHandlerto the DataViewspaginationobject. If this handler exists, an infinite scroll toggle is enabled in the view config.If both
infiniteScrollHandlerexists and infinite scroll is toggled on in the view config, a scroll event handler is attached to the dataviews wrapper.The PR also updates storybook with an infinite scroll story, though it currently has a double scrollbar as I had to add a fixed height container around it to trigger the scroll event. This won't happen in regular usage, at least provided the dataviews container isn't directly inside an iframe 😅
To try this out, this PR also changes the PostList to pass an
infiniteScrollHandlerinto DataViews, so its functionality can be tested on the Pages screen (provided there are enough pages to trigger pagination).The PostList change doesn't need to be done in this PR, I just added it so it's easier to test.
Testing Instructions
npm run storybook:devand check the infinite scroll story.infinite-scrolll.mp4
scroll-story.mp4