Skip to content

Commit

Permalink
New bulk actions UI
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgefilipecosta committed Aug 6, 2024
1 parent fa98fb5 commit 499d953
Show file tree
Hide file tree
Showing 10 changed files with 174 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ToolbarGroup,
__unstableMotion as motion,
__unstableAnimatePresence as AnimatePresence,
__experimentalHStack as HStack,
} from '@wordpress/components';
import { useMemo, useState, useRef, useContext } from '@wordpress/element';
import { _n, sprintf, __ } from '@wordpress/i18n';
Expand All @@ -17,7 +18,10 @@ import { useRegistry } from '@wordpress/data';
/**
* Internal dependencies
*/
import { useSomeItemHasAPossibleBulkAction } from '../dataviews-bulk-actions';
import {
useSomeItemHasAPossibleBulkAction,
BulkSelectionCheckbox,
} from '../dataviews-bulk-actions';
import DataViewsContext from '../dataviews-context';
import { ActionWithModal } from '../dataviews-item-actions';
import { LAYOUT_GRID, LAYOUT_TABLE } from '../../constants';
Expand All @@ -34,30 +38,12 @@ interface ActionButtonProps< Item > {

interface ToolbarContentProps< Item > {
selection: string[];
actionsToShow: Action< Item >[];
selectedItems: Item[];
onChangeSelection: SetSelection;
data: Item[];
actions: Action< Item >[];
getItemId: ( item: Item ) => string;
}

const SNACKBAR_VARIANTS = {
init: {
bottom: -48,
},
open: {
bottom: 24,
transition: {
bottom: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
},
},
exit: {
opacity: 0,
bottom: 24,
transition: {
opacity: { type: 'tween', duration: 0.2, ease: [ 0, 0, 0.2, 1 ] },
},
},
};

function ActionTrigger< Item >( {
action,
onClick,
Expand Down Expand Up @@ -120,104 +106,64 @@ function ActionButton< Item >( {
);
}

function renderToolbarContent< Item >(
selection: string[],
actionsToShow: Action< Item >[],
selectedItems: Item[],
actionInProgress: string | null,
setActionInProgress: ( actionId: string | null ) => void,
onChangeSelection: SetSelection
) {
return (
<>
<ToolbarGroup>
<div className="dataviews-bulk-actions-toolbar__selection-count">
{ selection.length === 1
? __( '1 item selected' )
: sprintf(
// translators: %s: Total number of selected items.
_n(
'%s item selected',
'%s items selected',
selection.length
),
selection.length
) }
</div>
</ToolbarGroup>
<ToolbarGroup>
{ actionsToShow.map( ( action ) => {
return (
<ActionButton
key={ action.id }
action={ action }
selectedItems={ selectedItems }
actionInProgress={ actionInProgress }
setActionInProgress={ setActionInProgress }
/>
);
} ) }
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton
icon={ closeSmall }
showTooltip
tooltipPosition="top"
label={ __( 'Cancel' ) }
disabled={ !! actionInProgress }
onClick={ () => {
onChangeSelection( EMPTY_ARRAY );
} }
/>
</ToolbarGroup>
</>
);
}

function ToolbarContent< Item >( {
selection,
actionsToShow,
selectedItems,
actions,
onChangeSelection,
data,
getItemId,
}: ToolbarContentProps< Item > ) {
const [ actionInProgress, setActionInProgress ] = useState< string | null >(
null
const bulkActions = useMemo(
() => actions.filter( ( action ) => action.supportsBulk ),
[ actions ]
);
const buttons = useRef< JSX.Element | null >( null );
if ( ! actionInProgress ) {
if ( buttons.current ) {
buttons.current = null;
}
return renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
onChangeSelection
);
} else if ( ! buttons.current ) {
buttons.current = renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
onChangeSelection
const selectableItems = useMemo( () => {
return data.filter( ( item ) => {
return bulkActions.some(
( action ) => ! action.isEligible || action.isEligible( item )
);
} );
}, [ data, bulkActions ] );

const selectedItems = useMemo( () => {
return data.filter(
( item ) =>
selection.includes( getItemId( item ) ) &&
selectableItems.includes( item )
);
}
return buttons.current;
}, [ selection, data, getItemId, selectableItems ] );
const countToShow =
selectedItems.length > 0
? selectedItems.length
: selectableItems.length;
return (
<HStack>
<BulkSelectionCheckbox
selection={ selection }
onChangeSelection={ onChangeSelection }
data={ data }
actions={ actions }
getItemId={ getItemId }
/>
<span>
{ sprintf(
/* translators: %d: number of items. */
_n( '%d item', '%d items', countToShow ),
countToShow
) }
</span>
</HStack>
);
}

function _BulkActionsToolbar() {
export default function BulkActionsToolbar() {
const {
data,
selection,
actions = EMPTY_ARRAY,
onChangeSelection,
getItemId,
} = useContext( DataViewsContext );
const isReducedMotion = useReducedMotion();
const selectedItems = useMemo( () => {
return data.filter( ( item ) =>
selection.includes( getItemId( item ) )
Expand All @@ -239,50 +185,13 @@ function _BulkActionsToolbar() {
[ actions, selectedItems ]
);

if (
( selection && selection.length === 0 ) ||
actionsToShow.length === 0
) {
return null;
}

return (
<AnimatePresence>
<motion.div
layout={ ! isReducedMotion } // See https://www.framer.com/docs/animation/#layout-animations
initial="init"
animate="open"
exit="exit"
variants={ isReducedMotion ? undefined : SNACKBAR_VARIANTS }
className="dataviews-bulk-actions-toolbar"
>
<Toolbar label={ __( 'Bulk actions' ) }>
<div className="dataviews-bulk-actions-toolbar__wrapper">
<ToolbarContent
selection={ selection }
actionsToShow={ actionsToShow }
selectedItems={ selectedItems }
onChangeSelection={ onChangeSelection }
/>
</div>
</Toolbar>
</motion.div>
</AnimatePresence>
);
}

export default function BulkActionsToolbar() {
const { data, actions = [], view } = useContext( DataViewsContext );
const hasPossibleBulkAction = useSomeItemHasAPossibleBulkAction(
actions,
data
<ToolbarContent
selection={ selection }
onChangeSelection={ onChangeSelection }
data={ data }
actions={ actions }
getItemId={ getItemId }
/>
);
if (
! [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) ||
! hasPossibleBulkAction
) {
return null;
}

return <_BulkActionsToolbar />;
}
Original file line number Diff line number Diff line change
@@ -1,45 +1,5 @@
.dataviews-bulk-actions-toolbar {
position: sticky;
.dataviews-bulk-actions-toolbar__selection-count {
display: flex;
flex-direction: column;
align-content: center;
flex-wrap: wrap;
width: fit-content;
margin-left: auto;
margin-right: auto;
bottom: $grid-unit-30;
z-index: z-index(".dataviews-bulk-actions-toolbar");

.components-accessible-toolbar {
border-color: $gray-300;
box-shadow: $shadow-popover;

.components-toolbar-group {
border-color: $gray-200;

&:last-child {
border: 0;
}
}
}

.dataviews-bulk-actions-toolbar__selection-count {
display: flex;
align-items: center;
margin: 0 $grid-unit-10 0 $grid-unit-10;
}
}

.dataviews-bulk-actions-toolbar__wrapper {
display: flex;
flex-grow: 1;
width: 100%;

.components-toolbar-group {
align-items: center;
}

.components-button.is-busy {
max-height: $button-size;
}
align-items: center;
margin: 0 $grid-unit-10 0 $grid-unit-10;
}
54 changes: 54 additions & 0 deletions packages/dataviews/src/components/dataviews-bulk-actions/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
privateApis as componentsPrivateApis,
Button,
Modal,
CheckboxControl,
} from '@wordpress/components';
import { __, sprintf, _n } from '@wordpress/i18n';
import { useMemo, useState, useCallback, useContext } from '@wordpress/element';
Expand All @@ -17,6 +18,7 @@ import DataViewsContext from '../dataviews-context';
import { LAYOUT_TABLE, LAYOUT_GRID } from '../../constants';
import { unlock } from '../../lock-unlock';
import type { Action, ActionModal } from '../../types';
import type { SetSelection } from '../../private-types';

const {
DropdownMenuV2: DropdownMenu,
Expand Down Expand Up @@ -74,6 +76,58 @@ export function useSomeItemHasAPossibleBulkAction< Item >(
}, [ actions, data ] );
}

interface BulkSelectionCheckboxProps< Item > {
selection: string[];
onChangeSelection: SetSelection;
data: Item[];
actions: Action< Item >[];
getItemId: ( item: Item ) => string;
}

export function BulkSelectionCheckbox< Item >( {
selection,
onChangeSelection,
data,
actions,
getItemId,
}: BulkSelectionCheckboxProps< Item > ) {
const selectableItems = useMemo( () => {
return data.filter( ( item ) => {
return actions.some(
( action ) =>
action.supportsBulk &&
( ! action.isEligible || action.isEligible( item ) )
);
} );
}, [ data, actions ] );
const selectedItems = data.filter(
( item ) =>
selection.includes( getItemId( item ) ) &&
selectableItems.includes( item )
);
const areAllSelected = selectedItems.length === selectableItems.length;
return (
<CheckboxControl
className="dataviews-view-table-selection-checkbox"
__nextHasNoMarginBottom
checked={ areAllSelected }
indeterminate={ ! areAllSelected && !! selectedItems.length }
onChange={ () => {
if ( areAllSelected ) {
onChangeSelection( [] );
} else {
onChangeSelection(
selectableItems.map( ( item ) => getItemId( item ) )
);
}
} }
aria-label={
areAllSelected ? __( 'Deselect all' ) : __( 'Select all' )
}
/>
);
}

function ActionWithModal< Item >( {
action,
selectedItems,
Expand Down
Loading

0 comments on commit 499d953

Please sign in to comment.