-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Try adding table view to data picker #72914
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: +2.81 kB (+0.12%) Total Size: 2.41 MB
ℹ️ View Unchanged
|
| { hasBulkActions && ( | ||
| <th | ||
| className="dataviews-view-table__checkbox-column" | ||
| scope="col" | ||
| > | ||
| <BulkSelectionCheckbox | ||
| selection={ selection } | ||
| onChangeSelection={ onChangeSelection } | ||
| data={ data } | ||
| actions={ actions } | ||
| getItemId={ getItemId } | ||
| /> | ||
| </th> | ||
| ) } |
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.
Looks like the checkbox is only shown for the multi-select picker.
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.
Fixed!
| } ) } | ||
| aria-busy={ isLoading } | ||
| aria-describedby={ tableNoticeId } | ||
| role={ 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.
One of my concerns over table is whether the aria semantics are right for a picker.
For grid and list I think it's easy to use the listbox role and it works well, but for table it's a bit harder to find the right fit because table already has its own role. Maybe there's a way to make it work. 🤔
Perhaps grid is a better fit than the default table roles, as I believe it's supposed to support aria-selected for rows - https://www.w3.org/WAI/ARIA/apg/patterns/grid/.
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 infinite scroll should always be a feed role regardless. whether that's compatible with the picker semantics I don't know 😅
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.
Is it worth updating the README as well?
The items in the view are rendered using the listbox and option aria roles.
Asking coz I don't know 😄
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.
Thinking about this, is there any good reason for it not to be a listbox? Really the only difference between picker-grid and picker-table is the visual layout, not anything functional. Wouldn't it make sense for them to use the same role?
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.
Perhaps it would work.
So I guess the idea would be to make the table the listbox and then the table rows become options, but then for table all the cells also have implicit aria roles, and that's the part that I'm not sure what to do:
![]()
I don't think cell roles work within option roles.
Similar situation with the header:

I think you end up losing the header relationship with the cells if it's switched to listbox.
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.
Do you mean it's unlikely to have multiple columns?
Yes sorry, columns, not rows!
OK let's try a listbox and removing table semantics. We can always revisit later if we find it doesn't work.
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, I was actually wondering if folks would use multiple columns to be able to sort by different columns easily. Would that still be possible in a listbox?
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 it would work, as the buttons that open the dropdowns within the table header cells would still be accessible/tabbable and have the button role, but some semantic info from the table might be lost. I haven't tested how well it works in trunk or #72997 yet though, so maybe it wouldn't be too different:

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 the dropdowns work. I'm not sure how clear it would be for a screen reader user that e.g. the "title" menu pop-up button hides sorting options for that column, but that's probably also the case in the table layout.
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 updated to use the listbox semantics, and added presentation roles to table elements that aren't interactive.
| key={ getItemId( item ) } | ||
| item={ item } | ||
| hasBulkActions={ hasBulkActions } | ||
| actions={ actions } |
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's no need for row actions for a picker.
| if ( ! hasPossibleBulkAction ) { | ||
| return; | ||
| } |
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.
Should probably be able to remove stuff like this, it's non-picker related.
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 wasn't sure about this because pickers can have actions, can't they? So they might have bulk actions too?
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.
They work a bit differently though. Regular dataviews doesn't allow row selection when there are no bulk actions (which is what this LOC prevents). It shows single item actions on the row itself so doesn't require an item to be checked.
The picker always allows row selection even when there are no bulk actions, single item actions appear in the footer so require an item to be checked.
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.
Oooh I see, thanks for explaining!
| ? selection.filter( | ||
| ( itemId ) => id !== itemId | ||
| ) | ||
| : [ id ] |
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 the regular dataview, clicking the row only selects one item at a time (same as what this code does). I think it could be revised when the picker support multi-selection so that clicking the row also multi-selects.
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.
Actually I don't think clicking the row to select is working at all here, need to fix that too 😅
|
Flaky tests detected in 376b725. 🔍 Workflow run URL: https://github.com/WordPress/gutenberg/actions/runs/19252780369
|
0c3541b to
01f08e7
Compare
|
Ok I've now addressed all the feedback from @talldan except the aria role issue that doesn't seem very straightforward. Perhaps that can be left for a follow-up? In any case I'm marking this ready for review. |
|
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. |
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.
Looking good! I think the main thing for me is the checkbox column headers mismatch, otherwise works as described 👍🏻
| > | ||
| <thead> | ||
| <tr className="dataviews-view-table__row"> | ||
| { hasPrimaryColumn && ( |
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 the checkbox column need a <th /> here? Without it the headers aren't matched with new columns:
Kapture.2025-11-05.at.12.13.49.mp4
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 I accidentally scrapped that in my earlier bulk select deletion spree 🤦
| } ) } | ||
| aria-busy={ isLoading } | ||
| aria-describedby={ tableNoticeId } | ||
| role={ 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.
Is it worth updating the README as well?
The items in the view are rendered using the listbox and option aria roles.
Asking coz I don't know 😄
|
|
||
| return ( | ||
| <tr | ||
| className={ clsx( 'dataviews-view-table__row', { |
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.
Is an aria-selected prop appropriate 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 depends on what role we end up assigning to the parent container following the discussion here 😄
| aria-posinset={ posinset } | ||
| role={ infiniteScrollEnabled ? 'article' : undefined } | ||
| onClick={ ( event ) => { | ||
| if ( event.target instanceof HTMLElement ) { |
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 this will be true most of the time.
What if it checks if the click came from the checkbox (or other interactive elements)?
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 well spotted I don't think we need this check at all!
| } | ||
| aria-posinset={ posinset } | ||
| role={ infiniteScrollEnabled ? 'article' : undefined } | ||
| onClick={ ( event ) => { |
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.
Do you reckon we should check for touch devices like the regular table does?
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 logic in the table layout handles multi-selecting with modifier keys (it only checks whether we're not on a touch device) whereas in the picker we can multi-select just by clicking on several items, no modifiers needed (as long as multi-select is enabled).
| colSpan={ | ||
| columns.length + | ||
| ( hasPrimaryColumn ? 1 : 0 ) | ||
| } |
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, but just wondering if the colSpan should include the checkbox column? E.g.,
colSpan={
columns.length +
( hasPrimaryColumn ? 1 : 0 ) +
1 // checkbox column
}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 yeah, maybe that's why it's looking weird 😂
| } | ||
| } } | ||
| > | ||
| <td className="dataviews-view-table__checkbox-column"> |
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.
In my testing, nothing breaks, but I noticed that the regular table stops event propagation to stop click events from bubbling up to the table row.
Worth doing here as well?
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 regular table does that because it can have inline buttons which do different actions, whereas here we want any click on any part of the row to select that row, so it should propagate.
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 gotcha. Thanks for the explainer.
| itemListLabel="Galactic Bodies" | ||
| defaultLayouts={ { | ||
| [ LAYOUT_PICKER_GRID ]: {}, | ||
| [ LAYOUT_PICKER_TABLE ]: {}, |
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 PR's testing really nicely for me so far! This is such a minor point, but I was wondering if it's worth defaulting just the table view in this story to show more of the fields by default? Just to get a slightly more real world sense of how this view would look, because otherwise these stories only show the title and media fields. I.e. defaulting it to look more like this:
Feel free to skip this idea, though, we can always keep tweaking the storybook stories for these in follow-ups.
34e5c2e to
4c6ee40
Compare
| </tr> | ||
| </thead> | ||
| { /* Render grouped data if groupByField is specified */ } | ||
| { hasData && groupField && dataByGroup ? ( |
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 grid uses the group role for grouping (the listbox role supports it), so maybe it can work the same here.
| <DataViewsSelectionCheckbox | ||
| item={ item } | ||
| selection={ selection } | ||
| onChangeSelection={ onChangeSelection } | ||
| getItemId={ getItemId } | ||
| titleField={ titleField } | ||
| disabled={ false } | ||
| /> |
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.
Testing with Safari + Voiceover, and it's interesting that when a checkbox receives focus all the content on the row is announced. Including some of the aria-labels:
Hopefully that's something that can be improved if we add keyboard navigation (so the rows are selectable instead of the checkboxes). I expect stuff on the row to still be announced, but maybe the "Click Item" will go away and the title won't be repeated.
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 we should also consider changing the styles compared to regular dataviews so that the checkboxes are always visible.
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.
if we add keyboard navigation (so the rows are selectable instead of the checkboxes)
I'm trying this now using Composite similarly to picker-grid, but for some reason it doesn't work - the whole list focuses instead of the first element, when tabbing into it.
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.
Feel free to push the changes and I'd be happy to help debugging.
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've pushed the changes, and I added table layout to the experimental media modal too, so it's easier to debug (the react dev tools don't seem to work very well in storybook)
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, I thought we'd want to preserve tab navigation for the table header elements, which is why I put it in the body. But if it only works on table then I guess that's what we have to do 😅
If your changes look good would you like to push them?
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 point, I didn't think of that.
Actually I retested with your changes, and it does work, the issue is that the styles need to be updated. The virtualFocus option uses aria-activedescendent, and ariakit adds data-active-item attributes to the elements, so that need to be styled. Here's the equivalent styles for the picker grid:
| &:focus-visible { | |
| // Only show one focus outline at a time. When focus is on a child element, | |
| // hide the grid's own focus outline. | |
| &[aria-activedescendant] { | |
| outline: none; | |
| } | |
| [data-active-item="true"] { | |
| outline: 2px solid var(--wp-admin-theme-color); | |
| } | |
| } |
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 it is still a bit broken for the grouped view, so that might need some fixes, or we decide not to support it.
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.
Should be fixed now, thanks for helping debug!
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 did some testing in voiceover/safari, and a problem is that because the composite isn't the element with the role="listbox" there were no announcements.
Probably the easiest thing is to move the role="listbox" prop to the Composite elements.
| descriptionField={ | ||
| showDescription ? descriptionField : undefined | ||
| } | ||
| isItemClickable={ () => false } |
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.
Actually, it's interesting that 'Click item' is still being output as an aria label for the cell, when we're passing false for isItemClickable. Looks like a bug in ColumnPrimary. I don't think it should have an aria-label at all.
In fact, I'm not even sure why an aria-label is needed anyway. Users know they can click the item since it's a button.
I think I might spin up a PR to remove the aria-labels.
edit: Actually, I think we do need it for the media when clickable, but I guess we can still remove 'Click item'.
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.
Made a PR here - #73034
| aria-label={ | ||
| titleField | ||
| ? titleField.getValue( { item } ) || __( '(no title)' ) | ||
| : 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 think it would be ok to remove the aria-label and instead rely on the contents.
| aria-setsize={ | ||
| infiniteScrollEnabled ? paginationInfo.totalItems : 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.
option roles support setsize and posinset, so this can be simplified to always set those props.
| onChangeSelection={ onChangeSelection } | ||
| multiselect={ isMultiselect } | ||
| posinset={ | ||
| isInfiniteScroll ? index + 1 : 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.
Same here as the previous comment. posinset can be passed for the listbox's option elements too.
| <Composite | ||
| virtualFocus | ||
| orientation="vertical" | ||
| render={ ( { children } ) => <>{ children }</> } |
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.
From what I can tell, this render callback won't work because the Composite ends up without an element to bind a ref to. It needs to render an element.
An option might be to make each <tbody a composite, though then the user will have to tab between each group. 🤔
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'll give that a try, it might actually make things easier to navigate between groups with lots of items.
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.
Looks like it works ok!
d084b9a to
ad93bdd
Compare
| const defaultLayouts = useMemo( | ||
| () => ( { | ||
| [ LAYOUT_PICKER_GRID ]: {}, | ||
| [ LAYOUT_PICKER_TABLE ]: {}, |
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.
Though this change is sort of extraneous to this PR I might leave it in because the table view can be handy for the media modal.
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.
Sounds good, gives us a good place to test it, too!
aaf3b17 to
7f5ed1c
Compare
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.
Just tested this again. In my view, after the e2e fix (hopefully it's straight forward?), I think it good to merge into the experiment and iterate later. Great work.
Feel free to let it simmer if you need to - just wanted to give a premptive ✅
Kapture.2025-11-11.at.11.39.10.mp4
Alt text is there too (after I added some), but unfortunately we can't sort by it and when we try it's stuck and there's no way to reset. Is that because dataviews is missing the field for it? I think so, it's being tracked in #72612
Anyway LGTM for now.
| .components-checkbox-control__input.components-checkbox-control__input { | ||
| // When the dataview is used as a picker, ensure the checkbox can't be clicked on. | ||
| // Only the row itself is clickable. | ||
| pointer-events: none; |
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.
specs/site-editor/page-list.spec.js E2E tests fail locally for me too because of this line.
I think because the checkbox has pointer-events: none, so it can't be clicked. The row is clickable, but the test clicks the checkbox.
Wrapping .dataviews-picker-wrapper around these styles appears to work, but not sure if that's a decent workaround for now 🤔
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. Well spotted, that needs to be specific to the picker layout.
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.
Fixed! I added a classname specific to the picker.
|
Just noting that I'd left a comment here that doesn't seem to have been addressed - #72914 (comment). I've made an issue here for it - DataViewsPicker Table layout: Active/Focused rows aren't announced by screenreaders |
Oh missed that, sorry! I'll put up a quick fix. Edit: it's actually not so straightforward; commented on the issue. |
What?
It'll be handy to have a table and maybe also a list view for when we implement #71128.
For now this just adds the table layout and it can be tested in storybook.
It essentially copies the table layout minus the inline clickable actions and any associated logic.
Testing Instructions
npm run storybook:dev