/** * External dependencies */ import clsx from 'clsx'; /** * WordPress dependencies */ import { __, sprintf } from '@wordpress/i18n'; import { Spinner, Composite } from '@wordpress/components'; import { useContext, useEffect, useId, useRef, useState, } from '@wordpress/element'; /** * Internal dependencies */ import DataViewsContext from '../../dataviews-context'; import DataViewsSelectionCheckbox from '../../dataviews-selection-checkbox'; import { useIsMultiselectPicker } from '../../dataviews-picker-footer'; import { BulkSelectionCheckbox } from '../../dataviews-bulk-actions'; import { sortValues } from '../../../constants'; import type { NormalizedField, ViewPickerTable as ViewPickerTableType, ViewPickerTableProps, } from '../../../types'; import type { SetSelection } from '../../../types/private'; import ColumnHeaderMenu from '../table/column-header-menu'; import ColumnPrimary from '../table/column-primary'; import getDataByGroup from '../utils/get-data-by-group'; import { useIntersectionObserver } from '../utils/use-infinite-scroll'; interface TableColumnFieldProps< Item > { fields: NormalizedField< Item >[]; column: string; item: Item; align?: 'start' | 'center' | 'end'; } interface TableRowProps< Item > { item: Item; fields: NormalizedField< Item >[]; id: string; view: ViewPickerTableType; titleField?: NormalizedField< Item >; mediaField?: NormalizedField< Item >; descriptionField?: NormalizedField< Item >; selection: string[]; getItemId: ( item: Item ) => string; onChangeSelection: SetSelection; multiselect: boolean; posinset?: number; } function TableColumnField< Item >( { item, fields, column, align, }: TableColumnFieldProps< Item > ) { const field = fields.find( ( f ) => f.id === column ); if ( ! field ) { return null; } const className = clsx( 'dataviews-view-table__cell-content-wrapper', { 'dataviews-view-table__cell-align-end': align === 'end', 'dataviews-view-table__cell-align-center': align === 'center', } ); return (
); } function TableRow< Item >( { item, fields, id, view, titleField, mediaField, descriptionField, selection, getItemId, onChangeSelection, multiselect, posinset, }: TableRowProps< Item > ) { const { paginationInfo } = useContext( DataViewsContext ); const isSelected = selection.includes( id ); const [ isHovered, setIsHovered ] = useState( false ); const elementRef = useRef< HTMLElement | null >( null ); const setElementRef = ( element: HTMLElement | null ) => { elementRef.current = element; }; useIntersectionObserver( elementRef, posinset ); const { showTitle = true, showMedia = true, showDescription = true, infiniteScrollEnabled, } = view; const handleMouseEnter = () => { setIsHovered( true ); }; const handleMouseLeave = () => { setIsHovered( false ); }; const columns = view.fields ?? []; const hasPrimaryColumn = ( titleField && showTitle ) || ( mediaField && showMedia ) || ( descriptionField && showDescription ); return ( ( ) } aria-selected={ isSelected } aria-setsize={ paginationInfo.totalItems || undefined } aria-posinset={ posinset } role={ infiniteScrollEnabled ? 'article' : 'option' } onClick={ () => { // Toggle in/out of selection array if ( isSelected ) { onChangeSelection( selection.filter( ( itemId ) => id !== itemId ) ); } else { const newSelection = multiselect ? [ ...selection, id ] : [ id ]; onChangeSelection( newSelection ); } } } >
{ hasPrimaryColumn && ( false } /> ) } { columns.map( ( column: string ) => { // Explicit picks the supported styles. const { width, maxWidth, minWidth, align } = view.layout?.styles?.[ column ] ?? {}; return ( ); } ) }
); } function ViewPickerTable< Item >( { actions, data, fields, getItemId, isLoading = false, onChangeView, onChangeSelection, selection, setOpenedFilter, view, className, empty, }: ViewPickerTableProps< Item > ) { const headerMenuRefs = useRef< Map< string, { node: HTMLButtonElement; fallback: string } > >( new Map() ); const headerMenuToFocusRef = useRef< HTMLButtonElement >( undefined ); const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState< HTMLButtonElement >(); const isMultiselect = useIsMultiselectPicker( actions ) ?? false; useEffect( () => { if ( headerMenuToFocusRef.current ) { headerMenuToFocusRef.current.focus(); headerMenuToFocusRef.current = undefined; } } ); 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; const tableNoticeId = useId(); if ( nextHeaderMenuToFocus ) { // If we need to force focus, we short-circuit rendering here // to prevent any additional work while we handle that. // Clearing out the focus directive is necessary to make sure // future renders don't cause unexpected focus jumps. headerMenuToFocusRef.current = nextHeaderMenuToFocus; setNextHeaderMenuToFocus( undefined ); return; } const onHide = ( field: NormalizedField< Item > ) => { const hidden = headerMenuRefs.current.get( field.id ); const fallback = hidden ? headerMenuRefs.current.get( hidden.fallback ) : undefined; setNextHeaderMenuToFocus( fallback?.node ); }; const hasData = !! data?.length; 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 { showTitle = true, showMedia = true, showDescription = true } = view; const hasPrimaryColumn = ( titleField && showTitle ) || ( mediaField && showMedia ) || ( descriptionField && showDescription ); const columns = view.fields ?? []; const headerMenuRef = ( column: string, index: number ) => ( node: HTMLButtonElement ) => { if ( node ) { headerMenuRefs.current.set( column, { node, fallback: columns[ index > 0 ? index - 1 : 1 ], } ); } else { headerMenuRefs.current.delete( column ); } }; return ( <> { hasPrimaryColumn && ( ) } { columns.map( ( column, index ) => { // Explicit picks the supported styles. const { width, maxWidth, minWidth, align } = view.layout?.styles?.[ column ] ?? {}; return ( ); } ) } { /* Render grouped data if groupBy is specified */ } { hasData && groupField && dataByGroup ? ( Array.from( dataByGroup.entries() ).map( ( [ groupName, groupItems ] ) => ( } > { groupItems.map( ( item, index ) => ( ) ) } ) ) ) : ( } virtualFocus orientation="vertical" > { hasData && data.map( ( item, index ) => { const itemId = getItemId( item ); // Use position from item for accessibility in infinite scroll mode. const posinset = ( item as any ).position; return ( ); } ) } ) }
{ isMultiselect && ( ) } { titleField && ( ) }
{ view.groupBy?.showLabel === false ? groupName : 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 ) }
{ ! hasData && ( isLoading ? (

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

) }
); } export default ViewPickerTable;