diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index b85ab247e81a5..121877e82a8d6 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -340,6 +340,7 @@ li { margin: 0; + cursor: pointer; .dataviews-view-list__item-wrapper { position: relative; @@ -355,14 +356,21 @@ background: $gray-100; height: 1px; } - } - &:not(.is-selected):hover { - color: var(--wp-admin-theme-color); + > * { + width: 100%; + } + } - .dataviews-view-list__primary-field, - .dataviews-view-list__fields { + &:not(.is-selected) { + &:hover, + &:focus-within { color: var(--wp-admin-theme-color); + + .dataviews-view-list__primary-field, + .dataviews-view-list__fields { + color: var(--wp-admin-theme-color); + } } } } @@ -388,7 +396,8 @@ .dataviews-view-list__item { padding: $grid-unit-15 0 $grid-unit-15 $grid-unit-30; width: 100%; - cursor: pointer; + scroll-margin: $grid-unit-10 0; + &:focus { &::before { position: absolute; @@ -449,7 +458,9 @@ line-height: $grid-unit-20; .dataviews-view-list__field { - &:empty { + margin: 0; + + &:has(.dataviews-view-list__field-value:empty) { display: none; } } diff --git a/packages/dataviews/src/view-list.js b/packages/dataviews/src/view-list.js index ca6de677b99fd..677d90c204f81 100644 --- a/packages/dataviews/src/view-list.js +++ b/packages/dataviews/src/view-list.js @@ -6,17 +6,135 @@ import classNames from 'classnames'; /** * WordPress dependencies */ -import { useAsyncList } from '@wordpress/compose'; +import { useAsyncList, useInstanceId } from '@wordpress/compose'; import { __experimentalHStack as HStack, __experimentalVStack as VStack, + privateApis as componentsPrivateApis, Button, Spinner, + VisuallyHidden, } from '@wordpress/components'; -import { ENTER, SPACE } from '@wordpress/keycodes'; +import { useCallback, useEffect, useRef } from '@wordpress/element'; import { info } from '@wordpress/icons'; import { __ } from '@wordpress/i18n'; +/** + * Internal dependencies + */ +import { unlock } from './lock-unlock'; + +const { + useCompositeStoreV2: useCompositeStore, + CompositeV2: Composite, + CompositeItemV2: CompositeItem, + CompositeRowV2: CompositeRow, +} = unlock( componentsPrivateApis ); + +function ListItem( { + id, + item, + isSelected, + onSelect, + onDetailsChange, + mediaField, + primaryField, + visibleFields, +} ) { + const itemRef = useRef( null ); + const labelId = `${ id }-label`; + const descriptionId = `${ id }-description`; + + useEffect( () => { + if ( isSelected ) { + itemRef.current?.scrollIntoView( { + behavior: 'auto', + block: 'nearest', + inline: 'nearest', + } ); + } + }, [ isSelected ] ); + + return ( + } + role="row" + className={ classNames( { + 'is-selected': isSelected, + } ) } + > + +
+ } + role="button" + id={ id } + aria-pressed={ isSelected } + aria-labelledby={ labelId } + aria-describedby={ descriptionId } + className="dataviews-view-list__item" + onClick={ () => onSelect( item ) } + > + +
+ { mediaField?.render( { item } ) || ( +
+ ) } +
+ + + { primaryField?.render( { item } ) } + +
+ { visibleFields.map( ( field ) => ( +

+ + { field.header } + + + { field.render( { item } ) } + +

+ ) ) } +
+
+
+
+
+ { onDetailsChange && ( +
+ } + className="dataviews-view-list__details-button" + onClick={ () => onDetailsChange( [ item ] ) } + icon={ info } + label={ __( 'View details' ) } + size="compact" + /> +
+ ) } +
+
+ ); +} + export default function ViewList( { view, fields, @@ -27,9 +145,15 @@ export default function ViewList( { onDetailsChange, selection, deferredRendering, + id: preferredId, } ) { + const baseId = useInstanceId( ViewList, 'view-list', preferredId ); const shownData = useAsyncList( data, { step: 3 } ); const usedData = deferredRendering ? shownData : data; + const selectedItem = usedData?.findLast( ( item ) => + selection.includes( item.id ) + ); + const mediaField = fields.find( ( field ) => field.id === view.layout.mediaField ); @@ -44,12 +168,19 @@ export default function ViewList( { ) ); - const onEnter = ( item ) => ( event ) => { - const { keyCode } = event; - if ( [ ENTER, SPACE ].includes( keyCode ) ) { - onSelectionChange( [ item ] ); - } - }; + const onSelect = useCallback( + ( item ) => onSelectionChange( [ item ] ), + [ onSelectionChange ] + ); + + const getItemDomId = useCallback( + ( item ) => ( item ? `${ baseId }-${ getItemId( item ) }` : undefined ), + [ baseId, getItemId ] + ); + + const store = useCompositeStore( { + defaultActiveId: getItemDomId( selectedItem ), + } ); const hasData = usedData?.length; if ( ! hasData ) { @@ -68,70 +199,29 @@ export default function ViewList( { } return ( - + ); }