Skip to content
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

[RNMobile] BlockDraggable component #39617

Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a779e95
Use animated scroll handler in KeyboardAwareFlatList
fluiddot Mar 17, 2022
b71b308
Add hook for scrolling the block list while dragging
fluiddot Mar 17, 2022
faea4cf
Improve scroll animation in useScrollWhenDragging
fluiddot Mar 18, 2022
799f077
Add draggable chip component
fluiddot Mar 18, 2022
6aaae21
Add block draggable component
fluiddot Mar 18, 2022
f0bd8c0
Remove icon prop from draggable chip component
fluiddot Mar 21, 2022
64708c5
Add draggable placeholder
fluiddot Mar 21, 2022
778b10a
Fix draggable chip location
fluiddot Mar 21, 2022
3822b27
Wrap BlockListItemCell with BlockDraggable
fluiddot Mar 21, 2022
3688013
Fix block draggable placeholder style
fluiddot Mar 21, 2022
14a9d6b
Animate scale property instead of opacity of draggable chip
fluiddot Mar 21, 2022
90838fd
Fix draggable placeholder container height calculation
fluiddot Mar 21, 2022
ab337ef
Fix BlockDraggable height animation
fluiddot Mar 22, 2022
ee179a9
Move draggable to BlockDraggableWrapper
fluiddot Mar 23, 2022
f173ab8
Disable isDragging when long-press gesture ends
fluiddot Mar 23, 2022
95fd2e1
Fix onLayout calculation in block list item cell
fluiddot Mar 23, 2022
593644d
Add findBlockLayoutByPosition helper
fluiddot Mar 23, 2022
a910072
Set up dragging block by position
fluiddot Mar 23, 2022
0ddca86
Remove animate scroll velocity
fluiddot Mar 23, 2022
1492550
Remove useScrollWhenDragging hook
fluiddot Mar 23, 2022
2bd4e66
Remove react-native-reanimated mock
fluiddot Mar 24, 2022
9a2ae54
Rename CHIP_OFFSET_TO_TOUCH_POSITION constant
fluiddot Mar 24, 2022
fd0883e
Remove unused shared values of chip component
fluiddot Mar 24, 2022
6bef485
Stop dragging when no block is found
fluiddot Mar 24, 2022
ad016ed
Fix drag position calculation
fluiddot Mar 24, 2022
7fac2ec
Update html text input styles
fluiddot Mar 25, 2022
5c6e21a
Unify container component within html text input
fluiddot Mar 25, 2022
55e62f1
Use only a single client id in block draggable
fluiddot Mar 28, 2022
258eaf3
Add documentation to block draggable components
fluiddot Mar 28, 2022
78da359
Add documentation to block draggable chip component
fluiddot Mar 28, 2022
8cac264
Add documentation to findBlockLayoutByPosition
fluiddot Mar 28, 2022
1612b48
Update scrollOffsetTarget calculation
fluiddot Mar 28, 2022
b7e800a
Fix typos in block draggable
fluiddot Mar 28, 2022
8c79ad3
Add draggable wrapper container style
fluiddot Mar 28, 2022
e9ce4e3
Add dark mode styles for draggable chip
fluiddot Mar 28, 2022
4060e1a
Add dark mode styles for block draggable
fluiddot Mar 28, 2022
d16ce15
Get container height from blocks layout data
fluiddot Mar 29, 2022
842b09b
Replace inline callback functions with useCallback hook
fluiddot Mar 29, 2022
bda7b24
Update collapse/expand animation when dragging a block
fluiddot Mar 29, 2022
b886b66
Force draggable chip to be displayed upfront
fluiddot Mar 29, 2022
d66fb3d
Remove refs from dependencies arrays
fluiddot Mar 29, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/**
* External dependencies
*/
import { View } from 'react-native';

/**
* WordPress dependencies
*/
import { dragHandle } from '@wordpress/icons';
import { useSelect } from '@wordpress/data';
import { getBlockType } from '@wordpress/blocks';

/**
* Internal dependencies
*/
import BlockIcon from '../block-icon';
import styles from './style.scss';
import { store as blockEditorStore } from '../../store';

const shadowStyle = {
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.25,
shadowRadius: 3.84,

elevation: 5,
};
Comment on lines +21 to +31
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I defined the shadow style properties here because these properties are not recognized in SCSS.


