Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 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
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ type DataViewsContextType< Item > = {
isItemClickable: ( item: Item ) => boolean;
containerWidth: number;
containerRef: React.MutableRefObject< HTMLDivElement | null >;
resizeObserverRef:
| ( ( element?: HTMLDivElement | null ) => void )
| React.RefObject< HTMLDivElement >;
defaultLayouts: SupportedLayouts;
filters: NormalizedFilter[];
isShowingFilter: boolean;
Expand All @@ -72,6 +75,7 @@ const DataViewsContext = createContext< DataViewsContextType< any > >( {
renderItemLink: undefined,
containerWidth: 0,
containerRef: createRef(),
resizeObserverRef: () => {},
defaultLayouts: { list: {}, grid: {}, table: {} },
filters: [],
isShowingFilter: false,
Expand Down
8 changes: 3 additions & 5 deletions packages/dataviews/src/components/dataviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import type { ReactNode, ComponentProps, ReactElement } from 'react';
*/
import { __experimentalHStack as HStack } from '@wordpress/components';
import { useContext, useMemo, useRef, useState } from '@wordpress/element';
import { useMergeRefs, useResizeObserver } from '@wordpress/compose';
import { useResizeObserver } from '@wordpress/compose';

/**
* Internal dependencies
Expand Down Expand Up @@ -193,17 +193,15 @@ function DataViews< Item >( {
renderItemLink,
containerWidth,
containerRef,
resizeObserverRef,
defaultLayouts,
filters,
isShowingFilter,
setIsShowingFilter,
perPageSizes,
} }
>
<div
className="dataviews-wrapper"
ref={ useMergeRefs( [ containerRef, resizeObserverRef ] ) }
>
<div className="dataviews-wrapper" ref={ containerRef }>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I believe that containerRef needs to be on this wrapper because its use is related to the scrolling behaviour of the wrapper.

The fact that this is the scrollable container for the layout is what makes it unsuitable to use with the resizeObserver, because the presence of a scrollbar will throw off the size of that actual layout. This is why I'm moving resizeObserverRef to context so it can be used directly in the grid layout.

{ children ?? (
<DefaultUI
header={ header }
Expand Down
59 changes: 37 additions & 22 deletions packages/dataviews/src/dataviews-layouts/grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import type { ComponentProps, ReactElement } from 'react';
* WordPress dependencies
*/
import {
__experimentalGrid as Grid,
__experimentalHStack as HStack,
__experimentalVStack as VStack,
Spinner,
Expand All @@ -19,13 +18,15 @@ import {
import { __, sprintf } from '@wordpress/i18n';
import { useInstanceId } from '@wordpress/compose';
import { isAppleOS } from '@wordpress/keycodes';
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import { unlock } from '../../lock-unlock';
import ItemActions from '../../components/dataviews-item-actions';
import DataViewsSelectionCheckbox from '../../components/dataviews-selection-checkbox';
import DataViewsContext from '../../components/dataviews-context';
import {
useHasAPossibleBulkAction,
useSomeItemHasAPossibleBulkAction,
Expand All @@ -38,7 +39,6 @@ import type {
} from '../../types';
import type { SetSelection } from '../../private-types';
import { ItemClickWrapper } from '../utils/item-click-wrapper';
import { useUpdatedPreviewSizeOnViewportChange } from './preview-size-picker';
const { Badge } = unlock( componentsPrivateApis );

interface GridItemProps< Item > {
Expand All @@ -61,6 +61,9 @@ interface GridItemProps< Item > {
regularFields: NormalizedField< Item >[];
badgeFields: NormalizedField< Item >[];
hasBulkActions: boolean;
config: {
size: string;
};
}

function GridItem< Item >( {
Expand All @@ -79,14 +82,19 @@ function GridItem< Item >( {
regularFields,
badgeFields,
hasBulkActions,
config,
}: GridItemProps< Item > ) {
const { showTitle = true, showMedia = true, showDescription = true } = view;
const hasBulkAction = useHasAPossibleBulkAction( actions, item );
const id = getItemId( item );
const instanceId = useInstanceId( GridItem );
const isSelected = selection.includes( id );
const renderedMediaField = mediaField?.render ? (
<mediaField.render item={ item } field={ mediaField } />
<mediaField.render
item={ item }
field={ mediaField }
config={ config }
/>
) : null;
const renderedTitleField =
showTitle && titleField?.render ? (
Expand Down Expand Up @@ -256,6 +264,7 @@ function ViewGrid< Item >( {
view,
className,
}: ViewGridProps< Item > ) {
const { resizeObserverRef } = useContext( DataViewsContext );
const titleField = fields.find(
( field ) => field.id === view?.titleField
);
Expand Down Expand Up @@ -286,14 +295,10 @@ function ViewGrid< Item >( {
{ regularFields: [], badgeFields: [] }
);
const hasData = !! data?.length;
const updatedPreviewSize = useUpdatedPreviewSizeOnViewportChange();
const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data );
const usedPreviewSize = updatedPreviewSize || view.layout?.previewSize;
const gridStyle = usedPreviewSize
? {
gridTemplateColumns: `repeat(${ usedPreviewSize }, minmax(0, 1fr))`,
}
: {};
const usedPreviewSize = view.layout?.previewSize;
// This is the maximum width that an image can achieve in the grid.
const size = '900px';
Copy link
Member

Choose a reason for hiding this comment

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

As a gift to our future selves, would you expand why this is the maximum width an image can achieve in the grid?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure!


const groupField = view.groupByField
? fields.find( ( f ) => f.id === view.groupByField )
Expand Down Expand Up @@ -328,16 +333,18 @@ function ViewGrid< Item >( {
groupName
) }
</h3>
<Grid
gap={ 8 }
columns={ 2 }
alignment="top"
<div
className={ clsx(
'dataviews-view-grid',
className
) }
style={ gridStyle }
style={ {
gridTemplateColumns:
usedPreviewSize &&
`repeat(auto-fill, minmax(${ usedPreviewSize }px, 1fr))`,
} }
aria-busy={ isLoading }
ref={ resizeObserverRef }
>
{ groupItems.map( ( item ) => {
return (
Expand Down Expand Up @@ -370,10 +377,13 @@ function ViewGrid< Item >( {
hasBulkActions={
hasBulkActions
}
config={ {
size,
} }
/>
);
} ) }
</Grid>
</div>
</VStack>
)
) }
Expand All @@ -384,13 +394,15 @@ function ViewGrid< Item >( {
{
// Render a single grid with all data.
hasData && ! dataByGroup && (
<Grid
gap={ 8 }
columns={ 2 }
alignment="top"
<div
className={ clsx( 'dataviews-view-grid', className ) }
style={ gridStyle }
style={ {
gridTemplateColumns:
usedPreviewSize &&
`repeat(auto-fill, minmax(${ usedPreviewSize }px, 1fr))`,
} }
aria-busy={ isLoading }
ref={ resizeObserverRef }
>
{ data.map( ( item ) => {
return (
Expand All @@ -411,10 +423,13 @@ function ViewGrid< Item >( {
regularFields={ regularFields }
badgeFields={ badgeFields }
hasBulkActions={ hasBulkActions }
config={ {
size,
} }
/>
);
} ) }
</Grid>
</div>
)
}
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,108 +3,83 @@
*/
import { RangeControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import { useMemo, useContext } from '@wordpress/element';
import { useContext } from '@wordpress/element';

/**
* Internal dependencies
*/
import DataViewsContext from '../../components/dataviews-context';
import type { ViewGrid } from '../../types';

const viewportBreaks: {
[ key: string ]: { min: number; max: number; default: number };
} = {
xhuge: { min: 3, max: 6, default: 5 },
huge: { min: 2, max: 4, default: 4 },
xlarge: { min: 2, max: 3, default: 3 },
large: { min: 1, max: 2, default: 2 },
mobile: { min: 1, max: 2, default: 2 },
};

/**
* Breakpoints were adjusted from media queries breakpoints to account for
* the sidebar width. This was done to match the existing styles we had.
*/
const BREAKPOINTS = {
xhuge: 1520,
huge: 1140,
xlarge: 780,
large: 480,
mobile: 0,
};

function useViewPortBreakpoint() {
const containerWidth = useContext( DataViewsContext ).containerWidth;
for ( const [ key, value ] of Object.entries( BREAKPOINTS ) ) {
if ( containerWidth >= value ) {
return key;
}
}
return 'mobile';
}

export function useUpdatedPreviewSizeOnViewportChange() {
const view = useContext( DataViewsContext ).view as ViewGrid;
const viewport = useViewPortBreakpoint();
return useMemo( () => {
const previewSize = view.layout?.previewSize;
let newPreviewSize;
if ( ! previewSize ) {
return;
}
const breakValues = viewportBreaks[ viewport ];
if ( previewSize < breakValues.min ) {
newPreviewSize = breakValues.min;
}
if ( previewSize > breakValues.max ) {
newPreviewSize = breakValues.max;
}
return newPreviewSize;
}, [ viewport, view ] );
}
const imageSizes = [
{
value: 230,
breakpoint: 1,
},
{
value: 290,
breakpoint: 1112, // at minimum image width, 4 images display at this container size
},
{
value: 350,
breakpoint: 1636, // at minimum image width, 6 images display at this container size
Copy link
Member

@oandregal oandregal Jul 30, 2025

Choose a reason for hiding this comment

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

I've tried this on a large screen (2560px), way past the max breakpoint (1636). With these changes, the images aren't that big and the space looks more balanced. Of course, I'm not designer, so others may have different thoughts 😅

In any case, I like that the mapping from a minimum breakpoint to a minimum image size is clearer and easier to tweak.

Before After
Min width (6 images before, 8 after) Screenshot 2025-07-30 at 10 20 43 Screenshot 2025-07-30 at 10 20 30
Max width (3 images before, 5 after) Screenshot 2025-07-30 at 10 20 17 Screenshot 2025-07-30 at 10 20 02

},
{
value: 430,
breakpoint: 588, // at minimum image width, 2 images display at this container size
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This tries to emulate the current behaviour, where setting the grid at its biggest size on a small container and resizing the window preserves the biggest size setting, even when more size options appear. This is probably not too important a point in user-facing terms though, because people don't go around resizing windows that much 😄

},
];

export default function PreviewSizePicker() {
const viewport = useViewPortBreakpoint();
const context = useContext( DataViewsContext );
const view = context.view as ViewGrid;
const breakValues = viewportBreaks[ viewport ];
const previewSizeToUse = view.layout?.previewSize || breakValues.default;
const marks = useMemo(
() =>
Array.from(
{ length: breakValues.max - breakValues.min + 1 },
( _, i ) => {
return {
value: breakValues.min + i,
};
}
),
[ breakValues ]
);
if ( viewport === 'mobile' ) {

if ( context.containerWidth < 588 ) {
return null;
}

const breakValues = imageSizes.filter( ( size ) => {
return context.containerWidth >= size.breakpoint;
} );

// If the container has resized and the set preview size is no longer available,
// we reset it to the next smallest size.
const previewSizeToUse = view.layout?.previewSize
? breakValues
.map( ( size, index ) => ( { ...size, index } ) )
.filter(
( size ) => size.value <= ( view.layout?.previewSize ?? 0 ) // We know the view.layout?.previewSize exists at this point but the linter doesn't seem to.
)
.sort( ( a, b ) => b.value - a.value )[ 0 ].index
: 0;

const marks = breakValues.map( ( size, index ) => {
return {
value: index,
};
} );

return (
<RangeControl
__nextHasNoMarginBottom
__next40pxDefaultSize
showTooltip={ false }
label={ __( 'Preview size' ) }
value={ breakValues.max + breakValues.min - previewSizeToUse }
marks={ marks }
min={ breakValues.min }
max={ breakValues.max }
value={ previewSizeToUse }
min={ 0 }
max={ breakValues.length - 1 }
withInputField={ false }
onChange={ ( value = 0 ) => {
context.onChangeView( {
...view,
layout: {
...view.layout,
previewSize: breakValues.max + breakValues.min - value,
previewSize: breakValues[ value ].value,
},
} );
} }
step={ 1 }
marks={ marks }
/>
);
}
Loading
Loading