diff --git a/packages/dataviews/src/bulk-actions-toolbar.js b/packages/dataviews/src/bulk-actions-toolbar.js
new file mode 100644
index 00000000000000..1b9bb5f2ffed24
--- /dev/null
+++ b/packages/dataviews/src/bulk-actions-toolbar.js
@@ -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 (
+
+ );
+}
+
+const EMPTY_ARRAY = [];
+
+function renderToolbarContent(
+ selection,
+ actionsToShow,
+ selectedItems,
+ actionInProgress,
+ setActionInProgress,
+ setSelection
+) {
+ return (
+ <>
+
+
+ { 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
+ ) }
+
+
+
+ { actionsToShow.map( ( action ) => {
+ if ( !! action.RenderModal ) {
+ return (
+ {
+ setActionInProgress( action.id );
+ } }
+ onActionPerformed={ () => {
+ setActionInProgress( null );
+ } }
+ />
+ );
+ }
+ return (
+ {
+ setActionInProgress( action.id );
+ action.callback( selectedItems, () => {
+ setActionInProgress( action.id );
+ } );
+ } }
+ isBusy={ actionInProgress === action.id }
+ />
+ );
+ } ) }
+
+
+ {
+ setSelection( EMPTY_ARRAY );
+ } }
+ />
+
+ >
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/packages/dataviews/src/dataviews.js b/packages/dataviews/src/dataviews.js
index d67115deb3d6b2..0d705bf1709902 100644
--- a/packages/dataviews/src/dataviews.js
+++ b/packages/dataviews/src/dataviews.js
@@ -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 = () => {};
@@ -81,6 +82,7 @@ export default function DataViews( {
actions,
data
);
+ console.log({hasPossibleBulkAction});
return (
+ { [ LAYOUT_TABLE, LAYOUT_GRID ].includes( view.type ) &&
+ hasPossibleBulkAction && (
+
+ ) }
);
}
diff --git a/packages/dataviews/src/item-actions.js b/packages/dataviews/src/item-actions.js
index db4da0d4924896..2d928cdbd451b5 100644
--- a/packages/dataviews/src/item-actions.js
+++ b/packages/dataviews/src/item-actions.js
@@ -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 (
@@ -69,8 +80,10 @@ function ActionWithModal( { action, item, ActionTrigger } ) {
) }` }
>
setIsModalOpen( false ) }
+ onActionStart={ onActionStart }
+ onActionPerformed={ onActionPerformed }
/>
) }
@@ -87,7 +100,7 @@ function ActionsDropdownMenuGroup( { actions, item } ) {
);
@@ -139,7 +152,7 @@ export default function ItemActions( { item, actions, isCompact } ) {
);
diff --git a/packages/dataviews/src/style.scss b/packages/dataviews/src/style.scss
index d2d61ee383173b..95c0f4f22c92fc 100644
--- a/packages/dataviews/src/style.scss
+++ b/packages/dataviews/src/style.scss
@@ -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;
+ }
+}
diff --git a/packages/editor/src/components/post-actions/actions.js b/packages/editor/src/components/post-actions/actions.js
index a6ff1f77486c99..ef4c26a1960a76 100644
--- a/packages/editor/src/components/post-actions/actions.js
+++ b/packages/editor/src/components/post-actions/actions.js
@@ -43,7 +43,13 @@ const trashPostAction = {
},
supportsBulk: true,
hideModalHeader: true,
- RenderModal: ( { items: posts, closeModal, onActionPerformed } ) => {
+ RenderModal: ( {
+ items: posts,
+ closeModal,
+ onActionStart,
+ onActionPerformed,
+ } ) => {
+ const [ isBusy, setIsBusy ] = useState( false );
const { createSuccessNotice, createErrorNotice } =
useDispatch( noticesStore );
const { deleteEntityRecord } = useDispatch( coreStore );
@@ -67,12 +73,20 @@ const trashPostAction = {
) }
-