Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/dataviews/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
- DataViews: Fix incorrect documentation for `defaultLayouts` prop. [#71334](https://github.com/WordPress/gutenberg/pull/71334)
- DataViews: Fix mismatched padding on mobile viewports for grid layout [#71455](https://github.com/WordPress/gutenberg/pull/71455)

### Enhancements

- Add support for hiding the `title` in Grid layouts, with the actions menu rendered over the media preview. [#71369](https://github.com/WordPress/gutenberg/pull/71369)

## 7.0.0 (2025-08-20)

### Breaking changes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ export const fields: Field< SpaceObject >[] = [
label: 'Title',
id: 'title',
type: 'text',
enableHiding: false,
enableHiding: true,
enableGlobalSearch: true,
filterBy: {
operators: [ 'contains', 'notContains', 'startsWith' ],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ import './style.css';
const meta = {
title: 'DataViews/DataViews',
component: DataViews,
// Use fullscreen layout and a wrapper div with padding to resolve conflicts
// between Ariakit's Dialog (usePreventBodyScroll) and Storybook's body padding
// (sb-main-padding class). This ensures consistent layout in DataViews stories
// when clicking actions menus. Without this the padding on the body will jump.
parameters: {
layout: 'fullscreen',
},
decorators: [
( Story ) => (
<div style={ { padding: '1rem' } }>
<Story />
</div>
),
],
Comment on lines +49 to +62
Copy link
Contributor Author

Choose a reason for hiding this comment

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

While this PR doesn't add any additional stories anymore, I'm leaving this change in, as it helps make things more consistent when interacting with the actions popovers within any of the DataViews stories.

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, that's nice.

} as Meta< typeof DataViews >;

export default meta;
Expand Down
50 changes: 31 additions & 19 deletions packages/dataviews/src/dataviews-layouts/grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ function GridItem< Item >( {
showTitle && titleField?.render ? (
<titleField.render item={ item } field={ titleField } />
) : null;
const shouldRenderMedia = showMedia && renderedMediaField;

let mediaA11yProps;
let titleA11yProps;
Expand Down Expand Up @@ -154,7 +155,7 @@ function GridItem< Item >( {
}
aria-posinset={ posinset }
>
{ showMedia && renderedMediaField && (
{ shouldRenderMedia && (
<ItemClickWrapper
item={ item }
isItemClickable={ isItemClickable }
Expand All @@ -166,7 +167,7 @@ function GridItem< Item >( {
{ renderedMediaField }
</ItemClickWrapper>
) }
{ hasBulkActions && showMedia && renderedMediaField && (
{ hasBulkActions && shouldRenderMedia && (
<DataViewsSelectionCheckbox
item={ item }
selection={ selection }
Expand All @@ -176,24 +177,35 @@ function GridItem< Item >( {
disabled={ ! hasBulkAction }
/>
) }
<HStack
justify="space-between"
className="dataviews-view-grid__title-actions"
>
<ItemClickWrapper
item={ item }
isItemClickable={ isItemClickable }
onClickItem={ onClickItem }
renderItemLink={ renderItemLink }
className="dataviews-view-grid__title-field dataviews-title-field"
{ ...titleA11yProps }
>
{ renderedTitleField }
</ItemClickWrapper>
{ !! actions?.length && (
{ ! showTitle && shouldRenderMedia && !! actions?.length && (
<div className="dataviews-view-grid__media-actions">
<ItemActions item={ item } actions={ actions } isCompact />
) }
</HStack>
</div>
) }
{ showTitle && (
<HStack
justify="space-between"
className="dataviews-view-grid__title-actions"
>
<ItemClickWrapper
item={ item }
isItemClickable={ isItemClickable }
onClickItem={ onClickItem }
renderItemLink={ renderItemLink }
className="dataviews-view-grid__title-field dataviews-title-field"
{ ...titleA11yProps }
>
{ renderedTitleField }
</ItemClickWrapper>
{ !! actions?.length && (
<ItemActions
item={ item }
actions={ actions }
isCompact
/>
) }
</HStack>
) }
<VStack spacing={ 1 }>
{ showDescription && descriptionField?.render && (
<descriptionField.render
Expand Down
34 changes: 34 additions & 0 deletions packages/dataviews/src/dataviews-layouts/grid/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -139,19 +139,53 @@
top: -9999em;
left: $grid-unit-10;
z-index: z-index(".dataviews-view-grid__card .dataviews-selection-checkbox");
opacity: 0;

@media not (prefers-reduced-motion) {
transition: opacity 0.1s linear;
}

@media (hover: none) {
// Show checkboxes on devices that do not support hover.
opacity: 1;
top: $grid-unit-10;
}
}

.dataviews-view-grid__card:hover .dataviews-selection-checkbox,
.dataviews-view-grid__card:focus-within .dataviews-selection-checkbox,
.dataviews-view-grid__card.is-selected .dataviews-selection-checkbox {
opacity: 1;
top: $grid-unit-10;
}

.dataviews-view-grid__card .dataviews-view-grid__media-actions {
position: absolute;
top: $grid-unit-05;
opacity: 0;
right: $grid-unit-05;

.dataviews-all-actions-button {
background-color: $white;
}

@media not (prefers-reduced-motion) {
transition: opacity 0.1s linear;
}

@media (hover: none) {
// Show actions on devices that do not support hover.
opacity: 1;
top: $grid-unit-05;
}
}

.dataviews-view-grid__card:hover .dataviews-view-grid__media-actions,
.dataviews-view-grid__card:focus-within .dataviews-view-grid__media-actions,
.dataviews-view-grid__card .dataviews-view-grid__media-actions:has(.dataviews-all-actions-button[aria-expanded="true"]) {
opacity: 1;
}

.dataviews-view-grid__media--clickable {
cursor: pointer;
}
Expand Down
86 changes: 86 additions & 0 deletions packages/dataviews/src/test/dataviews.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,92 @@ describe( 'DataViews component', () => {
await user.keyboard( '{/Control}' );
} );

it( 'supports tabbing to selection and actions when title is visible', async () => {
render(
<DataViewWrapper
view={ {
...DEFAULT_VIEW,
type: 'grid',
fields: [],
mediaField: 'image',
titleField: 'title',
} }
isItemClickable={ () => true }
actions={ actions }
/>
);

// Double check that the title is being rendered.
expect( screen.getByText( data[ 0 ].title ) ).toBeInTheDocument();

const viewOptionsButton = screen.getByRole( 'button', {
name: 'View options',
} );

const user = userEvent.setup();

// Double click to open and then close view options. This is performed
// instead of a direct .focus() so that effects have time to complete.
await user.click( viewOptionsButton );
await user.click( viewOptionsButton );

await user.tab();

expect(
screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
).toHaveFocus();

await user.tab();

expect(
screen.getAllByRole( 'button', { name: 'Actions' } )[ 0 ]
).toHaveFocus();
} );

it( 'supports tabbing to selection and actions when title is not visible', async () => {
render(
<DataViewWrapper
view={ {
...DEFAULT_VIEW,
type: 'grid',
fields: [],
mediaField: 'image',
titleField: 'title',
showTitle: false,
} }
isItemClickable={ () => true }
actions={ actions }
/>
);

// Double check that the title is not being rendered.
expect(
screen.queryByText( data[ 0 ].title )
).not.toBeInTheDocument();

const viewOptionsButton = screen.getByRole( 'button', {
name: 'View options',
} );

const user = userEvent.setup();

// Double click to open and then close view options. This is performed
// instead of a direct .focus() so that effects have time to complete.
await user.click( viewOptionsButton );
await user.click( viewOptionsButton );
await user.tab();

expect(
screen.getByRole( 'checkbox', { name: data[ 0 ].title } )
).toHaveFocus();

await user.tab();

expect(
screen.getAllByRole( 'button', { name: 'Actions' } )[ 0 ]
).toHaveFocus();
} );

Copy link
Contributor

Choose a reason for hiding this comment

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

Not something to address in this PR but these feel like they should be e2e tests. Could we perhaps run e2e against storybook for dataviews UI testing?

it( 'accepts an invalid previewSize and the preview size picker falls back to another size', async () => {
render(
<DataViewWrapper
Expand Down
4 changes: 4 additions & 0 deletions packages/fields/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Enhancements

- Update the base `titleField` to enable hiding. [#71369](https://github.com/WordPress/gutenberg/pull/71369)

## 0.21.0 (2025-08-20)

## 0.20.0 (2025-08-07)
Expand Down
2 changes: 1 addition & 1 deletion packages/fields/src/fields/title/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const titleField: Field< CommonPost > = {
placeholder: __( 'No title' ),
getValue: ( { item } ) => getItemTitle( item ),
render: TitleView,
enableHiding: false,
enableHiding: true,
Copy link
Member

Choose a reason for hiding this comment

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

I understand we want to display actions in the media field when title is not present. But, do we also want the title field to be hidable in the existing views (site editor's pages)? Not anything against it, just double-checking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But, do we also want the title field to be hidable in the existing views

Yes, it'll be a key part of building out a new media library that's powered by data views, as we'll need to be able to hide the title for attachment post types in a grid view.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah, that sounds a good experience for the media library, but can this be addressed differently without affecting existing views? For example, with this view config for the media library:

const view = {
  type: 'grid',
  // titleField not provided
  mediaField: '...',
  fields: [ /**/ ]
};

That way, we wouldn't enable hiding the title in the site editor's page.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

but can this be addressed differently without affecting existing views?

No, I don't think so. For the media use case, we still want the title field to exist, as it's needed for accessibility (i.e. the label provided to the selection checkbox). So it's important that we can hide the title field, without excluding it altogether from the view.

Also, I think it should still be possible for folks to show the title field if they want to, with it defaulted to hidden.

To mitigate the issue for existing views, that's one of the reasons I was proposing making it so that if any locked fields are present, we can't hide the last one that's visible. So we give users more flexibility, but add a guardrail so that they can't get to an unusable state where no fields at all are visible.

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 way, we wouldn't enable hiding the title in the site editor's page.

Oh, one detail to mention here is that this PR doesn't affect the pages list in the site editor as that uses a different field (pageTitleField), rather than the generic titleField which is used as a fallback here:

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it makes sense for the base title to default to enable hiding. If fields can usually be hidden, disabling hiding is a decision which should probably be made at the consumer level. Both Pages and Templates have their own title fields which disable hiding, so nothing will change in the actual site editor.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, one detail to mention here is that this PR doesn't affect the pages list in the site editor as that uses a different field

Oh, nice. My worry was about affecting existing screens 👍

Little thing (default value is true):

Suggested change
enableHiding: true,

disabling hiding is a decision which should probably be made at the consumer level

I think of wordpress/fields as consumer-level code! It's just a different package, but I see it as an opinionated fields bundle for WordPress entities that we expect to use in our screens. While any screen can modify the field, in this bundle, it's best to have good defaults.

Thanks to you both for the context. I now understand this change better.

Copy link
Member

Choose a reason for hiding this comment

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

Actually, this field is used in the post list experiment ("Gutenberg > Experiments > enable for posts", then visit "Gutenberg > Posts"). Now that I've looked more into how fields are organized, would this work for you instead?

  • Leave the existing title field as it is.
  • When it comes to use the title field in the media library, revisit this. We could/may need to add a mediaTitleField that has specific field configs. I understand we don't need that now, but this change was more of a future thing, right?

This would prevent making the field hidable for any CPT entity, for example.

In any case, if you still think it's best to carry on with this change, I'd suggest at least adding a changelog entry in the fields package so consumers are aware of this.

Copy link
Contributor Author

@andrewserong andrewserong Sep 3, 2025

Choose a reason for hiding this comment

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

Thanks for the continued discussion here, I do think it's really good for us to think through these nuances!

My take on it is that the base title field should be the most flexible, with each particular instance that inherits from it being more opinionated. Ideally, I don't think we'd create a separate title field for media unless we have other requirements for how the title is displayed than what's included in the base title field. Or alternately, we might have postTitle in addition to mediaTitle, keeping titleField as the generic / base field.

For now, I'm pretty sold on enabling hiding on this field, so I've added a changelog entry for it. As we explore follow-ups to tackle the UX issues with hiding fields, I'm very happy to revisit this, though!

Once this PR lands, let's keep the discussion going on the linked issue: #71078

Little thing (default value is true):

In this case, due to the discussion surrounding hiding, I actually prefer keeping the value explicit here so that if someone looks to make a change to it, they'll see that it was added here intentionally.

enableGlobalSearch: true,
filterBy: false,
};
Expand Down
Loading