/**
* Block draggable chip component
*
* @return {JSX.Element} Chip component.
*/
export default function BlockDraggableChip() {
const { blockIcon } = useSelect( ( select ) => {
const { getBlockName, getDraggedBlockClientIds } = select(
blockEditorStore
);
const draggedBlockClientIds = getDraggedBlockClientIds();
const blockName = getBlockName( draggedBlockClientIds[ 0 ] );

return {
blockIcon: getBlockType( blockName )?.icon,
};
} );
Comment on lines +44 to +54
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought of passing the blockIcon from the BlockDraggable component (i.e. its parent component) but I experienced downgrades in the performance due to re-renders caused by Redux state updates. For this reason, I finally decided to retrieve the blockIcon directly here.


return (
<View style={ [ styles[ 'draggable-chip__container' ], shadowStyle ] }>
<BlockIcon icon={ dragHandle } />
<BlockIcon icon={ blockIcon } />
</View>
);
}
315 changes: 315 additions & 0 deletions packages/block-editor/src/components/block-draggable/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,315 @@
/**
* External dependencies
*/
import Animated, {
interpolate,
runOnJS,
runOnUI,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withTiming,
scrollTo,
useAnimatedReaction,
} from 'react-native-reanimated';

/**
* WordPress dependencies
*/
import { Draggable } from '@wordpress/components';
import { useSelect, useDispatch } from '@wordpress/data';
import { useEffect } from '@wordpress/element';

/**
* Internal dependencies
*/
import DraggableChip from './draggable-chip';
import { store as blockEditorStore } from '../../store';
import { useBlockListContext } from '../block-list/block-list-context';
import styles from './style.scss';

const CHIP_OFFSET_TO_TOUCH_POSITION = 32;
const BLOCK_COLLAPSED_HEIGHT = 20;
const EXTRA_OFFSET_WHEN_CLOSE_TO_TOP_EDGE = 80;
const SCROLL_ANIMATION_DURATION = 350;

/**
* Block draggable wrapper component
*
* This component handles all the interactions for dragging blocks.
* It relies on the block list and its context for dragging, hence it
* should be rendered between the `BlockListProvider` component and the
* block list rendering. It also requires listening to scroll events,
* therefore for this purpose, it returns the `onScroll` event handler
* that should be attached to the list that renders the blocks.
*
*
* @param {Object} props Component props.
* @param {JSX.Element} props.children Children to be rendered.
*
* @return {Function} Render function that passes `onScroll` event handler.
*/
const BlockDraggableWrapper = ( { children } ) => {
const { startDraggingBlocks, stopDraggingBlocks } = useDispatch(
blockEditorStore
);

const {
blocksLayouts,
scrollRef,
findBlockLayoutByPosition,
} = useBlockListContext();
const animatedScrollRef = useAnimatedRef();
animatedScrollRef( scrollRef );
Comment on lines +77 to +78
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In order to use Reanimated's scrollTo function, we have to get the animated ref of the scroll. Since scrollRef is already provided via the block list context, we create the animated ref by invoking the function that sets the reference (source code reference).


const scroll = {
offsetY: useSharedValue( 0 ),
};
const chip = {
x: useSharedValue( 0 ),
y: useSharedValue( 0 ),
width: useSharedValue( 0 ),
height: useSharedValue( 0 ),
scale: useSharedValue( 0 ),
};
const isDragging = useSharedValue( false );
const scrollAnimation = useSharedValue( 0 );

const scrollHandler = ( event ) => {
'worklet';
const { contentOffset } = event;
scroll.offsetY.value = contentOffset.y;
};

// Stop dragging blocks if the block draggable is unmounted.
useEffect( () => {
return () => {
if ( isDragging.value ) {
stopDraggingBlocks();
}
};
}, [] );

const setupDraggingBlock = ( position ) => {
const blockLayout = findBlockLayoutByPosition( blocksLayouts.current, {
x: position.x,
y: position.y + scroll.offsetY.value,
} );

const foundClientId = blockLayout?.clientId;
if ( foundClientId ) {
startDraggingBlocks( [ foundClientId ] );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling this Redux action will notify the dragged block that is being dragged.


const isBlockOutOfScrollView = blockLayout.y < scroll.offsetY.value;
// If the dragging block is out of the scroll view, we have to
// scroll the block list to show the origin position of the block.
if ( isBlockOutOfScrollView ) {
scrollAnimation.value = scroll.offsetY.value;
const scrollOffsetTarget = Math.max(
0,
scroll.offsetY.value -
( scroll.offsetY.value - blockLayout.y ) -
EXTRA_OFFSET_WHEN_CLOSE_TO_TOP_EDGE
);
scrollAnimation.value = withTiming( scrollOffsetTarget, {
duration: SCROLL_ANIMATION_DURATION,
} );
}
} else {
// We stop dragging If no block is found.
runOnUI( stopDragging )();
}
};

// This hook is used for animating the scroll via a shared value.
useAnimatedReaction(
() => scrollAnimation.value,
( value ) => {
if ( isDragging.value ) {
scrollTo( animatedScrollRef, 0, value, false );
}
}
);

const onChipLayout = ( { nativeEvent: { layout } } ) => {
chip.width.value = layout.width;
chip.height.value = layout.height;
};

const startDragging = ( { x, y } ) => {
'worklet';
const dragPosition = { x, y };
chip.x.value = dragPosition.x;
chip.y.value = dragPosition.y;

isDragging.value = true;

chip.scale.value = withTiming( 1 );
runOnJS( setupDraggingBlock )( dragPosition );
};

const updateDragging = ( { x, y } ) => {
'worklet';
const dragPosition = { x, y };
chip.x.value = dragPosition.x;
chip.y.value = dragPosition.y;
};

const stopDragging = () => {
'worklet';
isDragging.value = false;

chip.scale.value = withTiming( 0 );
runOnJS( stopDraggingBlocks )();
};

const chipStyles = useAnimatedStyle( () => {
return {
position: 'absolute',
transform: [
{ translateX: chip.x.value - chip.width.value / 2 },
{
translateY:
chip.y.value -
chip.height.value -
CHIP_OFFSET_TO_TOUCH_POSITION,
},
{ scaleX: chip.scale.value },
{ scaleY: chip.scale.value },
],
};
} );

return (
<>
<Draggable
onDragStart={ startDragging }
onDragOver={ updateDragging }
onDragEnd={ stopDragging }
wrapperAnimatedStyles={ { flex: 1 } }
>
{ children( { onScroll: scrollHandler } ) }
</Draggable>
<Animated.View
onLayout={ onChipLayout }
style={ chipStyles }
pointerEvents="none"
>
<DraggableChip />
</Animated.View>
</>
);
};

