diff --git a/packages/block-editor/src/components/block-draggable/index.js b/packages/block-editor/src/components/block-draggable/index.js
index 8f960f4538a6c9..b75a3a1c2cecb0 100644
--- a/packages/block-editor/src/components/block-draggable/index.js
+++ b/packages/block-editor/src/components/block-draggable/index.js
@@ -4,8 +4,42 @@
import { Draggable } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect, useRef } from '@wordpress/element';
+import { getScrollContainer } from '@wordpress/dom';
-const BlockDraggable = ( { children, clientIds, cloneClassname } ) => {
+const SCROLL_INACTIVE_DISTANCE_PX = 50;
+const SCROLL_INTERVAL_MS = 25;
+const PIXELS_PER_SECOND_PER_DISTANCE = 5;
+const VELOCITY_MULTIPLIER =
+ PIXELS_PER_SECOND_PER_DISTANCE * ( SCROLL_INTERVAL_MS / 1000 );
+
+function startScrollingY( nodeRef, velocityRef ) {
+ return setInterval( () => {
+ if ( nodeRef.current && velocityRef.current ) {
+ const newTop = nodeRef.current.scrollTop + velocityRef.current;
+
+ nodeRef.current.scroll( {
+ top: newTop,
+ // behavior: 'smooth' // seems to hurt performance, better to use a small scroll interval
+ } );
+ }
+ }, SCROLL_INTERVAL_MS );
+}
+
+function getVerticalScrollParent( node ) {
+ if ( node === null ) {
+ return null;
+ }
+
+ return getScrollContainer( node );
+}
+
+const BlockDraggable = ( {
+ children,
+ clientIds,
+ cloneClassname,
+ onDragStart,
+ onDragEnd,
+} ) => {
const { srcRootClientId, index, isDraggable } = useSelect(
( select ) => {
const {
@@ -30,6 +64,14 @@ const BlockDraggable = ( { children, clientIds, cloneClassname } ) => {
[ clientIds ]
);
const isDragging = useRef( false );
+
+ // @todo - do this for horizontal scroll
+ const dragStartY = useRef( null );
+ const velocityY = useRef( null );
+ const scrollParentY = useRef( null );
+
+ const scrollEditorInterval = useRef( null );
+
const { startDraggingBlocks, stopDraggingBlocks } = useDispatch(
'core/block-editor'
);
@@ -40,6 +82,11 @@ const BlockDraggable = ( { children, clientIds, cloneClassname } ) => {
if ( isDragging.current ) {
stopDraggingBlocks();
}
+
+ if ( scrollEditorInterval.current ) {
+ clearInterval( scrollEditorInterval.current );
+ scrollEditorInterval.current = null;
+ }
};
}, [] );
@@ -60,13 +107,51 @@ const BlockDraggable = ( { children, clientIds, cloneClassname } ) => {
cloneClassname={ cloneClassname }
elementId={ blockElementId }
transferData={ transferData }
- onDragStart={ () => {
+ onDragStart={ ( event ) => {
startDraggingBlocks();
isDragging.current = true;
+ dragStartY.current = event.clientY;
+
+ // find nearest parent(s) to scroll
+ scrollParentY.current = getVerticalScrollParent(
+ document.getElementById( blockElementId )
+ );
+ scrollEditorInterval.current = startScrollingY(
+ scrollParentY,
+ velocityY
+ );
+ if ( onDragStart ) {
+ onDragStart();
+ }
+ } }
+ onDragOver={ ( event ) => {
+ const distanceY = event.clientY - dragStartY.current;
+ if ( distanceY > SCROLL_INACTIVE_DISTANCE_PX ) {
+ velocityY.current =
+ VELOCITY_MULTIPLIER *
+ ( distanceY - SCROLL_INACTIVE_DISTANCE_PX );
+ } else if ( distanceY < -SCROLL_INACTIVE_DISTANCE_PX ) {
+ velocityY.current =
+ VELOCITY_MULTIPLIER *
+ ( distanceY + SCROLL_INACTIVE_DISTANCE_PX );
+ } else {
+ velocityY.current = 0;
+ }
} }
onDragEnd={ () => {
stopDraggingBlocks();
isDragging.current = false;
+ dragStartY.current = null;
+ scrollParentY.current = null;
+
+ if ( scrollEditorInterval.current ) {
+ clearInterval( scrollEditorInterval.current );
+ scrollEditorInterval.current = null;
+ }
+
+ if ( onDragEnd ) {
+ onDragEnd();
+ }
} }
>
{ ( { onDraggableStart, onDraggableEnd } ) => {
diff --git a/packages/block-editor/src/components/block-list/block-contextual-toolbar.js b/packages/block-editor/src/components/block-list/block-contextual-toolbar.js
index 839cb84e87769d..c9faf2079a628d 100644
--- a/packages/block-editor/src/components/block-list/block-contextual-toolbar.js
+++ b/packages/block-editor/src/components/block-list/block-contextual-toolbar.js
@@ -39,7 +39,10 @@ function BlockContextualToolbar( { focusOnMount, ...props } ) {
aria-label={ __( 'Block tools' ) }
{ ...props }
>
-
+
);
diff --git a/packages/block-editor/src/components/block-list/block-popover.js b/packages/block-editor/src/components/block-list/block-popover.js
index fa29a157b762ab..2f7c4bb193b0c5 100644
--- a/packages/block-editor/src/components/block-list/block-popover.js
+++ b/packages/block-editor/src/components/block-list/block-popover.js
@@ -31,6 +31,7 @@ function selector( select ) {
isCaretWithinFormattedText,
getSettings,
getLastMultiSelectedBlockClientId,
+ isDraggingBlocks,
} = select( 'core/block-editor' );
return {
isNavigationMode: isNavigationMode(),
@@ -40,6 +41,7 @@ function selector( select ) {
hasMultiSelection: hasMultiSelection(),
hasFixedToolbar: getSettings().hasFixedToolbar,
lastClientId: getLastMultiSelectedBlockClientId(),
+ isDragging: isDraggingBlocks(),
};
}
@@ -60,6 +62,7 @@ function BlockPopover( {
hasMultiSelection,
hasFixedToolbar,
lastClientId,
+ isDragging,
} = useSelect( selector, [] );
const isLargeViewport = useViewportMatch( 'medium' );
const [ isToolbarForced, setIsToolbarForced ] = useState( false );
@@ -96,7 +99,8 @@ function BlockPopover( {
! shouldShowBreadcrumb &&
! shouldShowContextualToolbar &&
! isToolbarForced &&
- ! showEmptyBlockSideInserter
+ ! showEmptyBlockSideInserter &&
+ ! isDragging
) {
return null;
}
@@ -136,6 +140,14 @@ function BlockPopover( {
setIsInserterShown( false );
}
+ function onDragStart() {
+ setIsToolbarForced( true );
+ }
+
+ function onDragEnd() {
+ setIsToolbarForced( false );
+ }
+
// Position above the anchor, pop out towards the right, and position in the
// left corner. For the side inserter, pop out towards the left, and
// position in the right corner.
@@ -165,8 +177,10 @@ function BlockPopover( {
onBlur={ () => setIsToolbarForced( false ) }
shouldAnchorIncludePadding
// Popover calculates the width once. Trigger a reset by remounting
- // the component.
- key={ shouldShowContextualToolbar }
+ // the component. We include both shouldShowContextualToolbar and isToolbarForced
+ // in the key to prevent the component being unmounted unexpectedly when isToolbarForced = true,
+ // e.g. during drag and drop
+ key={ shouldShowContextualToolbar || isToolbarForced }
>
{ ( shouldShowContextualToolbar || isToolbarForced ) && (
) }
{ shouldShowBreadcrumb && (
diff --git a/packages/components/src/draggable/README.md b/packages/components/src/draggable/README.md
index 28bdee89faf23e..adfe59d4d0b04b 100644
--- a/packages/components/src/draggable/README.md
+++ b/packages/components/src/draggable/README.md
@@ -24,7 +24,15 @@ Arbitrary data object attached to the drag and drop event.
### onDragStart
-A function to be called when dragging starts.
+A function called when dragging starts. This callback receives the `event` object from the `dragstart` event as its first parameter.
+
+- Type: `Function`
+- Required: No
+- Default: `noop`
+
+### onDragOver
+
+A function called when the element being dragged is dragged over a valid drop target. This callback receives the `event` object from the `dragover` event as its first parameter.
- Type: `Function`
- Required: No
@@ -32,7 +40,7 @@ A function to be called when dragging starts.
### onDragEnd
-A function to be called when dragging ends.
+A function called when dragging ends. This callback receives the `event` object from the `dragend` event as its first parameter.
- Type: `Function`
- Required: No
diff --git a/packages/components/src/draggable/index.js b/packages/components/src/draggable/index.js
index 0a44ebeb70d8b6..7b74a253ea288e 100644
--- a/packages/components/src/draggable/index.js
+++ b/packages/components/src/draggable/index.js
@@ -38,7 +38,11 @@ class Draggable extends Component {
event.preventDefault();
this.resetDragState();
- this.props.setTimeout( onDragEnd );
+
+ // Allow the Synthetic Event to be accessed from asynchronous code.
+ // https://reactjs.org/docs/events.html#event-pooling
+ event.persist();
+ this.props.setTimeout( onDragEnd.bind( this, event ) );
}
/**
@@ -61,6 +65,12 @@ class Draggable extends Component {
// Update cursor coordinates.
this.cursorLeft = event.clientX;
this.cursorTop = event.clientY;
+
+ const { onDragOver = noop } = this.props;
+
+ // The `event` from `onDragOver` is not a SyntheticEvent
+ // and so it doesn't require `event.persist()`.
+ this.props.setTimeout( onDragOver.bind( this, event ) );
}
/**
@@ -150,7 +160,10 @@ class Draggable extends Component {
document.body.classList.add( 'is-dragging-components-draggable' );
document.addEventListener( 'dragover', this.onDragOver );
- this.props.setTimeout( onDragStart );
+ // Allow the Synthetic Event to be accessed from asynchronous code.
+ // https://reactjs.org/docs/events.html#event-pooling
+ event.persist();
+ this.props.setTimeout( onDragStart.bind( this, event ) );
}
/**
@@ -165,6 +178,9 @@ class Draggable extends Component {
this.cloneWrapper = null;
}
+ this.cursorLeft = null;
+ this.cursorTop = null;
+
// Reset cursor.
document.body.classList.remove( 'is-dragging-components-draggable' );
}
diff --git a/packages/components/src/draggable/style.scss b/packages/components/src/draggable/style.scss
index 6cbc4ff09db4da..076207caed7fc0 100644
--- a/packages/components/src/draggable/style.scss
+++ b/packages/components/src/draggable/style.scss
@@ -16,6 +16,7 @@ body.is-dragging-components-draggable {
background: transparent;
pointer-events: none;
z-index: z-index(".components-draggable__clone");
+ opacity: 0.7;
> * {
// This needs specificity as a theme is meant to define these by default.