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

[Mobile] - KeyboardAwareFlatList - Enable FlatList virtualization for iOS #59833

Merged
merged 19 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
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
Expand Up @@ -211,7 +211,7 @@ export default function BlockList( {
);
};

const { blockToolbar, headerToolbar, floatingToolbar } = styles;
const { blockToolbar, floatingToolbar } = styles;

const containerStyle = {
flex: isRootList ? 1 : 0,
Expand All @@ -224,7 +224,6 @@ export default function BlockList( {
const isMultiBlocks = blockClientIds.length > 1;
const { isWider } = alignmentHelpers;
const extraScrollHeight =
headerToolbar.height +
blockToolbar.height +
( isFloatingToolbarVisible ? floatingToolbar.height : 0 );

Expand All @@ -245,14 +244,10 @@ export default function BlockList( {
<BlockDraggableWrapper isRTL={ isRTL }>
{ ( { onScroll } ) => (
<KeyboardAwareFlatList
{ ...( Platform.OS === 'android'
? { 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"
ref={ scrollRef }
extraScrollHeight={ extraScrollHeight }
keyboardShouldPersistTaps="always"
scrollViewStyle={ { flex: 1 } }
extraData={ getExtraData() }
scrollEnabled={ isRootList }
contentContainerStyle={ [
Expand Down
11 changes: 11 additions & 0 deletions packages/block-editor/src/components/rich-text/index.native.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export function RichTextWrapper(
getBlock,
isMultiSelecting,
hasMultiSelection,
getSelectedBlockClientId,
} = select( blockEditorStore );

const selectionStart = getSelectionStart();
Expand Down Expand Up @@ -154,6 +155,7 @@ export function RichTextWrapper(
didAutomaticChange: didAutomaticChange(),
disabled: isMultiSelecting() || hasMultiSelection(),
undo,
getSelectedBlockClientId,
...extraProps,
};
};
Expand All @@ -164,6 +166,7 @@ export function RichTextWrapper(
selectionStart,
selectionEnd,
isSelected,
getSelectedBlockClientId,
didAutomaticChange,
disabled,
undo,
Expand All @@ -175,6 +178,7 @@ export function RichTextWrapper(
exitFormattedText,
selectionChange,
__unstableMarkAutomaticChange,
clearSelectedBlock,
} = useDispatch( blockEditorStore );
const adjustedAllowedFormats = getAllowedFormats( {
allowedFormats,
Expand Down Expand Up @@ -209,6 +213,12 @@ export function RichTextWrapper(
[ clientId, identifier ]
);

const clearCurrentSelectionOnUnmount = useCallback( () => {
if ( getSelectedBlockClientId() === clientId ) {
clearSelectedBlock();
}
}, [ clearSelectedBlock, clientId, getSelectedBlockClientId ] );

const onDelete = useCallback(
( { value, isReverse } ) => {
if ( onMerge ) {
Expand Down Expand Up @@ -590,6 +600,7 @@ export function RichTextWrapper(
disableSuggestions={ disableSuggestions }
disableAutocorrection={ disableAutocorrection }
containerWidth={ containerWidth }
clearCurrentSelectionOnUnmount={ clearCurrentSelectionOnUnmount }
// Props to be set on the editable container are destructured on the
// element itself for web (see below), but passed through rich text
// for native.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,17 @@ export class RichText extends Component {
}
}

componentWillUnmount() {
const { clearCurrentSelectionOnUnmount } = this.props;

// There are cases when the component is unmounted e.g. scrolling in a
// long post due to virtualization, so the block selection needs to be cleared
// so it doesn't auto-focus when it's added back.
if ( this._editor?.isFocused() ) {
clearCurrentSelectionOnUnmount?.();
}
}

getHtmlToRender( record, tagName ) {
// Save back to HTML from React tree.
let value = this.valueToFormat( record );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ import {
*/
import useScroll from './use-scroll';
import KeyboardAvoidingView from '../keyboard-avoiding-view';
import { OPTIMIZATION_ITEMS_THRESHOLD, OPTIMIZATION_PROPS } from './shared';

const AnimatedFlatList = Animated.createAnimatedComponent( FlatList );
const EMPTY_OBJECT = {};

export const KeyboardAwareFlatList = ( { onScroll, ...props }, ref ) => {
const { extraScrollHeight, scrollEnabled, shouldPreventAutomaticScroll } =
Expand All @@ -41,8 +43,6 @@ export const KeyboardAwareFlatList = ( { onScroll, ...props }, ref ) => {

const getFlatListRef = useCallback(
( flatListRef ) => {
// On Android, we get the ref of the associated scroll
// view to the FlatList.
scrollViewRef.current = flatListRef?.getNativeScrollRef();
},
[ scrollViewRef ]
Expand All @@ -57,12 +57,21 @@ export const KeyboardAwareFlatList = ( { onScroll, ...props }, ref ) => {
};
} );

const optimizationProps =
props.data?.length > OPTIMIZATION_ITEMS_THRESHOLD
? OPTIMIZATION_PROPS
: EMPTY_OBJECT;

return (
<KeyboardAvoidingView style={ { flex: 1 } }>
<AnimatedFlatList
ref={ getFlatListRef }
onScroll={ scrollHandler }
onContentSizeChange={ onContentSizeChange }
// Disable clipping to fix focus losing.
// See https://github.com/wordpress-mobile/gutenberg-mobile/pull/741#issuecomment-472746541
removeClippedSubviews={ false }
{ ...optimizationProps }
{ ...props }
/>
</KeyboardAvoidingView>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
/**
* External dependencies
*/

import { ScrollView, FlatList } from 'react-native';
import { FlatList, View } from 'react-native';
import Animated from 'react-native-reanimated';

/**
Expand All @@ -22,9 +21,12 @@ import { useThrottle } from '@wordpress/compose';
import useScroll from './use-scroll';
import useTextInputOffset from './use-text-input-offset';
import useTextInputCaretPosition from './use-text-input-caret-position';
import { OPTIMIZATION_ITEMS_THRESHOLD, OPTIMIZATION_PROPS } from './shared';
import styles from './styles.scss';

const DEFAULT_FONT_SIZE = 16;
const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView );
const AnimatedFlatList = Animated.createAnimatedComponent( FlatList );
const EMPTY_OBJECT = {};

/** @typedef {import('@wordpress/element').RefObject} RefObject */
/**
Expand All @@ -35,7 +37,6 @@ const AnimatedScrollView = Animated.createAnimatedComponent( ScrollView );
* @param {number} props.extraScrollHeight Extra scroll height for the content.
* @param {Function} props.onScroll Function to be called when the list is scrolled.
* @param {boolean} props.scrollEnabled Whether the list can be scrolled.
* @param {Object} props.scrollViewStyle Additional style for the ScrollView component.
* @param {boolean} props.shouldPreventAutomaticScroll Whether to prevent scrolling when there's a Keyboard offset set.
* @param {Object} props... Other props to pass to the FlatList component.
* @param {RefObject} ref
Expand All @@ -46,7 +47,6 @@ export const KeyboardAwareFlatList = (
extraScrollHeight,
onScroll,
scrollEnabled,
scrollViewStyle,
shouldPreventAutomaticScroll,
...props
},
Expand Down Expand Up @@ -105,7 +105,12 @@ export const KeyboardAwareFlatList = (
// extra padding at the bottom.
const contentInset = { bottom: keyboardOffset };

const style = [ { flex: 1 }, scrollViewStyle ];
const getFlatListRef = useCallback(
( flatListRef ) => {
scrollViewRef.current = flatListRef?.getNativeScrollRef();
},
[ scrollViewRef ]
);

useImperativeHandle( ref, () => {
return {
Expand All @@ -116,20 +121,26 @@ export const KeyboardAwareFlatList = (
};
} );

const optimizationProps =
props.data?.length > OPTIMIZATION_ITEMS_THRESHOLD
? OPTIMIZATION_PROPS
: EMPTY_OBJECT;

return (
<AnimatedScrollView
automaticallyAdjustContentInsets={ false }
contentInset={ contentInset }
keyboardShouldPersistTaps="handled"
onContentSizeChange={ onContentSizeChange }
onScroll={ scrollHandler }
ref={ scrollViewRef }
scrollEnabled={ scrollEnabled }
scrollEventThrottle={ 16 }
style={ style }
>
<FlatList { ...props } scrollEnabled={ false } />
</AnimatedScrollView>
<View style={ styles.list__container }>
<AnimatedFlatList
ref={ getFlatListRef }
automaticallyAdjustContentInsets={ false }
contentInset={ contentInset }
keyboardShouldPersistTaps="handled"
onContentSizeChange={ onContentSizeChange }
onScroll={ scrollHandler }
scrollEventThrottle={ 16 }
Copy link
Member

Choose a reason for hiding this comment

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

At some point, we may want to consider exporting the scrollEventThrottle value from a shared constant, as it's used in a few other places. Also not a blocker (since it wasn't a change in this PR), but in context of adding the shared.native.js file for optimization prop values, it may also help provide some context for other developers in the future if all non-default scroll behavior values are exported from the same place.

Copy link
Member Author

Choose a reason for hiding this comment

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

I agree!

style={ styles.list__content }
{ ...optimizationProps }
{ ...props }
/>
</View>
);
};

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const OPTIMIZATION_PROPS = {
windowSize: 17,
maxToRenderPerBatch: 15,
};

export const OPTIMIZATION_ITEMS_THRESHOLD = 30;
Copy link
Member

Choose a reason for hiding this comment

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

Potentially, we may want to leave a note or comment with some further context on these values (as they deviate from the default values). Not a blocker for this PR, however.

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea! Updated in a9dc932

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.list__container {
flex-grow: 1;
align-items: stretch;
}

.list__content {
margin-bottom: $mobile-block-toolbar-height;
}
1 change: 1 addition & 0 deletions packages/react-native-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ For each user feature we should also add a importance categorization label to i
-->

## Unreleased
- [*] KeyboardAwareFlatList - Enable FlatList virtualization for iOS [#59833]

## 1.117.0
- [*] Add empty fallback option for the BottomSheetSelectControl component [#60333]
Expand Down
Loading