diff --git a/packages/dataviews/CHANGELOG.md b/packages/dataviews/CHANGELOG.md
index b6b3891e33d9e5..7470ef368044cd 100644
--- a/packages/dataviews/CHANGELOG.md
+++ b/packages/dataviews/CHANGELOG.md
@@ -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
diff --git a/packages/dataviews/src/components/dataviews/stories/fixtures.tsx b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
index ba0aeee5661af0..4657ff23c60a15 100644
--- a/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
+++ b/packages/dataviews/src/components/dataviews/stories/fixtures.tsx
@@ -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' ],
diff --git a/packages/dataviews/src/components/dataviews/stories/index.story.tsx b/packages/dataviews/src/components/dataviews/stories/index.story.tsx
index 813211c66ca658..1023f9f13d2c9c 100644
--- a/packages/dataviews/src/components/dataviews/stories/index.story.tsx
+++ b/packages/dataviews/src/components/dataviews/stories/index.story.tsx
@@ -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 ) => (
+
+
+
+ ),
+ ],
} as Meta< typeof DataViews >;
export default meta;
diff --git a/packages/dataviews/src/dataviews-layouts/grid/index.tsx b/packages/dataviews/src/dataviews-layouts/grid/index.tsx
index b7d6adb04fc9f2..36284357ea47d2 100644
--- a/packages/dataviews/src/dataviews-layouts/grid/index.tsx
+++ b/packages/dataviews/src/dataviews-layouts/grid/index.tsx
@@ -108,6 +108,7 @@ function GridItem< Item >( {
showTitle && titleField?.render ? (
) : null;
+ const shouldRenderMedia = showMedia && renderedMediaField;
let mediaA11yProps;
let titleA11yProps;
@@ -154,7 +155,7 @@ function GridItem< Item >( {
}
aria-posinset={ posinset }
>
- { showMedia && renderedMediaField && (
+ { shouldRenderMedia && (
( {
{ renderedMediaField }
) }
- { hasBulkActions && showMedia && renderedMediaField && (
+ { hasBulkActions && shouldRenderMedia && (
( {
disabled={ ! hasBulkAction }
/>
) }
-
-
- { renderedTitleField }
-
- { !! actions?.length && (
+ { ! showTitle && shouldRenderMedia && !! actions?.length && (
+
- ) }
-
+
+ ) }
+ { showTitle && (
+
+
+ { renderedTitleField }
+
+ { !! actions?.length && (
+
+ ) }
+
+ ) }
{ showDescription && descriptionField?.render && (
{
await user.keyboard( '{/Control}' );
} );
+ it( 'supports tabbing to selection and actions when title is visible', async () => {
+ render(
+ 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(
+ 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();
+ } );
+
it( 'accepts an invalid previewSize and the preview size picker falls back to another size', async () => {
render(
= {
placeholder: __( 'No title' ),
getValue: ( { item } ) => getItemTitle( item ),
render: TitleView,
- enableHiding: false,
+ enableHiding: true,
enableGlobalSearch: true,
filterBy: false,
};