/**
* Block draggable component
*
* This component serves for animating the block when it is being dragged.
* Hence, It should be wrapped around the rendering of a block.
*
* @param {Object} props Component props.
* @param {JSX.Element} props.children Children to be rendered.
* @param {string[]} props.clientId Client id of the block.
*
* @return {Function} Render function which includes the parameter `isDraggable` to determine if the block can be dragged.
*/
const BlockDraggable = ( { clientId, children } ) => {
const container = {
height: useSharedValue( 0 ),
};
const containerHeightBeforeDragging = useSharedValue( 0 );
const collapseAnimation = useSharedValue( 0 );

const onContainerLayout = ( { nativeEvent: { layout } } ) => {
container.height.value = layout.height;
};

const startBlockDragging = () => {
'worklet';
containerHeightBeforeDragging.value = container.height.value;
collapseAnimation.value = withTiming( 1 );
};

const stopBlockDragging = () => {
'worklet';
collapseAnimation.value = withTiming( 0 );
};

const { isDraggable, isBeingDragged } = useSelect(
( select ) => {
const {
getBlockRootClientId,
getTemplateLock,
isBlockBeingDragged,
} = select( blockEditorStore );
const rootClientId = getBlockRootClientId( clientId );
const templateLock = rootClientId
? getTemplateLock( rootClientId )
: null;

return {
isBeingDragged: isBlockBeingDragged( clientId ),
isDraggable: 'all' !== templateLock,
};
},
[ clientId ]
);

useEffect( () => {
if ( isBeingDragged ) {
runOnUI( startBlockDragging )();
} else {
runOnUI( stopBlockDragging )();
}
}, [ isBeingDragged ] );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The animations applied to the block are triggered when the block detects that is being dragged.


const containerStyles = useAnimatedStyle( () => {
const height = interpolate(
collapseAnimation.value,
[ 0, 1 ],
[ containerHeightBeforeDragging.value, BLOCK_COLLAPSED_HEIGHT ]
);
return {
height:
containerHeightBeforeDragging.value === 0 ||
collapseAnimation.value === 0
? 'auto'
: height,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only set a fixed height when the block is collapsed or when the container height value hasn't been calculated (i.e. value is 0).

};
} );

const blockStyles = useAnimatedStyle( () => {
return {
opacity: 1 - collapseAnimation.value,
};
} );

const placeholderStyles = useAnimatedStyle( () => {
return {
display: collapseAnimation.value === 0 ? 'none' : 'flex',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only show the placeholder component when the block is collapsed.

opacity: collapseAnimation.value,
};
} );

if ( ! isDraggable ) {
return children( { isDraggable: false } );
}

return (
<Animated.View onLayout={ onContainerLayout } style={ containerStyles }>
geriux marked this conversation as resolved.
Show resolved Hide resolved
<Animated.View style={ blockStyles }>
{ children( { isDraggable: true } ) }
</Animated.View>
<Animated.View
style={ [
geriux marked this conversation as resolved.
Show resolved Hide resolved
styles[ 'draggable-placeholder__container' ],
placeholderStyles,
] }
pointerEvents="none"
/>
</Animated.View>
);
};

export { BlockDraggableWrapper };
export default BlockDraggable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.draggable-chip__container {
flex-direction: row;
padding: 16px;
background-color: #f7f7f7;
geriux marked this conversation as resolved.
Show resolved Hide resolved
border-radius: 8px;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to follow a similar design to this example:

Untitled


.draggable-placeholder__container {
position: absolute;
top: 0;
left: $solid-border-space;
right: $solid-border-space;
bottom: 0;
z-index: 10;
background-color: $gray-lighten-30;
border-radius: 8px;
}
Loading