/** * External dependencies */ import clsx from 'clsx'; import type { ReactNode } from 'react'; /** * WordPress dependencies */ import { Spinner, Flex, FlexItem, privateApis as componentsPrivateApis, Composite, } from '@wordpress/components'; import { __, sprintf } from '@wordpress/i18n'; import { useInstanceId } from '@wordpress/compose'; import { useContext, useRef } from '@wordpress/element'; import { Stack } from '@wordpress/ui'; /** * Internal dependencies */ import { unlock } from '../../../lock-unlock'; import DataViewsSelectionCheckbox from '../../dataviews-selection-checkbox'; import DataViewsContext from '../../dataviews-context'; import { useIsMultiselectPicker } from '../../dataviews-picker-footer'; import type { NormalizedField, ViewPickerGrid as ViewPickerGridType, ViewPickerGridProps, } from '../../../types'; import type { SetSelection } from '../../../types/private'; import { GridItems } from '../utils/grid-items'; const { Badge } = unlock( componentsPrivateApis ); import getDataByGroup from '../utils/get-data-by-group'; import { useGridColumns } from '../grid/preview-size-picker'; import { useIntersectionObserver, usePlaceholdersNeeded, } from '../utils/use-infinite-scroll'; interface GridItemProps< Item > { view: ViewPickerGridType; multiselect?: boolean; selection: string[]; onChangeSelection: SetSelection; getItemId: ( item: Item ) => string; item: Item; titleField?: NormalizedField< Item >; mediaField?: NormalizedField< Item >; descriptionField?: NormalizedField< Item >; regularFields: NormalizedField< Item >[]; badgeFields: NormalizedField< Item >[]; config: { sizes: string; }; posinset?: number; setsize?: number; } function GridItem< Item >( { view, multiselect, selection, onChangeSelection, getItemId, item, mediaField, titleField, descriptionField, regularFields, badgeFields, config, posinset, setsize, }: GridItemProps< Item > ) { const { showTitle = true, showMedia = true, showDescription = true } = view; const id = getItemId( item ); const elementRef = useRef< HTMLElement | null >( null ); const isSelected = selection.includes( id ); const setElementRef = ( element: HTMLElement | null ) => { elementRef.current = element; }; useIntersectionObserver( elementRef, posinset ); const renderedMediaField = mediaField?.render ? ( ) : null; const renderedTitleField = showTitle && titleField?.render ? ( ) : null; return ( ( ) } role="option" aria-posinset={ posinset } aria-setsize={ setsize } className={ clsx( 'dataviews-view-picker-grid__card', { 'is-selected': isSelected, } ) } aria-selected={ isSelected } onClick={ () => { // Toggle in/out of selection array if ( isSelected ) { onChangeSelection( selection.filter( ( itemId ) => id !== itemId ) ); } else { const newSelection = multiselect ? [ ...selection, id ] : [ id ]; onChangeSelection( newSelection ); } } } > { showMedia && renderedMediaField && (
{ renderedMediaField }
) } { showMedia && renderedMediaField && ( ) } { showTitle && (
{ renderedTitleField }
) } { showDescription && descriptionField?.render && ( ) } { !! badgeFields?.length && ( { badgeFields.map( ( field ) => { return ( ); } ) } ) } { !! regularFields?.length && ( { regularFields.map( ( field ) => { return ( <> { field.header } ); } ) } ) }
); } function GridGroup< Item >( { groupName, groupField, showLabel = true, children, }: { groupName: string; groupField: NormalizedField< Item >; showLabel?: boolean; children: ReactNode; } ) { const headerId = useInstanceId( GridGroup, 'dataviews-view-picker-grid-group__header' ); return (

{ showLabel ? sprintf( // translators: 1: The label of the field e.g. "Date". 2: The value of the field, e.g.: "May 2022". __( '%1$s: %2$s' ), groupField.label, groupName ) : groupName }

{ children }
); } function ViewPickerGrid< Item >( { actions, data, fields, getItemId, isLoading, onChangeSelection, selection, view, className, empty, }: ViewPickerGridProps< Item > ) { const { resizeObserverRef, paginationInfo, itemListLabel } = useContext( DataViewsContext ); const titleField = fields.find( ( field ) => field.id === view?.titleField ); const mediaField = fields.find( ( field ) => field.id === view?.mediaField ); const descriptionField = fields.find( ( field ) => field.id === view?.descriptionField ); const otherFields = view.fields ?? []; const { regularFields, badgeFields } = otherFields.reduce( ( accumulator: Record< string, NormalizedField< Item >[] >, fieldId ) => { const field = fields.find( ( f ) => f.id === fieldId ); if ( ! field ) { return accumulator; } // If the field is a badge field, add it to the badgeFields array // otherwise add it to the rest visibleFields array. const key = view.layout?.badgeFields?.includes( fieldId ) ? 'badgeFields' : 'regularFields'; accumulator[ key ].push( field ); return accumulator; }, { regularFields: [], badgeFields: [] } ); const hasData = !! data?.length; const usedPreviewSize = view.layout?.previewSize; const isMultiselect = useIsMultiselectPicker( actions ); /* * This is the maximum width that an image can achieve in the grid. The reasoning is: * The biggest min image width available is 430px (see /dataviews-layouts/grid/preview-size-picker.tsx). * Because the grid is responsive, once there is room for another column, the images shrink to accommodate it. * So each image will never grow past 2*430px plus a little more to account for the gaps. */ const size = '900px'; const groupField = view.groupBy?.field ? fields.find( ( f ) => f.id === view.groupBy?.field ) : null; const dataByGroup = groupField ? getDataByGroup( data, groupField ) : null; const isInfiniteScroll = ( view.infiniteScrollEnabled && ! dataByGroup ) ?? false; const currentPage = view?.page ?? 1; const perPage = view?.perPage ?? 0; const setSize = isInfiniteScroll ? paginationInfo?.totalItems : undefined; // Calculate placeholders needed for infinite scroll const gridColumns = useGridColumns(); const placeholdersNeeded = usePlaceholdersNeeded( data, isInfiniteScroll, gridColumns ); return ( <> { // Render multiple groups. hasData && groupField && dataByGroup && ( ( ) } > { Array.from( dataByGroup.entries() ).map( ( [ groupName, groupItems ] ) => ( } > { groupItems.map( ( item ) => { // Use position from item if available (infinite scroll), otherwise calculate. const posInSet = ( item as any ).position ?? ( currentPage - 1 ) * perPage + data.indexOf( item ) + 1; return ( ); } ) } ) ) } ) } { // Render a single grid with all data. hasData && ! dataByGroup && ( } /> } virtualFocus orientation="horizontal" role="listbox" aria-multiselectable={ isMultiselect } aria-label={ itemListLabel } > { /* Render placeholders for unloaded items in first row */ } { Array.from( { length: placeholdersNeeded } ).map( ( _, index ) => ( ( ) } role="option" aria-hidden tabIndex={ -1 } className="dataviews-view-picker-grid__card dataviews-view-picker-grid__placeholder" /> ) ) } { data.map( ( item ) => { // Use position from item for accessibility in infinite scroll mode. const posinset = ( item as any ).position; return ( ); } ) } ) } { // Render empty state. ! hasData && (
{ isLoading ? (

) : ( empty ) }
) } { hasData && isLoading && (

) } ); } export default ViewPickerGrid;