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