From f4363e934ce3871cc6fc9a8c4138f86995177938 Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Thu, 22 Feb 2024 14:56:10 +0000 Subject: [PATCH 1/3] Adding semantics to grid layout --- packages/dataviews/src/style.scss | 23 ++-- packages/dataviews/src/view-grid.js | 194 ++++++++++++++++++++++------ 2 files changed, 172 insertions(+), 45 deletions(-) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index cd293b46d4a47..c166d6d9f4717 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -243,17 +243,18 @@ } .dataviews-view-grid { - margin-bottom: $grid-unit-30; - grid-template-columns: repeat(2, minmax(0, 1fr)) !important; - grid-template-rows: max-content; - padding: 0 $grid-unit-40; + margin: 0 $grid-unit-40 $grid-unit-30; + position: relative; - @include break-xlarge() { - grid-template-columns: repeat(3, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency + .dataviews-view-grid__rows { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(17em, 1fr)); + grid-template-rows: max-content; + gap: 1em; } - @include break-huge() { - grid-template-columns: repeat(4, minmax(0, 1fr)) !important; // Todo: eliminate !important dependency + .dataviews-view-grid__row { + display: contents; } .dataviews-view-grid__card { @@ -261,6 +262,12 @@ border: 1px solid $gray-200; height: 100%; justify-content: flex-start; + scroll-margin-top: 5em; + max-width: 26em; + + &:focus-visible { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + } .dataviews-view-grid__title-actions { padding: $grid-unit-05 $grid-unit $grid-unit-05 $grid-unit-05; diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 44ab1822a6075..5da480396f82c 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -7,21 +7,123 @@ import classnames from 'classnames'; * WordPress dependencies */ import { - __experimentalGrid as Grid, __experimentalHStack as HStack, __experimentalVStack as VStack, + privateApis as componentsPrivateApis, Tooltip, } from '@wordpress/components'; -import { __ } from '@wordpress/i18n'; -import { useAsyncList } from '@wordpress/compose'; -import { useState } from '@wordpress/element'; +import { + useAsyncList, + useInstanceId, + useResizeObserver, +} from '@wordpress/compose'; +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from '@wordpress/element'; +import { __, isRTL } from '@wordpress/i18n'; +import { isAppleOS } from '@wordpress/keycodes'; /** * Internal dependencies */ +import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import SingleSelectionCheckbox from './single-selection-checkbox'; +const { + useCompositeStoreV2: useCompositeStore, + CompositeV2: Composite, + CompositeItemV2: CompositeItem, + CompositeRowV2: CompositeRow, +} = unlock( componentsPrivateApis ); + +const GridContext = createContext( {} ); + +function Grid( { id, children, ...gridProps } ) { + const baseId = useInstanceId( Grid, 'view-grid', id ); + const gridRef = useRef( null ); + const store = useCompositeStore( { + focusWrap: 'horizontal', + rtl: isRTL(), + } ); + const context = useMemo( + () => ( { ...store, baseId } ), + [ store, baseId ] + ); + + return ( + + + + { children } + + + + ); +} + +function GridRows( { baseId, gridRef, children } ) { + const [ columnCount, setColumnCount ] = useState( children?.length || 1 ); + const [ resizeListener, { width: totalWidth } ] = useResizeObserver(); + + const referenceCell = + gridRef?.current?.querySelector( '[role="gridcell"]' ) || {}; + const { offsetWidth: columnWidth = totalWidth } = referenceCell; + + useEffect( () => { + if ( columnWidth && totalWidth ) { + setColumnCount( + Math.max( 1, Math.floor( totalWidth / columnWidth ) ) + ); + } + }, [ columnWidth, totalWidth ] ); + + const rows = useMemo( + () => + Array.from( + { length: Math.ceil( children?.length / columnCount ) }, + ( _, index ) => + children.slice( + index * columnCount, + ( index + 1 ) * columnCount + ) + ), + [ children, columnCount ] + ); + + return useMemo( + () => ( + <> + { resizeListener } +
+ { rows.map( ( row, index ) => ( + + { row } + + ) ) } +
+ + ), + [ baseId, resizeListener, rows ] + ); +} + function GridItem( { selection, data, @@ -34,39 +136,63 @@ function GridItem( { visibleFields, } ) { const [ hasNoPointerEvents, setHasNoPointerEvents ] = useState( false ); - const id = getItemId( item ); - const isSelected = selection.includes( id ); + const itemRef = useRef( null ); + const { baseId, move, next, previous, up, down } = + useContext( GridContext ); + const itemId = getItemId( item ); + const id = `${ baseId }-item-${ itemId }`; + const isSelected = selection.includes( itemId ); + const rtl = isRTL(); + const movementMap = useMemo( + () => + new Map( [ + [ 'ArrowUp', up ], + [ 'ArrowDown', down ], + [ 'ArrowLeft', rtl ? next : previous ], + [ 'ArrowRight', rtl ? previous : next ], + ] ), + [ down, next, previous, rtl, up ] + ); + return ( - } spacing={ 0 } - key={ id } + key={ itemId } + id={ id } + role="gridcell" + aria-label={ primaryField?.getValue( { item } ) } className={ classnames( 'dataviews-view-grid__card', { 'is-selected': isSelected, 'has-no-pointer-events': hasNoPointerEvents, } ) } onMouseDown={ ( event ) => { - if ( event.ctrlKey || event.metaKey ) { + if ( event.defaultPrevented ) return; + + if ( isAppleOS() ? event.ctrlKey : event.metaKey ) { setHasNoPointerEvents( true ); - if ( ! isSelected ) { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId === id || - selection.includes( itemId ) - ); - } ) - ); - } else { - onSelectionChange( - data.filter( ( _item ) => { - const itemId = getItemId?.( _item ); - return ( - itemId !== id && - selection.includes( itemId ) - ); - } ) - ); + const setAsSelected = ! isSelected; + const selectedData = data.filter( ( _item ) => { + const _itemId = getItemId?.( _item ); + const currentlyIncluded = selection.includes( _itemId ); + return setAsSelected + ? itemId === _itemId || currentlyIncluded + : itemId !== _itemId && currentlyIncluded; + } ); + onSelectionChange( selectedData ); + } + } } + onKeyDown={ ( event ) => { + if ( event.defaultPrevented ) return; + + const { target, currentTarget, key } = event; + + if ( target !== currentTarget ) { + if ( movementMap.has( key ) ) { + move( movementMap.get( key )() || id ); + } else if ( key === 'Escape' ) { + move( id ); } } } } @@ -120,7 +246,7 @@ function GridItem( { ); } ) } - + ); } @@ -154,13 +280,7 @@ export default function ViewGrid( { return ( <> { hasData && ( - + { usedData.map( ( item ) => { return ( Date: Tue, 12 Mar 2024 20:00:29 +0000 Subject: [PATCH 2/3] Adding label and description to grid items --- packages/dataviews/src/view-grid.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index cbc9a1d66fff4..c651d0638197c 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -144,6 +144,8 @@ function GridItem( { useContext( GridContext ); const itemId = getItemId( item ); const id = `${ baseId }-item-${ itemId }`; + const labelId = `${ id }--label`; + const descriptionId = `${ id }--description`; const isSelected = selection.includes( itemId ); const rtl = isRTL(); const movementMap = useMemo( @@ -164,8 +166,9 @@ function GridItem( { spacing={ 0 } key={ itemId } id={ id } + aria-labelledby={ labelId } + aria-describedby={ descriptionId } role="gridcell" - aria-label={ primaryField?.getValue( { item } ) } className={ classnames( 'dataviews-view-grid__card', { 'is-selected': hasBulkAction && isSelected, } ) } @@ -217,12 +220,19 @@ function GridItem( { primaryField={ primaryField } disabled={ ! hasBulkAction } /> - + { primaryField?.render( { item } ) } - + { visibleFields.map( ( field ) => { const renderedValue = field.render( { item, From ce05e15dda5e2360f02304b4ead31d97606e2fdc Mon Sep 17 00:00:00 2001 From: Andrew Hayward Date: Tue, 12 Mar 2024 20:03:42 +0000 Subject: [PATCH 3/3] Refactoring row rendering to prevent layout changes --- packages/dataviews/src/style.scss | 6 +---- packages/dataviews/src/view-grid.js | 35 ++++++++++++++++++++--------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index e9f55664577a3..c490fde0c7752 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -267,17 +267,13 @@ margin: 0 $grid-unit-40 $grid-unit-30; position: relative; - .dataviews-view-grid__rows { + .dataviews-view-grid__cells { display: grid; grid-template-columns: repeat(auto-fill, minmax(17em, 1fr)); grid-template-rows: max-content; gap: 1em; } - .dataviews-view-grid__row { - display: contents; - } - .dataviews-view-grid__card { border-radius: $radius-block-ui * 2; border: 1px solid $gray-200; diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index c651d0638197c..ec688605f1398 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -19,6 +19,7 @@ import { useResizeObserver, } from '@wordpress/compose'; import { + Children, createContext, useContext, useEffect, @@ -97,33 +98,47 @@ function GridRows( { baseId, gridRef, children } ) { Array.from( { length: Math.ceil( children?.length / columnCount ) }, ( _, index ) => - children.slice( - index * columnCount, - ( index + 1 ) * columnCount - ) + Children.toArray( children ) + .slice( + index * columnCount, + ( index + 1 ) * columnCount + ) + .map( ( { key } ) => `${ baseId }-item-${ key }` ) + .join( ' ' ) ), - [ children, columnCount ] + [ baseId, children, columnCount ] ); return useMemo( () => ( <> { resizeListener } +
+ { Children.map( children, ( child, index ) => ( + } + id={ `${ baseId }-row-${ Math.floor( + index / columnCount + ) }` } + > + { child } + + ) ) } +
{ rows.map( ( row, index ) => ( - - { row } - + aria-owns={ row } + /> ) ) }
), - [ baseId, resizeListener, rows ] + [ baseId, children, columnCount, resizeListener, rows ] ); }