diff --git a/packages/base-styles/_mixins.scss b/packages/base-styles/_mixins.scss index 0333a8bb6adf9..109926ebf411a 100644 --- a/packages/base-styles/_mixins.scss +++ b/packages/base-styles/_mixins.scss @@ -297,6 +297,16 @@ } } } + + &[aria-disabled="true"], + &:disabled { + background: $gray-100; + border-color: $gray-300; + cursor: default; + + // Override style inherited from wp-admin. Required to avoid degraded appearance on different backgrounds. + opacity: 1; + } } @mixin radio-control { diff --git a/packages/dataviews/src/bulk-actions.js b/packages/dataviews/src/bulk-actions.js index 9fd9f628286e0..5e4139fe0622e 100644 --- a/packages/dataviews/src/bulk-actions.js +++ b/packages/dataviews/src/bulk-actions.js @@ -7,7 +7,7 @@ import { Modal, } from '@wordpress/components'; import { __, sprintf, _n } from '@wordpress/i18n'; -import { useMemo, useState, useCallback } from '@wordpress/element'; +import { useMemo, useState, useCallback, useEffect } from '@wordpress/element'; /** * Internal dependencies @@ -21,6 +21,24 @@ const { DropdownMenuSeparatorV2: DropdownMenuSeparator, } = unlock( componentsPrivateApis ); +export function useHasAPossibleBulkAction( actions, item ) { + return useMemo( () => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + }, [ actions, item ] ); +} + +export function useSomeItemHasAPossibleBulkAction( actions, data ) { + return useMemo( () => { + return data.some( ( item ) => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + } ); + }, [ actions, data ] ); +} + function ActionWithModal( { action, selectedItems, @@ -107,15 +125,47 @@ export default function BulkActions( { () => actions.filter( ( action ) => action.supportsBulk ), [ actions ] ); - const areAllSelected = selection && selection.length === data.length; const [ isMenuOpen, onMenuOpenChange ] = useState( false ); const [ actionWithModal, setActionWithModal ] = useState(); + const selectableItems = useMemo( () => { + return data.filter( ( item ) => { + return bulkActions.some( ( action ) => action.isEligible( item ) ); + } ); + }, [ data, bulkActions ] ); + + const numberSelectableItems = selectableItems.length; + const areAllSelected = + selection && selection.length === numberSelectableItems; + const selectedItems = useMemo( () => { return data.filter( ( item ) => selection.includes( getItemId( item ) ) ); }, [ selection, data, getItemId ] ); + const hasNonSelectableItemSelected = useMemo( () => { + return selectedItems.some( ( item ) => { + return ! selectableItems.includes( item ); + } ); + }, [ selectedItems, selectableItems ] ); + useEffect( () => { + if ( hasNonSelectableItemSelected ) { + onSelectionChange( + selectedItems.filter( ( selectedItem ) => { + return selectableItems.some( ( item ) => { + return getItemId( selectedItem ) === getItemId( item ); + } ); + } ) + ); + } + }, [ + hasNonSelectableItemSelected, + selectedItems, + selectableItems, + getItemId, + onSelectionChange, + ] ); + if ( bulkActions.length === 0 ) { return null; } @@ -157,9 +207,9 @@ export default function BulkActions( { disabled={ areAllSelected } hideOnClick={ false } onClick={ () => { - onSelectionChange( data ); + onSelectionChange( selectableItems ); } } - suffix={ data.length } + suffix={ numberSelectableItems } > { __( 'Select all' ) } diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js index 49143d0bb0753..9bcf531006406 100644 --- a/packages/dataviews/src/dataviews.js +++ b/packages/dataviews/src/dataviews.js @@ -20,6 +20,16 @@ import BulkActions from './bulk-actions'; const defaultGetItemId = ( item ) => item.id; const defaultOnSelectionChange = () => {}; +function useSomeItemHasAPossibleBulkAction( actions, data ) { + return useMemo( () => { + return data.some( ( item ) => { + return actions.some( ( action ) => { + return action.supportsBulk && action.isEligible( item ); + } ); + } ); + }, [ actions, data ] ); +} + export default function DataViews( { view, onChangeView, @@ -75,6 +85,11 @@ export default function DataViews( { render: field.render || field.getValue, } ) ); }, [ fields ] ); + + const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction( + actions, + data + ); return (
@@ -103,15 +118,16 @@ export default function DataViews( { setOpenedFilter={ setOpenedFilter } /> - { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && ( - - ) } + { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) && + hasPossibleBulkAction && ( + + ) } { if ( ! isSelected ) { onSelectionChange( diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss index e865ff8a25fed..7e45bec25856f 100644 --- a/packages/dataviews/src/style.scss +++ b/packages/dataviews/src/style.scss @@ -119,7 +119,7 @@ background-color: #f8f8f8; } - .components-checkbox-control__input { + .components-checkbox-control__input.components-checkbox-control__input { opacity: 0; &:checked, diff --git a/packages/dataviews/src/view-grid.js b/packages/dataviews/src/view-grid.js index 44ab1822a6075..3b46a7424dc1b 100644 --- a/packages/dataviews/src/view-grid.js +++ b/packages/dataviews/src/view-grid.js @@ -22,6 +22,8 @@ import { useState } from '@wordpress/element'; import ItemActions from './item-actions'; import SingleSelectionCheckbox from './single-selection-checkbox'; +import { useHasAPossibleBulkAction } from './bulk-actions'; + function GridItem( { selection, data, @@ -34,6 +36,7 @@ function GridItem( { visibleFields, } ) { const [ hasNoPointerEvents, setHasNoPointerEvents ] = useState( false ); + const hasBulkAction = useHasAPossibleBulkAction( actions, item ); const id = getItemId( item ); const isSelected = selection.includes( id ); return ( @@ -41,11 +44,11 @@ function GridItem( { spacing={ 0 } key={ id } className={ classnames( 'dataviews-view-grid__card', { - 'is-selected': isSelected, + 'is-selected': hasBulkAction && isSelected, 'has-no-pointer-events': hasNoPointerEvents, } ) } onMouseDown={ ( event ) => { - if ( event.ctrlKey || event.metaKey ) { + if ( hasBulkAction && ( event.ctrlKey || event.metaKey ) ) { setHasNoPointerEvents( true ); if ( ! isSelected ) { onSelectionChange( @@ -91,6 +94,7 @@ function GridItem( { getItemId={ getItemId } data={ data } primaryField={ primaryField } + disabled={ ! hasBulkAction } /> { primaryField?.render( { item } ) } diff --git a/packages/dataviews/src/view-table.js b/packages/dataviews/src/view-table.js index dc16657703425..9737aa3d46283 100644 --- a/packages/dataviews/src/view-table.js +++ b/packages/dataviews/src/view-table.js @@ -23,6 +23,7 @@ import { useState, Children, Fragment, + useMemo, } from '@wordpress/element'; /** @@ -33,6 +34,10 @@ import { unlock } from './lock-unlock'; import ItemActions from './item-actions'; import { sanitizeOperators } from './utils'; import { ENUMERATION_TYPE, SORTING_DIRECTIONS } from './constants'; +import { + useSomeItemHasAPossibleBulkAction, + useHasAPossibleBulkAction, +} from './bulk-actions'; const { DropdownMenuV2: DropdownMenu, @@ -186,8 +191,20 @@ const HeaderMenu = forwardRef( function HeaderMenu( ); } ); -function BulkSelectionCheckbox( { selection, onSelectionChange, data } ) { - const areAllSelected = selection.length === data.length; +function BulkSelectionCheckbox( { + selection, + onSelectionChange, + data, + actions, +} ) { + const selectableItems = useMemo( () => { + return data.filter( ( item ) => { + return actions.some( + ( action ) => action.supportsBulk && action.isEligible( item ) + ); + } ); + }, [ data, actions ] ); + const areAllSelected = selection.length === selectableItems.length; return ( + { hasBulkActions && ( + +
+ +
+ + ) } + { visibleFields.map( ( field ) => ( + +
+ { field.render( { + item, + } ) } +
+ + ) ) } + { !! actions?.length && ( + + + + ) } + + ); +} + function ViewTable( { view, onChangeView, @@ -219,10 +311,10 @@ function ViewTable( { onSelectionChange, setOpenedFilter, } ) { - const hasBulkActions = actions?.some( ( action ) => action.supportsBulk ); const headerMenuRefs = useRef( new Map() ); const headerMenuToFocusRef = useRef(); const [ nextHeaderMenuToFocus, setNextHeaderMenuToFocus ] = useState(); + const hasBulkActions = useSomeItemHasAPossibleBulkAction( actions, data ); useEffect( () => { if ( headerMenuToFocusRef.current ) { @@ -285,6 +377,7 @@ function ViewTable( { selection={ selection } onSelectionChange={ onSelectionChange } data={ data } + actions={ actions } /> ) } @@ -347,78 +440,19 @@ function ViewTable( { { hasData && usedData.map( ( item, index ) => ( - - { hasBulkActions && ( - -
- -
- - ) } - { visibleFields.map( ( field ) => ( - -
- { field.render( { - item, - } ) } -
- - ) ) } - { !! actions?.length && ( - - - - ) } - + item={ item } + hasBulkActions={ hasBulkActions } + actions={ actions } + id={ getItemId( item ) || index } + visibleFields={ visibleFields } + primaryField={ primaryField } + selection={ selection } + getItemId={ getItemId } + onSelectionChange={ onSelectionChange } + data={ data } + /> ) ) }