Skip to content

Commit

Permalink
Implement busy states on the toolbar
Browse files Browse the repository at this point in the history
  • Loading branch information
jorgefilipecosta committed Apr 19, 2024
1 parent fb6d46f commit 324614f
Show file tree
Hide file tree
Showing 5 changed files with 409 additions and 33 deletions.
224 changes: 224 additions & 0 deletions packages/dataviews/src/bulk-actions-toolbar.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
/**
* WordPress dependencies
*/
import {
ToolbarButton,
Toolbar,
ToolbarGroup,
__unstableMotion as motion,
__unstableAnimatePresence as AnimatePresence,
} from '@wordpress/components';
import { useMemo, useState, useRef } from '@wordpress/element';
import { _n, sprintf, __ } from '@wordpress/i18n';
import { closeSmall } from '@wordpress/icons';
import { useReducedMotion } from '@wordpress/compose';

/**
* Internal dependencies
*/
import { ActionWithModal } from './item-actions';

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( { action, onClick, items, isBusy } ) {
const isDisabled = useMemo( () => {
return isBusy || items.some( ( item ) => ! action.isEligible( item ) );
}, [ action, items, isBusy ] );
return (
<ToolbarButton
disabled={ isDisabled }
label={ action.label }
icon={ action.icon }
isDestructive={ action.isDestructive }
size="compact"
onClick={ onClick }
isBusy={ isBusy }
isDisabled={ isDisabled }
/>
);
}

const EMPTY_ARRAY = [];

function renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
) {
return (
<>
<ToolbarGroup>
<div className="dataviews-bulk-actions__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 ) => {
if ( !! action.RenderModal ) {
return (
<ActionWithModal
key={ action.id }
action={ action }
items={ selectedItems }
ActionTrigger={ ActionTrigger }
isBusy={ actionInProgress === action.id }
onActionStart={ () => {
setActionInProgress( action.id );
} }
onActionPerformed={ () => {
setActionInProgress( null );
} }
/>
);
}
return (
<ActionTrigger
key={ action.id }
action={ action }
items={ selectedItems }
onClick={ () => {
setActionInProgress( action.id );
action.callback( selectedItems, () => {
setActionInProgress( action.id );
} );
} }
isBusy={ actionInProgress === action.id }
/>
);
} ) }
</ToolbarGroup>
<ToolbarGroup>
<ToolbarButton
icon={ closeSmall }
showTooltip
label={ __( 'Cancel' ) }
isDisabled={ !! actionInProgress }
onClick={ () => {
setSelection( EMPTY_ARRAY );
} }
/>
</ToolbarGroup>
</>
);
}

function ToolbarContent( {
selection,
actionsToShow,
selectedItems,
setSelection,
} ) {
const [ actionInProgress, setActionInProgress ] = useState( null );
const buttons = useRef( null );
if ( ! actionInProgress ) {
if ( buttons.current ) {
buttons.current = null;
}
return renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
);
} else if ( ! buttons.current ) {
buttons.current = renderToolbarContent(
selection,
actionsToShow,
selectedItems,
actionInProgress,
setActionInProgress,
setSelection
);
}
return buttons.current;
}

export default function BulkActionsToolbar( {
data,
selection,
actions = EMPTY_ARRAY,
setSelection,
getItemId,
} ) {
const isReducedMotion = useReducedMotion();
const selectedItems = useMemo( () => {
return data.filter( ( item ) =>
selection.includes( getItemId( item ) )
);
}, [ selection, data, getItemId ] );

const actionsToShow = useMemo(
() =>
actions.filter( ( action ) => {
return (
action.supportsBulk &&
action.icon &&
selectedItems.some( ( item ) => action.isEligible( item ) )
);
} ),
[ 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 label={ __( 'Bulk actions' ) }>
<div className="dataviews-bulk-actions-toolbar-wrapper">
<ToolbarContent
selection={ selection }
actionsToShow={ actionsToShow }
selectedItems={ selectedItems }
setSelection={ setSelection }
/>
</div>
</Toolbar>
</motion.div>
</AnimatePresence>
);
}
12 changes: 12 additions & 0 deletions packages/dataviews/src/dataviews.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import Search from './search';
import { VIEW_LAYOUTS, LAYOUT_TABLE, LAYOUT_GRID } from './constants';
import BulkActions from './bulk-actions';
import { normalizeFields } from './normalize-fields';
import BulkActionsToolbar from './bulk-actions-toolbar';

const defaultGetItemId = ( item ) => item.id;
const defaultOnSelectionChange = () => {};
Expand Down Expand Up @@ -81,6 +82,7 @@ export default function DataViews( {
actions,
data
);
console.log({hasPossibleBulkAction});
return (
<div className="dataviews-wrapper">
<HStack
Expand Down Expand Up @@ -142,6 +144,16 @@ export default function DataViews( {
onChangeView={ onChangeView }
paginationInfo={ paginationInfo }
/>
{ [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) &&
hasPossibleBulkAction && (
<BulkActionsToolbar
data={ data }
actions={ actions }
selection={ selection }
setSelection={ setSelection }
getItemId={ getItemId }
/>
) }
</div>
);
}
23 changes: 18 additions & 5 deletions packages/dataviews/src/item-actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,22 @@ function DropdownMenuItemTrigger( { action, onClick } ) {
);
}

function ActionWithModal( { action, item, ActionTrigger } ) {
export function ActionWithModal( {
action,
items,
ActionTrigger,
onActionStart,
onActionPerformed,
isBusy,
} ) {
const [ isModalOpen, setIsModalOpen ] = useState( false );
const actionTriggerProps = {
action,
onClick: () => setIsModalOpen( true ),
onClick: () => {
setIsModalOpen( true );
},
items,
isBusy,
};
const { RenderModal, hideModalHeader } = action;
return (
Expand All @@ -69,8 +80,10 @@ function ActionWithModal( { action, item, ActionTrigger } ) {
) }` }
>
<RenderModal
items={ [ item ] }
items={ items }
closeModal={ () => setIsModalOpen( false ) }
onActionStart={ onActionStart }
onActionPerformed={ onActionPerformed }
/>
</Modal>
) }
Expand All @@ -87,7 +100,7 @@ function ActionsDropdownMenuGroup( { actions, item } ) {
<ActionWithModal
key={ action.id }
action={ action }
item={ item }
items={ [ item ] }
ActionTrigger={ DropdownMenuItemTrigger }
/>
);
Expand Down Expand Up @@ -139,7 +152,7 @@ export default function ItemActions( { item, actions, isCompact } ) {
<ActionWithModal
key={ action.id }
action={ action }
item={ item }
items={ [ item ] }
ActionTrigger={ ButtonTrigger }
/>
);
Expand Down
37 changes: 37 additions & 0 deletions packages/dataviews/src/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -790,3 +790,40 @@
}
}
}


.dataviews-bulk-actions-toolbar-wrapper {
display: flex;
flex-grow: 1;
width: 100%;
}

.dataviews-bulk-actions {
position: absolute;
display: flex;
flex-direction: column;
align-content: center;
flex-wrap: wrap;
width: 100%;
bottom: $grid-unit-30;

.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__selection-count {
display: flex;
align-items: center;
margin: 0 $grid-unit-10 0 $grid-unit-15;
color: $gray-700;
}
}
Loading

0 comments on commit 324614f

Please sign in to comment.