/**
* 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;