diff --git a/packages/block-editor/src/components/block-draggable/draggable-chip.native.js b/packages/block-editor/src/components/block-draggable/draggable-chip.native.js
new file mode 100644
index 0000000000000..2559b2089252b
--- /dev/null
+++ b/packages/block-editor/src/components/block-draggable/draggable-chip.native.js
@@ -0,0 +1,62 @@
+/**
+ * External dependencies
+ */
+import { View } from 'react-native';
+
+/**
+ * WordPress dependencies
+ */
+import { dragHandle } from '@wordpress/icons';
+import { useSelect } from '@wordpress/data';
+import { getBlockType } from '@wordpress/blocks';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+
+/**
+ * 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,
+};
+
+/**
+ * Block draggable chip component
+ *
+ * @return {JSX.Element} Chip component.
+ */
+export default function BlockDraggableChip() {
+ const containerStyle = usePreferredColorSchemeStyle(
+ styles[ 'draggable-chip__container' ],
+ styles[ 'draggable-chip__container--dark' ]
+ );
+
+ const { blockIcon } = useSelect( ( select ) => {
+ const { getBlockName, getDraggedBlockClientIds } = select(
+ blockEditorStore
+ );
+ const draggedBlockClientIds = getDraggedBlockClientIds();
+ const blockName = getBlockName( draggedBlockClientIds[ 0 ] );
+
+ return {
+ blockIcon: getBlockType( blockName )?.icon,
+ };
+ } );
+
+ return (
+
+
+
+
+ );
+}
diff --git a/packages/block-editor/src/components/block-draggable/index.native.js b/packages/block-editor/src/components/block-draggable/index.native.js
new file mode 100644
index 0000000000000..8524e4aa48f94
--- /dev/null
+++ b/packages/block-editor/src/components/block-draggable/index.native.js
@@ -0,0 +1,352 @@
+/**
+ * External dependencies
+ */
+import Animated, {
+ runOnJS,
+ runOnUI,
+ useAnimatedRef,
+ useAnimatedStyle,
+ useSharedValue,
+ withTiming,
+ scrollTo,
+ useAnimatedReaction,
+ Easing,
+} from 'react-native-reanimated';
+
+/**
+ * WordPress dependencies
+ */
+import { Draggable } from '@wordpress/components';
+import { useSelect, useDispatch } from '@wordpress/data';
+import { useEffect } from '@wordpress/element';
+import { usePreferredColorSchemeStyle } from '@wordpress/compose';
+
+/**
+ * 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;
+const COLLAPSE_HEIGHT_ANIMATION_CONFIG = {
+ duration: 350,
+ easing: Easing.out( Easing.exp ),
+};
+const EXPAND_HEIGHT_ANIMATION_CONFIG = {
+ duration: 350,
+ easing: Easing.in( Easing.exp ),
+};
+const COLLAPSE_OPACITY_ANIMATION_CONFIG = { duration: 150 };
+
+/**
+ * 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 wrapperStyles = usePreferredColorSchemeStyle(
+ styles[ 'draggable-wrapper__container' ],
+ styles[ 'draggable-wrapper__container--dark' ]
+ );
+
+ const { startDraggingBlocks, stopDraggingBlocks } = useDispatch(
+ blockEditorStore
+ );
+
+ const {
+ blocksLayouts,
+ scrollRef,
+ findBlockLayoutByPosition,
+ } = useBlockListContext();
+ const animatedScrollRef = useAnimatedRef();
+ animatedScrollRef( scrollRef );
+
+ 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 ] );
+
+ 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,
+ 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 chipDynamicStyles = useAnimatedStyle( () => {
+ return {
+ 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 },
+ ],
+ };
+ } );
+ const chipStyles = [
+ chipDynamicStyles,
+ styles[ 'draggable-chip__wrapper' ],
+ ];
+
+ return (
+ <>
+
+ { children( { onScroll: scrollHandler } ) }
+
+
+
+
+ >
+ );
+};
+
+/**
+ * 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 { blocksLayouts, findBlockLayoutByClientId } = useBlockListContext();
+
+ const collapseAnimation = {
+ opacity: useSharedValue( 0 ),
+ height: useSharedValue( 0 ),
+ initialHeight: useSharedValue( 0 ),
+ };
+
+ const startBlockDragging = () => {
+ const blockLayout = findBlockLayoutByClientId(
+ blocksLayouts.current,
+ clientId
+ );
+ if ( blockLayout?.height > 0 ) {
+ collapseAnimation.initialHeight.value = blockLayout.height;
+ collapseAnimation.height.value = blockLayout.height;
+ collapseAnimation.opacity.value = withTiming(
+ 1,
+ COLLAPSE_OPACITY_ANIMATION_CONFIG,
+ ( completed ) => {
+ if ( completed ) {
+ collapseAnimation.height.value = withTiming(
+ BLOCK_COLLAPSED_HEIGHT,
+ COLLAPSE_HEIGHT_ANIMATION_CONFIG
+ );
+ }
+ }
+ );
+ }
+ };
+
+ const stopBlockDragging = () => {
+ collapseAnimation.height.value = withTiming(
+ collapseAnimation.initialHeight.value,
+ EXPAND_HEIGHT_ANIMATION_CONFIG,
+ ( completed ) => {
+ if ( completed ) {
+ collapseAnimation.opacity.value = withTiming(
+ 0,
+ COLLAPSE_OPACITY_ANIMATION_CONFIG
+ );
+ }
+ }
+ );
+ };
+
+ 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 ) {
+ startBlockDragging();
+ } else {
+ stopBlockDragging();
+ }
+ }, [ isBeingDragged ] );
+
+ const containerStyles = useAnimatedStyle( () => {
+ const canAnimateHeight =
+ collapseAnimation.height.value !== 0 &&
+ collapseAnimation.opacity.value !== 0;
+ return {
+ height: canAnimateHeight ? collapseAnimation.height.value : 'auto',
+ };
+ } );
+
+ const blockStyles = useAnimatedStyle( () => {
+ return {
+ opacity: 1 - collapseAnimation.opacity.value,
+ };
+ } );
+
+ const placeholderDynamicStyles = useAnimatedStyle( () => {
+ return {
+ display: collapseAnimation.opacity.value === 0 ? 'none' : 'flex',
+ opacity: collapseAnimation.opacity.value,
+ };
+ } );
+ const placeholderStaticStyles = usePreferredColorSchemeStyle(
+ styles[ 'draggable-placeholder__container' ],
+ styles[ 'draggable-placeholder__container--dark' ]
+ );
+ const placeholderStyles = [
+ placeholderStaticStyles,
+ placeholderDynamicStyles,
+ ];
+
+ if ( ! isDraggable ) {
+ return children( { isDraggable: false } );
+ }
+
+ return (
+
+
+ { children( { isDraggable: true } ) }
+
+
+
+ );
+};
+
+export { BlockDraggableWrapper };
+export default BlockDraggable;
diff --git a/packages/block-editor/src/components/block-draggable/style.native.scss b/packages/block-editor/src/components/block-draggable/style.native.scss
new file mode 100644
index 0000000000000..b8133c966e9e4
--- /dev/null
+++ b/packages/block-editor/src/components/block-draggable/style.native.scss
@@ -0,0 +1,34 @@
+.draggable-wrapper__container {
+ flex: 1;
+}
+
+.draggable-chip__wrapper {
+ position: absolute;
+ z-index: 10;
+}
+
+.draggable-chip__container {
+ flex-direction: row;
+ padding: 16px;
+ background-color: $gray-0;
+ border-radius: 8px;
+}
+
+.draggable-chip__container--dark {
+ background-color: $app-background-dark-alt;
+}
+
+.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;
+}
+
+.draggable-placeholder__container--dark {
+ background-color: $gray-darken-30;
+}
diff --git a/packages/block-editor/src/components/block-list/block-list-context.native.js b/packages/block-editor/src/components/block-list/block-list-context.native.js
index 95385b480b3d1..21f850ec5551c 100644
--- a/packages/block-editor/src/components/block-list/block-list-context.native.js
+++ b/packages/block-editor/src/components/block-list/block-list-context.native.js
@@ -7,12 +7,35 @@ export const DEFAULT_BLOCK_LIST_CONTEXT = {
scrollRef: null,
blocksLayouts: { current: {} },
findBlockLayoutByClientId,
+ findBlockLayoutByPosition,
updateBlocksLayouts,
};
const Context = createContext( DEFAULT_BLOCK_LIST_CONTEXT );
const { Provider, Consumer } = Context;
+/**
+ * Finds a block's layout data by position.
+ *
+ * @param {Object} data Blocks layouts object.
+ * @param {Object} position Position to use for finding the block.
+ * @param {number} position.x X coordinate.
+ * @param {number} position.y Y coordinate.
+ *
+ * @return {Object|undefined} Found block layout data that matches the provided position. If none is found, `undefined` will be returned.
+ */
+function findBlockLayoutByPosition( data, position ) {
+ // Only enabled for root level blocks
+ return Object.values( data ).find( ( block ) => {
+ return (
+ position.x >= block.x &&
+ position.x <= block.x + block.width &&
+ position.y >= block.y &&
+ position.y <= block.y + block.height
+ );
+ } );
+}
+
/**
* Finds a block's layout data by its client Id.
*
diff --git a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js
index c399643a63399..5577b0705d686 100644
--- a/packages/block-editor/src/components/block-list/block-list-item-cell.native.js
+++ b/packages/block-editor/src/components/block-list/block-list-item-cell.native.js
@@ -12,6 +12,7 @@ import { useEffect, useCallback } from '@wordpress/element';
* Internal dependencies
*/
import { useBlockListContext } from './block-list-context';
+import BlockDraggable from '../block-draggable';
function BlockListItemCell( { children, clientId, rootClientId } ) {
const { blocksLayouts, updateBlocksLayouts } = useBlockListContext();
@@ -36,7 +37,13 @@ function BlockListItemCell( { children, clientId, rootClientId } ) {
[ clientId, rootClientId, updateBlocksLayouts ]
);
- return { children };
+ return (
+
+
+ { () => children }
+
+
+ );
}
export default BlockListItemCell;
diff --git a/packages/block-editor/src/components/block-list/index.native.js b/packages/block-editor/src/components/block-list/index.native.js
index f1dc31da0e91e..1f52d0fd7441f 100644
--- a/packages/block-editor/src/components/block-list/index.native.js
+++ b/packages/block-editor/src/components/block-list/index.native.js
@@ -31,6 +31,7 @@ import {
BlockListConsumer,
DEFAULT_BLOCK_LIST_CONTEXT,
} from './block-list-context';
+import { BlockDraggableWrapper } from '../block-draggable';
import { store as blockEditorStore } from '../../store';
export const OnCaretVerticalPositionChange = createContext();
@@ -197,7 +198,9 @@ export class BlockList extends Component {
scrollRef: this.scrollViewRef,
} }
>
- { this.renderList() }
+
+ { ( { onScroll } ) => this.renderList( { onScroll } ) }
+
) : (
@@ -235,7 +238,7 @@ export class BlockList extends Component {
contentResizeMode,
blockWidth,
} = this.props;
- const { parentScrollRef } = extraProps;
+ const { parentScrollRef, onScroll } = extraProps;
const {
blockToolbar,
@@ -310,6 +313,7 @@ export class BlockList extends Component {
ListHeaderComponent={ header }
ListEmptyComponent={ ! isReadOnly && this.renderEmptyList }
ListFooterComponent={ this.renderBlockListFooter }
+ onScroll={ onScroll }
/>
{ this.shouldShowInnerBlockAppender() && (
{
'worklet';
+ isDragging.value = false;
if ( onDragEnd ) {
onDragEnd();
}
diff --git a/packages/components/src/mobile/html-text-input/container.android.js b/packages/components/src/mobile/html-text-input/container.android.js
deleted file mode 100644
index 68d69783f3b9f..0000000000000
--- a/packages/components/src/mobile/html-text-input/container.android.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * External dependencies
- */
-import { ScrollView } from 'react-native';
-
-/**
- * Internal dependencies
- */
-import KeyboardAvoidingView from '../keyboard-avoiding-view';
-import styles from './style.android.scss';
-
-const HTMLInputContainer = ( { children, parentHeight } ) => (
-
- { children }
-
-);
-
-HTMLInputContainer.scrollEnabled = false;
-
-export default HTMLInputContainer;
diff --git a/packages/components/src/mobile/html-text-input/container.ios.js b/packages/components/src/mobile/html-text-input/container.ios.js
deleted file mode 100644
index b40214e1eaab0..0000000000000
--- a/packages/components/src/mobile/html-text-input/container.ios.js
+++ /dev/null
@@ -1,50 +0,0 @@
-/**
- * External dependencies
- */
-import { UIManager, PanResponder } from 'react-native';
-
-/**
- * WordPress dependencies
- */
-import { Component } from '@wordpress/element';
-
-/**
- * Internal dependencies
- */
-import KeyboardAvoidingView from '../keyboard-avoiding-view';
-import styles from './style.ios.scss';
-
-class HTMLInputContainer extends Component {
- constructor() {
- super( ...arguments );
-
- this.panResponder = PanResponder.create( {
- onStartShouldSetPanResponderCapture: () => true,
-
- onPanResponderMove: ( e, gestureState ) => {
- if ( gestureState.dy > 100 && gestureState.dy < 110 ) {
- // Keyboard.dismiss() and this.textInput.blur() are not working here
- // They require to know the currentlyFocusedID under the hood but
- // during this gesture there's no currentlyFocusedID.
- UIManager.blur( e.target );
- }
- },
- } );
- }
-
- render() {
- return (
-
- { this.props.children }
-
- );
- }
-}
-
-HTMLInputContainer.scrollEnabled = true;
-
-export default HTMLInputContainer;
diff --git a/packages/components/src/mobile/html-text-input/index.native.js b/packages/components/src/mobile/html-text-input/index.native.js
index 438a1ca88ed4b..eab8dafcbd490 100644
--- a/packages/components/src/mobile/html-text-input/index.native.js
+++ b/packages/components/src/mobile/html-text-input/index.native.js
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import { TextInput } from 'react-native';
+import { ScrollView, TextInput } from 'react-native';
/**
* WordPress dependencies
@@ -20,7 +20,7 @@ import {
/**
* Internal dependencies
*/
-import HTMLInputContainer from './container';
+import KeyboardAvoidingView from '../keyboard-avoiding-view';
import styles from './style.scss';
export class HTMLTextInput extends Component {
@@ -73,7 +73,13 @@ export class HTMLTextInput extends Component {
}
render() {
- const { getStylesFromColorScheme, style } = this.props;
+ const {
+ editTitle,
+ getStylesFromColorScheme,
+ parentHeight,
+ style,
+ title,
+ } = this.props;
const titleStyle = [
styles.htmlViewTitle,
style?.text && { color: style.text },
@@ -90,32 +96,42 @@ export class HTMLTextInput extends Component {
...( style?.text && { color: style.text } ),
};
return (
-
-
-
-
+
+
+
+
+
+
);
}
}
diff --git a/packages/components/src/mobile/html-text-input/style.android.scss b/packages/components/src/mobile/html-text-input/style.android.scss
index 1dca01274d75b..e292901922dbc 100644
--- a/packages/components/src/mobile/html-text-input/style.android.scss
+++ b/packages/components/src/mobile/html-text-input/style.android.scss
@@ -1,21 +1,7 @@
-@import "./style-common.scss";
-
-.htmlView {
- font-family: $htmlFont;
- padding-left: $padding;
- padding-right: $padding;
- padding-top: $padding;
- padding-bottom: $padding + 16;
-}
+@import "./style.scss";
.htmlViewTitle {
font-family: $htmlFont;
padding-left: $padding;
padding-right: $padding;
- padding-top: $padding;
- padding-bottom: $padding;
-}
-
-.scrollView {
- flex: 1;
}
diff --git a/packages/components/src/mobile/html-text-input/style.ios.scss b/packages/components/src/mobile/html-text-input/style.ios.scss
index 97cf00a7512ff..cd269a6b9876f 100644
--- a/packages/components/src/mobile/html-text-input/style.ios.scss
+++ b/packages/components/src/mobile/html-text-input/style.ios.scss
@@ -1,17 +1,4 @@
-@import "./style-common.scss";
-
-$title-height: 32;
-
-.htmlView {
- font-family: $htmlFont;
- padding-left: $padding;
- padding-right: $padding;
- padding-bottom: $title-height + $padding;
-}
-
-.htmlViewDark {
- color: $textColorDark;
-}
+@import "./style.scss";
.htmlViewTitle {
font-family: $htmlFont;
@@ -19,5 +6,4 @@ $title-height: 32;
padding-right: $padding;
padding-top: $padding;
padding-bottom: $padding;
- height: $title-height;
}
diff --git a/packages/components/src/mobile/html-text-input/style-common.native.scss b/packages/components/src/mobile/html-text-input/style.scss
similarity index 54%
rename from packages/components/src/mobile/html-text-input/style-common.native.scss
rename to packages/components/src/mobile/html-text-input/style.scss
index c1ac9f155d4c7..89b81e898ad4b 100644
--- a/packages/components/src/mobile/html-text-input/style-common.native.scss
+++ b/packages/components/src/mobile/html-text-input/style.scss
@@ -21,3 +21,19 @@ $textColorDark: $white;
.placeholderDark {
color: $gray-50;
}
+
+.htmlView {
+ font-family: $htmlFont;
+ padding-left: $padding;
+ padding-right: $padding;
+ padding-top: $padding;
+ padding-bottom: $padding + 16;
+}
+
+.htmlViewDark {
+ color: $textColorDark;
+}
+
+.scrollView {
+ flex: 1;
+}
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js
index bdfc2ef1fd847..ffdd97dd5acbb 100644
--- a/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.android.js
@@ -2,17 +2,27 @@
* External dependencies
*/
import { FlatList } from 'react-native';
+import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated';
/**
* Internal dependencies
*/
import KeyboardAvoidingView from '../keyboard-avoiding-view';
-export const KeyboardAwareFlatList = ( props ) => (
-
-
-
-);
+const AnimatedFlatList = Animated.createAnimatedComponent( FlatList );
+
+export const KeyboardAwareFlatList = ( { innerRef, onScroll, ...props } ) => {
+ const scrollHandler = useAnimatedScrollHandler( { onScroll } );
+ return (
+
+
+
+ );
+};
KeyboardAwareFlatList.handleCaretVerticalPositionChange = () => {
// no need to handle on Android, it is system managed
diff --git a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
index 6c954d451dc17..a8e84aaf1c2a4 100644
--- a/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
+++ b/packages/components/src/mobile/keyboard-aware-flat-list/index.ios.js
@@ -4,13 +4,20 @@
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
import { FlatList } from 'react-native';
import { isEqual } from 'lodash';
+import Animated, {
+ useAnimatedScrollHandler,
+ useSharedValue,
+} from 'react-native-reanimated';
/**
* WordPress dependencies
*/
-import { memo } from '@wordpress/element';
+import { memo, useCallback, useRef } from '@wordpress/element';
const List = memo( FlatList, isEqual );
+const AnimatedKeyboardAwareScrollView = Animated.createAnimatedComponent(
+ KeyboardAwareScrollView
+);
export const KeyboardAwareFlatList = ( {
extraScrollHeight,
@@ -19,53 +26,75 @@ export const KeyboardAwareFlatList = ( {
autoScroll,
scrollViewStyle,
inputAccessoryViewHeight,
+ onScroll,
...listProps
-} ) => (
- {
- this.scrollViewRef = ref;
+} ) => {
+ const scrollViewRef = useRef();
+ const keyboardWillShowIndicator = useRef();
+
+ const latestContentOffsetY = useSharedValue( -1 );
+
+ const scrollHandler = useAnimatedScrollHandler( {
+ onScroll: ( event ) => {
+ const { contentOffset } = event;
+ latestContentOffsetY.value = contentOffset.y;
+ onScroll( event );
+ },
+ } );
+
+ const getRef = useCallback(
+ ( ref ) => {
+ scrollViewRef.current = ref;
innerRef( ref );
- } }
- onKeyboardWillHide={ () => {
- this.keyboardWillShowIndicator = false;
- } }
- onKeyboardDidHide={ () => {
- setTimeout( () => {
- if (
- ! this.keyboardWillShowIndicator &&
- this.latestContentOffsetY !== undefined &&
- ! shouldPreventAutomaticScroll()
- ) {
- // Reset the content position if keyboard is still closed.
- if ( this.scrollViewRef ) {
- this.scrollViewRef.scrollToPosition(
- 0,
- this.latestContentOffsetY,
- true
- );
- }
- }
- }, 50 );
- } }
- onKeyboardWillShow={ () => {
- this.keyboardWillShowIndicator = true;
- } }
- scrollEnabled={ listProps.scrollEnabled }
- onScroll={ ( event ) => {
- this.latestContentOffsetY = event.nativeEvent.contentOffset.y;
- } }
- >
-
-
-);
+ },
+ [ innerRef ]
+ );
+ const onKeyboardWillHide = useCallback( () => {
+ keyboardWillShowIndicator.current = false;
+ }, [] );
+ const onKeyboardDidHide = useCallback( () => {
+ setTimeout( () => {
+ if (
+ ! keyboardWillShowIndicator.current &&
+ latestContentOffsetY.value !== -1 &&
+ ! shouldPreventAutomaticScroll()
+ ) {
+ // Reset the content position if keyboard is still closed.
+ scrollViewRef.current?.scrollToPosition(
+ 0,
+ latestContentOffsetY.value,
+ true
+ );
+ }
+ }, 50 );
+ }, [ latestContentOffsetY, shouldPreventAutomaticScroll ] );
+ const onKeyboardWillShow = useCallback( () => {
+ keyboardWillShowIndicator.current = true;
+ }, [] );
+
+ return (
+
+
+
+ );
+};
KeyboardAwareFlatList.handleCaretVerticalPositionChange = (
scrollView,
diff --git a/test/native/setup.js b/test/native/setup.js
index 5fb9927752771..014b7f0ce667c 100644
--- a/test/native/setup.js
+++ b/test/native/setup.js
@@ -153,16 +153,6 @@ jest.mock( '@react-native-community/blur', () => () => 'BlurView', {
virtual: true,
} );
-jest.mock( 'react-native-reanimated', () => {
- const Reanimated = require( 'react-native-reanimated/mock' );
-
- // The mock for `call` immediately calls the callback which is incorrect
- // So we override it with a no-op
- Reanimated.default.call = () => {};
-
- return Reanimated;
-} );
-
// Silence the warning: Animated: `useNativeDriver` is not supported because the
// native animated module is missing. This was added per React Navigation docs.
// https://reactnavigation.org/docs/testing/#mocking-native-modules