-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Try using responsive images in dataview grid layouts #70493
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
Changes from 11 commits
c51c52b
11c4cb6
8ed1094
e6fb1f7
4bb82b8
fc7a29d
a4cb98b
4f74b4c
792b887
ad0f857
dfcc42d
29cc5de
653e108
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,6 @@ import type { ComponentProps, ReactElement } from 'react'; | |
| * WordPress dependencies | ||
| */ | ||
| import { | ||
| __experimentalGrid as Grid, | ||
| __experimentalHStack as HStack, | ||
| __experimentalVStack as VStack, | ||
| Spinner, | ||
|
|
@@ -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, | ||
|
|
@@ -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 > { | ||
|
|
@@ -61,6 +61,9 @@ interface GridItemProps< Item > { | |
| regularFields: NormalizedField< Item >[]; | ||
| badgeFields: NormalizedField< Item >[]; | ||
| hasBulkActions: boolean; | ||
| config: { | ||
| size: string; | ||
| }; | ||
| } | ||
|
|
||
| function GridItem< Item >( { | ||
|
|
@@ -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 ? ( | ||
|
|
@@ -256,6 +264,7 @@ function ViewGrid< Item >( { | |
| view, | ||
| className, | ||
| }: ViewGridProps< Item > ) { | ||
| const { resizeObserverRef } = useContext( DataViewsContext ); | ||
| const titleField = fields.find( | ||
| ( field ) => field.id === view?.titleField | ||
| ); | ||
|
|
@@ -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'; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! |
||
|
|
||
| const groupField = view.groupByField | ||
| ? fields.find( ( f ) => f.id === view.groupByField ) | ||
|
|
@@ -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 ( | ||
|
|
@@ -370,10 +377,13 @@ function ViewGrid< Item >( { | |
| hasBulkActions={ | ||
| hasBulkActions | ||
| } | ||
| config={ { | ||
| size, | ||
| } } | ||
| /> | ||
| ); | ||
| } ) } | ||
| </Grid> | ||
| </div> | ||
| </VStack> | ||
| ) | ||
| ) } | ||
|
|
@@ -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 ( | ||
|
|
@@ -411,10 +423,13 @@ function ViewGrid< Item >( { | |
| regularFields={ regularFields } | ||
| badgeFields={ badgeFields } | ||
| hasBulkActions={ hasBulkActions } | ||
| config={ { | ||
| size, | ||
| } } | ||
| /> | ||
| ); | ||
| } ) } | ||
| </Grid> | ||
| </div> | ||
| ) | ||
| } | ||
| { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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 | |||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
|||||||||||
| }, | |||||||||||
| { | |||||||||||
| value: 430, | |||||||||||
| breakpoint: 588, // at minimum image width, 2 images display at this container size | |||||||||||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 } | |||||||||||
| /> | |||||||||||
| ); | |||||||||||
| } | |||||||||||




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 believe that
containerRefneeds 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.