Skip to content

Commit

Permalink
DataViews: updating keyboard navigation in list layouts (WordPress#59637
Browse files Browse the repository at this point in the history
)

Co-authored-by: andrewhayward <andrewhayward@git.wordpress.org>
Co-authored-by: oandregal <oandregal@git.wordpress.org>
Co-authored-by: jameskoster <jameskoster@git.wordpress.org>
  • Loading branch information
4 people authored and carstingaxion committed Mar 27, 2024
1 parent 255e076 commit 1bf2154
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 76 deletions.
25 changes: 18 additions & 7 deletions packages/dataviews/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@

li {
margin: 0;
cursor: pointer;

.dataviews-view-list__item-wrapper {
position: relative;
Expand All @@ -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);
}
}
}
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}
Expand Down
228 changes: 159 additions & 69 deletions packages/dataviews/src/view-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<CompositeRow
ref={ itemRef }
render={ <li /> }
role="row"
className={ classNames( {
'is-selected': isSelected,
} ) }
>
<HStack className="dataviews-view-list__item-wrapper">
<div role="gridcell">
<CompositeItem
render={ <div /> }
role="button"
id={ id }
aria-pressed={ isSelected }
aria-labelledby={ labelId }
aria-describedby={ descriptionId }
className="dataviews-view-list__item"
onClick={ () => onSelect( item ) }
>
<HStack
spacing={ 3 }
justify="start"
alignment="flex-start"
>
<div className="dataviews-view-list__media-wrapper">
{ mediaField?.render( { item } ) || (
<div className="dataviews-view-list__media-placeholder"></div>
) }
</div>
<VStack spacing={ 1 }>
<span
className="dataviews-view-list__primary-field"
id={ labelId }
>
{ primaryField?.render( { item } ) }
</span>
<div
className="dataviews-view-list__fields"
id={ descriptionId }
>
{ visibleFields.map( ( field ) => (
<p
key={ field.id }
className="dataviews-view-list__field"
>
<VisuallyHidden
as="span"
className="dataviews-view-list__field-label"
>
{ field.header }
</VisuallyHidden>
<span className="dataviews-view-list__field-value">
{ field.render( { item } ) }
</span>
</p>
) ) }
</div>
</VStack>
</HStack>
</CompositeItem>
</div>
{ onDetailsChange && (
<div role="gridcell">
<CompositeItem
render={ <Button /> }
className="dataviews-view-list__details-button"
onClick={ () => onDetailsChange( [ item ] ) }
icon={ info }
label={ __( 'View details' ) }
size="compact"
/>
</div>
) }
</HStack>
</CompositeRow>
);
}

export default function ViewList( {
view,
fields,
Expand All @@ -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
);
Expand All @@ -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 ) {
Expand All @@ -68,70 +199,29 @@ export default function ViewList( {
}

return (
<ul className="dataviews-view-list">
<Composite
id={ baseId }
render={ <ul /> }
className="dataviews-view-list"
role="grid"
store={ store }
>
{ usedData.map( ( item ) => {
const id = getItemDomId( item );
return (
<li
key={ getItemId( item ) }
className={ classNames( {
'is-selected': selection.includes( item.id ),
} ) }
>
<HStack className="dataviews-view-list__item-wrapper">
<div
role="button"
tabIndex={ 0 }
aria-pressed={ selection.includes( item.id ) }
onKeyDown={ onEnter( item ) }
className="dataviews-view-list__item"
onClick={ () => onSelectionChange( [ item ] ) }
>
<HStack
spacing={ 3 }
justify="start"
alignment="flex-start"
>
<div className="dataviews-view-list__media-wrapper">
{ mediaField?.render( { item } ) || (
<div className="dataviews-view-list__media-placeholder"></div>
) }
</div>
<VStack spacing={ 1 }>
<span className="dataviews-view-list__primary-field">
{ primaryField?.render( { item } ) }
</span>
<div className="dataviews-view-list__fields">
{ visibleFields.map( ( field ) => {
return (
<span
key={ field.id }
className="dataviews-view-list__field"
>
{ field.render( {
item,
} ) }
</span>
);
} ) }
</div>
</VStack>
</HStack>
</div>
{ onDetailsChange && (
<Button
className="dataviews-view-list__details-button"
onClick={ () =>
onDetailsChange( [ item ] )
}
icon={ info }
label={ __( 'View details' ) }
size="compact"
/>
) }
</HStack>
</li>
<ListItem
key={ id }
id={ id }
item={ item }
isSelected={ item === selectedItem }
onSelect={ onSelect }
onDetailsChange={ onDetailsChange }
mediaField={ mediaField }
primaryField={ primaryField }
visibleFields={ visibleFields }
/>
);
} ) }
</ul>
</Composite>
);
}

0 comments on commit 1bf2154

Please sign in to comment.