diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index 823999b07621b..db8dda4b1386b 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -2,9 +2,6 @@ * External dependencies */ import clsx from 'clsx'; -// TODO: use the @wordpress/components one once public -// eslint-disable-next-line no-restricted-imports -import * as Ariakit from '@ariakit/react'; /** * WordPress dependencies @@ -24,7 +21,6 @@ import { useMemo, useRef, useState, - useContext, } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import { moreVertical } from '@wordpress/icons'; @@ -48,8 +44,9 @@ interface ListViewItemProps< Item > { mediaField?: NormalizedField< Item >; onSelect: ( item: Item ) => void; primaryField?: NormalizedField< Item >; - store?: Ariakit.CompositeStore; visibleFields: NormalizedField< Item >[]; + selectPreviousItemDropdownTrigger: () => void; + selectNextItemDropdownTrigger: () => void; } const { @@ -67,8 +64,9 @@ function ListItem< Item >( { mediaField, onSelect, primaryField, - store, visibleFields, + selectPreviousItemDropdownTrigger, + selectNextItemDropdownTrigger, }: ListViewItemProps< Item > ) { const registry = useRegistry(); const itemRef = useRef< HTMLElement >( null ); @@ -209,6 +207,7 @@ function ListItem< Item >( { { primaryAction && 'RenderModal' in primaryAction && (
( { ! ( 'RenderModal' in primaryAction ) && (
( { ( { onKeyDown={ ( event: React.KeyboardEvent< HTMLButtonElement > ) => { - if ( ! store ) { - return; - } - if ( event.key === 'ArrowDown' ) { event.preventDefault(); - store.move( - store.down() - ); + selectNextItemDropdownTrigger(); } if ( event.key === 'ArrowUp' ) { event.preventDefault(); - store.move( - store.up() - ); + selectPreviousItemDropdownTrigger(); } } } /> @@ -317,35 +310,6 @@ function ListItem< Item >( { ); } -function ViewListWrapper( { - children, -}: { - children: ( store?: Ariakit.CompositeStore ) => React.ReactNode; -} ) { - const { store: compositeStore } = - ( useContext( Composite.Context ) as - | { store: Ariakit.CompositeStore } - | undefined ) ?? {}; - - // Manage focused item, when the active one is removed from the list. - const isActiveIdInList = Ariakit.useStoreState( - compositeStore, - ( state ) => state?.items.some( ( item ) => item.id === state.activeId ) - ); - useEffect( () => { - if ( compositeStore && ! isActiveIdInList ) { - // Prefer going down, except if there is no item below (last item), then go up (last item in list). - if ( compositeStore.down() ) { - compositeStore.move( compositeStore.down() ); - } else if ( compositeStore.up() ) { - compositeStore.move( compositeStore.up() ); - } - } - }, [ compositeStore, isActiveIdInList ] ); - - return children( compositeStore ); -} - export default function ViewList< Item >( props: ViewListProps< Item > ) { const { actions, @@ -358,9 +322,25 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { view, } = props; const baseId = useInstanceId( ViewList, 'view-list' ); + + const getItemDomId = useCallback( + ( item?: Item ) => + item ? `${ baseId }-${ getItemId( item ) }` : undefined, + [ baseId, getItemId ] + ); + const selectedItem = data?.findLast( ( item ) => selection.includes( getItemId( item ) ) ); + const onSelect = ( item: Item ) => + onChangeSelection( [ getItemId( item ) ] ); + + const [ activeCompositeId, setActiveCompositeId ] = useState< + string | null | undefined + >( + // By default, the active composite item is the selected one. + getItemDomId( selectedItem ) + ); const mediaField = fields.find( ( field ) => field.id === view.layout?.mediaField @@ -377,14 +357,58 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { ) ); - const onSelect = ( item: Item ) => - onChangeSelection( [ getItemId( item ) ] ); + const activeItemIndex = data.findIndex( ( item ) => { + const itemCompositeIdPrefix = getItemDomId( item ); + return ( + !! itemCompositeIdPrefix && + activeCompositeId?.startsWith( itemCompositeIdPrefix ) + ); + } ); + const isActiveIdInList = activeItemIndex !== -1; - const getItemDomId = useCallback( - ( item?: Item ) => - item ? `${ baseId }-${ getItemId( item ) }` : undefined, - [ baseId, getItemId ] - ); + const firstDataitem = data?.[ 0 ]; + useEffect( () => { + if ( ! isActiveIdInList && firstDataitem ) { + // Prefer going down, except if there is no item below (last item), then go up (last item in list). + // if ( compositeStore.down() ) { + // compositeStore.move( compositeStore.down() ); + // } else if ( compositeStore.up() ) { + // compositeStore.move( compositeStore.up() ); + // } + + // TODO: better next active item selection logic (up/down), + // instead of always picking first item. + const nextCompositeActiveId = getItemDomId( firstDataitem ); + setActiveCompositeId( nextCompositeActiveId ); + ( + document.querySelector( `#${ nextCompositeActiveId }` ) as + | HTMLElement + | undefined + )?.focus(); + } + }, [ firstDataitem, getItemDomId, isActiveIdInList ] ); + + const selectItemDropdownTrigger = ( itemIndex: number ) => { + const item = data[ itemIndex ]; + const nextCompositeActiveId = `${ getItemDomId( item ) }-dropdown`; + setActiveCompositeId( nextCompositeActiveId ); + ( + document.querySelector( `#${ nextCompositeActiveId }` ) as + | HTMLElement + | undefined + )?.focus(); + }; + + const selectPreviousItemDropdownTrigger = () => { + selectItemDropdownTrigger( + Math.min( data.length - 1, Math.max( 0, activeItemIndex - 1 ) ) + ); + }; + const selectNextItemDropdownTrigger = () => { + selectItemDropdownTrigger( + Math.min( data.length - 1, Math.max( 0, activeItemIndex + 1 ) ) + ); + }; const hasData = data?.length; if ( ! hasData ) { @@ -408,29 +432,31 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { render={
    } className="dataviews-view-list" role="grid" - defaultActiveId={ getItemDomId( selectedItem ) } + activeId={ activeCompositeId } + setActiveId={ setActiveCompositeId } > - - { ( store ) => - data.map( ( item ) => { - const id = getItemDomId( item ); - return ( - - ); - } ) - } - + { data.map( ( item ) => { + const id = getItemDomId( item ); + return ( + + ); + } ) } ); }