diff --git a/packages/dataviews/src/dataviews-layouts/list/index.tsx b/packages/dataviews/src/dataviews-layouts/list/index.tsx index fc817a237c9e19..d3eb366bb1dab3 100644 --- a/packages/dataviews/src/dataviews-layouts/list/index.tsx +++ b/packages/dataviews/src/dataviews-layouts/list/index.tsx @@ -38,7 +38,7 @@ import type { Action, NormalizedField, ViewListProps } from '../../types'; interface ListViewItemProps< Item > { actions: Action< Item >[]; - id: string; + idPrefix: string; isSelected: boolean; item: Item; mediaField?: NormalizedField< Item >; @@ -55,23 +55,39 @@ const { DropdownMenuV2: DropdownMenu, } = unlock( componentsPrivateApis ); -function generateDropdownTriggerCompositeId( domId: string ) { - return `${ domId }-dropdown`; +function generateItemWrapperCompositeId( idPrefix: string ) { + return `${ idPrefix }-item-wrapper`; +} +function generatePrimaryActionCompositeId( + idPrefix: string, + primaryActionId: string +) { + return `${ idPrefix }-primary-action-${ primaryActionId }`; +} +function generateDropdownTriggerCompositeId( idPrefix: string ) { + return `${ idPrefix }-dropdown`; +} +function isCompositeItemId( idToCheck: string, idPrefix: string ) { + // All composite items use the same prefix in their IDs. + return idToCheck.startsWith( idPrefix ); } function PrimaryActionGridCell< Item >( { + idPrefix, primaryAction, - id, item, }: { - id: string; + idPrefix: string; primaryAction: Action< Item >; item: Item; } ) { const registry = useRegistry(); const [ isModalOpen, setIsModalOpen ] = useState( false ); - const compositeItemId = `${ id }-${ primaryAction.id }`; + const compositeItemId = generatePrimaryActionCompositeId( + idPrefix, + primaryAction.id + ); const label = typeof primaryAction.label === 'string' @@ -123,7 +139,7 @@ function PrimaryActionGridCell< Item >( { function ListItem< Item >( { actions, - id, + idPrefix, isSelected, item, mediaField, @@ -133,8 +149,8 @@ function ListItem< Item >( { onDropdownTriggerKeyDown, }: ListViewItemProps< Item > ) { const itemRef = useRef< HTMLElement >( null ); - const labelId = `${ id }-label`; - const descriptionId = `${ id }-description`; + const labelId = `${ idPrefix }-label`; + const descriptionId = `${ idPrefix }-description`; const [ isHovered, setIsHovered ] = useState( false ); const handleMouseEnter = () => { @@ -200,7 +216,7 @@ function ListItem< Item >( { } role="button" - id={ id } + id={ generateItemWrapperCompositeId( idPrefix ) } aria-pressed={ isSelected } aria-labelledby={ labelId } aria-describedby={ descriptionId } @@ -262,7 +278,7 @@ function ListItem< Item >( { > { primaryAction && ( @@ -272,7 +288,7 @@ function ListItem< Item >( { trigger={ ( props: ViewListProps< Item > ) { const onSelect = ( item: Item ) => onChangeSelection( [ getItemId( item ) ] ); - const getItemDomId = useCallback( + const generateCompositeItemIdPrefix = useCallback( ( item: Item ) => `${ baseId }-${ getItemId( item ) }`, [ baseId, getItemId ] ); + // Controlled state for the active composite item. const [ activeCompositeId, setActiveCompositeId ] = useState< string | null | undefined >( // By default, the active composite item is the selected one. - selectedItem ? getItemDomId( selectedItem ) : undefined + selectedItem ? generateCompositeItemIdPrefix( selectedItem ) : undefined ); - const activeItemIndex = data.findIndex( ( item ) => { - const itemCompositeIdPrefix = getItemDomId( item ); - return ( - !! itemCompositeIdPrefix && - activeCompositeId?.startsWith( itemCompositeIdPrefix ) - ); - } ); + const activeItemIndex = data.findIndex( ( item ) => + isCompositeItemId( + activeCompositeId ?? '', + generateCompositeItemIdPrefix( item ) + ) + ); const previousActiveItemIndex = usePrevious( activeItemIndex ); const isActiveIdInList = activeItemIndex !== -1; @@ -364,21 +380,23 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { ( targetIndex: number, // Allows invokers to specify a custom function to generate the - // target composite item ID (e.g. for the dropdown menu trigger). - getCompositeId = ( id: string ) => id + // target composite item ID + generateCompositeId: ( idPrefix: string ) => string ) => { // Clamping between 0 and data.length - 1 to avoid out of bounds. const clampedIndex = Math.min( data.length - 1, Math.max( 0, targetIndex ) ); - const domId = getItemDomId( data[ clampedIndex ] ); - const targetCompositeItemId = getCompositeId( domId ); + const itemIdPrefix = generateCompositeItemIdPrefix( + data[ clampedIndex ] + ); + const targetCompositeItemId = generateCompositeId( itemIdPrefix ); setActiveCompositeId( targetCompositeItemId ); document.getElementById( targetCompositeItemId )?.focus(); }, - [ data, getItemDomId ] + [ data, generateCompositeItemIdPrefix ] ); // Select a new active composite item when the current active item @@ -389,7 +407,10 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { // basically picking the item that would have been after the deleted one. // If the previously active (and removed) item was the last of the list, // we will select the item before it — which is the new last item. - selectCompositeItem( previousActiveItemIndex ?? 0 ); + selectCompositeItem( + previousActiveItemIndex ?? 0, + generateItemWrapperCompositeId + ); } }, [ previousActiveItemIndex, isActiveIdInList, selectCompositeItem ] ); @@ -444,11 +465,11 @@ export default function ViewList< Item >( props: ViewListProps< Item > ) { setActiveId={ setActiveCompositeId } > { data.map( ( item ) => { - const id = getItemDomId( item ); + const id = generateCompositeItemIdPrefix( item ); return (