-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Hide drop indicator where block isn't allowed to drop. #56843
Changes from 7 commits
a2fd12c
761fb61
4f0513f
e0a9753
43a1bc0
359b5b8
59607c9
9e40e4e
3bf8a0f
5270a64
f8aba09
1da2470
f194767
37096af
e9f3a48
240cd39
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,14 +4,16 @@ | |
import { store as blocksStore } from '@wordpress/blocks'; | ||
import { Draggable } from '@wordpress/components'; | ||
import { useSelect, useDispatch } from '@wordpress/data'; | ||
import { useEffect, useRef } from '@wordpress/element'; | ||
import { useEffect, useRef, useState } from '@wordpress/element'; | ||
|
||
/** | ||
* Internal dependencies | ||
*/ | ||
import BlockDraggableChip from './draggable-chip'; | ||
import useScrollWhenDragging from './use-scroll-when-dragging'; | ||
import { store as blockEditorStore } from '../../store'; | ||
import { __unstableUseBlockRef as useBlockRef } from '../block-list/use-block-props/use-block-refs'; | ||
import { isDropTargetValid } from '../use-block-drop-zone'; | ||
|
||
const BlockDraggable = ( { | ||
children, | ||
|
@@ -20,15 +22,28 @@ const BlockDraggable = ( { | |
onDragStart, | ||
onDragEnd, | ||
} ) => { | ||
const { srcRootClientId, isDraggable, icon } = useSelect( | ||
const { | ||
srcRootClientId, | ||
isDraggable, | ||
icon, | ||
visibleInserter, | ||
getBlockType, | ||
getAllowedBlocks, | ||
draggedBlockNames, | ||
getBlockNamesByClientId, | ||
} = useSelect( | ||
( select ) => { | ||
const { | ||
canMoveBlocks, | ||
getBlockRootClientId, | ||
getBlockName, | ||
getBlockAttributes, | ||
isBlockInsertionPointVisible, | ||
getDraggedBlockClientIds, | ||
getBlockNamesByClientId: _getBlockNamesByClientId, | ||
getAllowedBlocks: _getAllowedBlocks, | ||
} = select( blockEditorStore ); | ||
const { getBlockType, getActiveBlockVariation } = | ||
const { getBlockType: _getBlockType, getActiveBlockVariation } = | ||
select( blocksStore ); | ||
const rootClientId = getBlockRootClientId( clientIds[ 0 ] ); | ||
const blockName = getBlockName( clientIds[ 0 ] ); | ||
|
@@ -40,11 +55,21 @@ const BlockDraggable = ( { | |
return { | ||
srcRootClientId: rootClientId, | ||
isDraggable: canMoveBlocks( clientIds, rootClientId ), | ||
icon: variation?.icon || getBlockType( blockName )?.icon, | ||
icon: variation?.icon || _getBlockType( blockName )?.icon, | ||
visibleInserter: isBlockInsertionPointVisible(), | ||
getBlockType: _getBlockType, | ||
getAllowedBlocks: _getAllowedBlocks, | ||
draggedBlockNames: _getBlockNamesByClientId( | ||
getDraggedBlockClientIds() | ||
), | ||
getBlockNamesByClientId: _getBlockNamesByClientId, | ||
}; | ||
}, | ||
[ clientIds ] | ||
); | ||
|
||
const [ targetClientId, setTargetClientId ] = useState( null ); | ||
|
||
const isDragging = useRef( false ); | ||
const [ startScrolling, scrollOnDragOver, stopScrolling ] = | ||
useScrollWhenDragging(); | ||
|
@@ -61,6 +86,64 @@ const BlockDraggable = ( { | |
}; | ||
}, [] ); | ||
|
||
// Find the root of the editor iframe. | ||
const blockRef = useBlockRef( clientIds[ 0 ] ); | ||
const editorRoot = blockRef.current?.closest( 'body' ); | ||
|
||
// Add a dragover event listener to the editor root to track the blocks being dragged over. | ||
// The listener has to be inside the editor iframe otherwise the target isn't accessible. | ||
// Check if the dragged blocks are allowed inside the target. If not, grey out the draggable. | ||
useEffect( () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this
Then the logic of when to show and hide the insertion point could live in one place, so that the Or is there another reason why this component needs to perform its own separate checks? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, so the crucial bit here is finding the Unfortunately there's not much that can be done to restrict how many times this runs without slowing down the responsiveness of the drag chip to what it's hovering over. The problem here is that, unlike with the drop indicators that exist statically between all blocks so we know where they're placed from the start, we need to find out what the draggable is being dragged over, and afaik the only way to do that is through the event target of a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Ah, gotcha! Thanks for the explanation, that makes sense now 👍
Yeah, it can be really tricky to get the right balance when slowing things down so that it's slow enough for performance reasons, but fast enough to feel responsive to a user! 😅
That sounds about right to me, too. If it winds up being a bit of a rabbithole trying to get it working smoothly, would it be worth splitting out the |
||
if ( ! editorRoot ) { | ||
return; | ||
} | ||
|
||
const onDragOver = ( event ) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this callback might need to be throttled similar to the code in 2023-12-14.10.37.55.mp4There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hrm yeah I'm wondering how best to do that given the inability to use hooks inside the effect callback 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I ran into this issue when testing, it seemed to crash in Safari and I couldn't move the chip any more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oo wait I see there's a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok the throttle seems to make it better! I set it to the same as draggable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a little better, but it's still lagging a bit for me with the throttle at
Would it work to maybe move defining the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So I had a think about this 😄 and realised that we actually don't need to use local state for I also tried increasing the throttle time and it doesn't seem to make things any worse, so might leave it at 200 or so. I just pushed an update, let me know if it improves things at all! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice work! That appears to have fixed up the performance issues for me. Subjectively, without CPU throttling, it's feeling smooth now, and with CPU throttling, it seems much the same as |
||
if ( ! event.target.closest( '[data-block]' ) ) { | ||
return; | ||
} | ||
|
||
const newTargetClientId = event.target | ||
.closest( '[data-block]' ) | ||
.getAttribute( 'data-block' ); | ||
//Only update targetClientId if it has changed | ||
// and if it's a container block | ||
if ( targetClientId !== newTargetClientId ) { | ||
setTargetClientId( newTargetClientId ); | ||
} | ||
|
||
const allowedBlocks = getAllowedBlocks( targetClientId ); | ||
const targetBlockName = getBlockNamesByClientId( [ | ||
targetClientId, | ||
] )[ 0 ]; | ||
const dropTargetValid = isDropTargetValid( | ||
getBlockType, | ||
allowedBlocks, | ||
draggedBlockNames, | ||
targetBlockName | ||
); | ||
|
||
// Update the body class to reflect if drop target is valid. | ||
// This has to be done on the document body because the draggable | ||
// chip is rendered outside of the editor iframe. | ||
if ( ! dropTargetValid && ! visibleInserter ) { | ||
window?.document?.body?.classList?.add( | ||
'block-draggable-invalid-drag-token' | ||
); | ||
} else { | ||
window?.document?.body?.classList?.remove( | ||
'block-draggable-invalid-drag-token' | ||
); | ||
} | ||
}; | ||
|
||
editorRoot.addEventListener( 'dragover', onDragOver ); | ||
|
||
return () => { | ||
editorRoot.removeEventListener( 'dragover', onDragOver ); | ||
}; | ||
} ); | ||
|
||
if ( ! isDraggable ) { | ||
return children( { draggable: false } ); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this applied via an inline style and CSS variable instead of by classname and direct value in
style.scss
? Or to put it slightly differently, does the JS version of the component need to have an opinion about how it's styled?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! I don't think it has to be applied as an inline style; I initially added it that way for the sake of development speed. It does have to be a CSS custom property, because its value needs to change when the body class gets added, and given that the chip we see on the page is a stored copy of itself changing classes on the chip itself won't work. Essentially the custom property is acting like a ref to the element, so we can manipulate its value from outside.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I was imagining something like the following:
Or does that not work? (Totally fine to go with a CSS variable by the way, just trying to understand what the limitations are 🙂)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, yeah that should work too! Would that be preferable to a custom property? I don't think specificity matters either way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel strongly either way, but I'd lean slightly more toward the nested selectors so that it's easy to swap out for (or combine) other styles like background color, border, or other subtle styling changes that folks might want to explore in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh good point, yeah that would make it easier to change styles altogether if we wanted to.