Skip to content

Commit

Permalink
[RNMobile] Auto-scroll upon block insertion (#57273)
Browse files Browse the repository at this point in the history
* Add `useScrollToSection` hook

* Add `useScrollToElement` hook

* Update logic to retrieve `KeyboardAwareFlatList` ref using `forwardRef`

* Replace `useScrollToTextInput` with `useScrollToSection`

* Expose `scrollToSection` and `scrollToElement` in Android implementation of `KeyboardAwareFlatList `

* Add `useScrollUponInsertion` hook

* Add `useScroll` hook to abstract common logic in `KeyboardAwareFlatList`

* Calculate `nativeScrollRef` based on platform in `useScroll` hook

* Update unit test to adapt `useScrollToSection` hook

* Remove `nativeScrollRef` in favor of calculating the ref in `KeyboardAwareFlatList` platform-specific components

* Add unit test for `useScroll` hook

* Ensure layout event of `BlockListItemCell` is triggered in tests

* Avoid triggering auto-scroll for the same client ID

* Update `react-native-editor` changelog
  • Loading branch information
fluiddot authored Jan 2, 2024
1 parent 2d43054 commit f364b68
Show file tree
Hide file tree
Showing 15 changed files with 462 additions and 168 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import Animated, {
runOnJS,
runOnUI,
useAnimatedRef,
useAnimatedStyle,
useSharedValue,
withDelay,
Expand Down Expand Up @@ -39,7 +38,6 @@ import RCTAztecView from '@wordpress/react-native-aztec';
import useScrollWhenDragging from './use-scroll-when-dragging';
import DraggableChip from './draggable-chip';
import { store as blockEditorStore } from '../../store';
import { useBlockListContext } from '../block-list/block-list-context';
import DroppingInsertionPoint from './dropping-insertion-point';
import useBlockDropZone from '../use-block-drop-zone';
import styles from './style.scss';
Expand Down Expand Up @@ -74,13 +72,10 @@ const BlockDraggableWrapper = ( { children, isRTL } ) => {
const { selectBlock, startDraggingBlocks, stopDraggingBlocks } =
useDispatch( blockEditorStore );

const { scrollRef } = useBlockListContext();
const animatedScrollRef = useAnimatedRef();
const { left, right } = useSafeAreaInsets();
const { width } = useSafeAreaFrame();
const safeAreaOffset = left + right;
const contentWidth = width - safeAreaOffset;
animatedScrollRef( scrollRef );

const scroll = {
offsetY: useSharedValue( 0 ),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
*/
import {
act,
advanceAnimationByFrames,
fireEvent,
initializeEditor,
screen,
waitForStoreResolvers,
within,
advanceAnimationByFrames,
} from 'test/helpers';
import { fireGestureHandler } from 'react-native-gesture-handler/jest-utils';
import { State } from 'react-native-gesture-handler';
Expand Down Expand Up @@ -52,15 +53,15 @@ const DEFAULT_TOUCH_EVENTS = [
export const initializeWithBlocksLayouts = async ( blocks ) => {
const initialHtml = blocks.map( ( block ) => block.html ).join( '\n' );

const screen = await initializeEditor( { initialHtml } );
const { getAllByLabelText } = screen;
await initializeEditor( { initialHtml } );

const waitPromises = [];
const blockListItems = screen.getAllByTestId( 'block-list-item-cell' );
// Check that rendered block list items match expected block count.
expect( blockListItems.length ).toBe( blocks.length );

blocks.forEach( ( block, index ) => {
const a11yLabel = new RegExp(
`${ block.name } Block\\. Row ${ index + 1 }`
);
const [ element ] = getAllByLabelText( a11yLabel );
const element = blockListItems[ index ];
// "onLayout" event will populate the blocks layouts data.
fireEvent( element, 'layout', {
nativeEvent: { layout: block.layout },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const VELOCITY_MULTIPLIER = 5000;
export default function useScrollWhenDragging() {
const { scrollRef } = useBlockListContext();
const animatedScrollRef = useAnimatedRef();
animatedScrollRef( scrollRef );
animatedScrollRef( scrollRef?.scrollViewRef );

const { height: windowHeight } = useWindowDimensions();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,11 @@ function BlockListItemCell( { children, item: clientId, onLayout } ) {
[ clientId, rootClientId, updateBlocksLayouts, onLayout ]
);

return <View onLayout={ onCellLayout }>{ children }</View>;
return (
<View testID="block-list-item-cell" onLayout={ onCellLayout }>
{ children }
</View>
);
}

export default BlockListItemCell;
17 changes: 16 additions & 1 deletion packages/block-editor/src/components/block-list/block.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import classnames from 'classnames';
/**
* WordPress dependencies
*/
import { useCallback, useMemo, useState } from '@wordpress/element';
import { useCallback, useMemo, useState, useRef } from '@wordpress/element';
import {
GlobalStylesContext,
getMergedGlobalStyles,
Expand Down Expand Up @@ -40,6 +40,7 @@ import BlockInvalidWarning from './block-invalid-warning';
import BlockOutline from './block-outline';
import { store as blockEditorStore } from '../../store';
import { useLayout } from './layout';
import useScrollUponInsertion from './use-scroll-upon-insertion';
import { useSettings } from '../use-settings';

const EMPTY_ARRAY = [];
Expand Down Expand Up @@ -103,6 +104,18 @@ function BlockWrapper( {
];
const accessible = ! ( isSelected || isDescendentBlockSelected );

const ref = useRef();
const [ isLayoutCalculated, setIsLayoutCalculated ] = useState();
useScrollUponInsertion( {
clientId,
isSelected,
isLayoutCalculated,
elementRef: ref,
} );
const onLayout = useCallback( () => {
setIsLayoutCalculated( true );
}, [] );

return (
<Pressable
accessibilityLabel={ accessibilityLabel }
Expand All @@ -111,6 +124,8 @@ function BlockWrapper( {
disabled={ ! isTouchable }
onPress={ onFocus }
style={ blockWrapperStyle }
ref={ ref }
onLayout={ onLayout }
>
<BlockOutline
blockCategory={ blockCategory }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export default function BlockList( {
insertBlock( newBlock, blockCount );
};

const scrollViewRef = useRef( null );
const scrollRef = useRef( null );

const shouldFlatListPreventAutomaticScroll = () =>
blockInsertionPointIsVisible;
Expand Down Expand Up @@ -239,7 +239,7 @@ export default function BlockList( {
<BlockListProvider
value={ {
...DEFAULT_BLOCK_LIST_CONTEXT,
scrollRef: scrollViewRef.current,
scrollRef: scrollRef.current,
} }
>
<BlockDraggableWrapper isRTL={ isRTL }>
Expand All @@ -249,9 +249,7 @@ export default function BlockList( {
? { removeClippedSubviews: false }
: {} ) } // Disable clipping on Android to fix focus losing. See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541
accessibilityLabel="block-list"
innerRef={ ( ref ) => {
scrollViewRef.current = ref;
} }
ref={ scrollRef }
extraScrollHeight={ extraScrollHeight }
keyboardShouldPersistTaps="always"
scrollViewStyle={ { flex: 1 } }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* WordPress dependencies
*/
import { useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';

/**
* Internal dependencies
*/
import { useBlockListContext } from './block-list-context';
import { store as blockEditorStore } from '../../store';

const useScrollUponInsertion = ( {
clientId,
isSelected,
isLayoutCalculated,
elementRef,
} ) => {
const { scrollRef } = useBlockListContext();
const wasBlockJustInserted = useSelect(
( select ) =>
!! select( blockEditorStore ).wasBlockJustInserted(
clientId,
'inserter_menu'
),
[ clientId ]
);
useEffect( () => {
const lastScrollTo = scrollRef?.lastScrollTo.current;
const alreadyScrolledTo = lastScrollTo?.clientId === clientId;
if (
alreadyScrolledTo ||
! isSelected ||
! scrollRef ||
! wasBlockJustInserted ||
! isLayoutCalculated
) {
return;
}
scrollRef.scrollToElement( elementRef );
lastScrollTo.clientId = clientId;
}, [
isSelected,
scrollRef,
wasBlockJustInserted,
elementRef,
isLayoutCalculated,
clientId,
] );
};

export default useScrollUponInsertion;
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,71 @@
* External dependencies
*/
import { FlatList } from 'react-native';
import Animated, { useAnimatedScrollHandler } from 'react-native-reanimated';
import Animated from 'react-native-reanimated';

/**
* WordPress dependencies
*/
import {
forwardRef,
useCallback,
useImperativeHandle,
} from '@wordpress/element';

/**
* Internal dependencies
*/
import useScroll from './use-scroll';
import KeyboardAvoidingView from '../keyboard-avoiding-view';

const AnimatedFlatList = Animated.createAnimatedComponent( FlatList );

export const KeyboardAwareFlatList = ( { innerRef, onScroll, ...props } ) => {
const scrollHandler = useAnimatedScrollHandler( { onScroll } );
export const KeyboardAwareFlatList = ( { onScroll, ...props }, ref ) => {
const { extraScrollHeight, scrollEnabled, shouldPreventAutomaticScroll } =
props;

const {
scrollViewRef,
scrollHandler,
scrollToSection,
scrollToElement,
onContentSizeChange,
lastScrollTo,
} = useScroll( {
scrollEnabled,
shouldPreventAutomaticScroll,
extraScrollHeight,
onScroll,
} );

const getFlatListRef = useCallback(
( flatListRef ) => {
// On Android, we get the ref of the associated scroll
// view to the FlatList.
scrollViewRef.current = flatListRef?.getNativeScrollRef();
},
[ scrollViewRef ]
);

useImperativeHandle( ref, () => {
return {
scrollViewRef: scrollViewRef.current,
scrollToSection,
scrollToElement,
lastScrollTo,
};
} );

return (
<KeyboardAvoidingView style={ { flex: 1 } }>
<AnimatedFlatList
ref={ innerRef }
ref={ getFlatListRef }
onScroll={ scrollHandler }
onContentSizeChange={ onContentSizeChange }
{ ...props }
/>
</KeyboardAvoidingView>
);
};

export default KeyboardAwareFlatList;
export default forwardRef( KeyboardAwareFlatList );
Loading

0 comments on commit f364b68

Please sign in to comment.