Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
3f500db
chore: remove SwipeableView component
wgsquayson Jul 2, 2025
e21b4fa
chore: track chat scroll using reanimated
wgsquayson Jul 4, 2025
ba4acee
chore: use keyboard insets and height value in order to position cont…
wgsquayson Jul 10, 2025
32c34ac
fix: scroll offset logic
wgsquayson Jul 10, 2025
a572423
chore: adapt composer translate bottom spacing, use composer height t…
wgsquayson Jul 11, 2025
d14ea9b
chore: adjust bottom padding value
wgsquayson Jul 11, 2025
1c90717
chore: remove console.log
wgsquayson Jul 11, 2025
bda18df
chore: started handling wrong spacing when keyboard is open
wgsquayson Jul 11, 2025
d2a3f66
chore: adjust android to handle interactive keyboard dismissal
wgsquayson Jul 16, 2025
bfb7f5c
fix: platform on animated style
wgsquayson Jul 16, 2025
2a68097
chore: adjust padding bottom on ios
wgsquayson Jul 16, 2025
24a95dd
chore: keep scroll position when keyboard moves
wgsquayson Jul 18, 2025
3115859
fix: keyboard jumping on scroll
wgsquayson Jul 21, 2025
7e585a1
chore: preserve keyboard position when popovers are shown
wgsquayson Jul 22, 2025
232fe51
chore: handle full size composer on ios
wgsquayson Jul 22, 2025
2e64810
fix: content under keyboard on android when composer was expanded
wgsquayson Jul 23, 2025
5f73410
chore: ensure the full height composer does not overlap the chat header
wgsquayson Jul 23, 2025
ff749be
chore: properly handle composer resizing on ios and android
wgsquayson Jul 23, 2025
1ae6dff
fix: height resizing breaking on android
wgsquayson Jul 23, 2025
8d25791
fix: styling on mobile browsers
wgsquayson Jul 23, 2025
d9876b5
chore: add description to some props and adjust the ActionSheet to be…
wgsquayson Jul 24, 2025
293df6d
refactor: make useReportUnreadMessageScrollTracking an animated handler
wgsquayson Jul 24, 2025
9271b17
chore: add descriptions to new props
wgsquayson Jul 24, 2025
0e2a08d
fix: report screen values after rebase
wgsquayson Jul 24, 2025
404460c
chore: simplify the custom keyboard handler
wgsquayson Jul 24, 2025
c630277
chore: adjust money request report actions list to support interactiv…
wgsquayson Jul 25, 2025
b282a4c
fix: android bottom spacing
wgsquayson Jul 25, 2025
5ddb608
chore: remove calculated height rule
wgsquayson Jul 25, 2025
cfc5516
fix: useReportUnreadMessageScrollTracking hook tests
wgsquayson Jul 25, 2025
04f4c00
chore: implement onStartReached and onEndReached for lists
wgsquayson Jul 28, 2025
3cd52ae
refactor: create KeyboardDismissableFlatList
wgsquayson Aug 4, 2025
61ada5a
chore: remove unecessary onScroll
wgsquayson Aug 5, 2025
79a6f15
chore: remove unused on scroll
wgsquayson Aug 5, 2025
a1cba5d
chore: adjust typing
wgsquayson Aug 5, 2025
7f38bbd
fix: accidental style assignment
wgsquayson Aug 5, 2025
c301c45
fix: remove unecessary composer on layout function
wgsquayson Aug 5, 2025
fafd10f
chore: added animated keyboard handler to android flatlist via prop
wgsquayson Aug 5, 2025
d8e1d34
chore: remove unecessary onScroll usage
wgsquayson Aug 5, 2025
bef0d18
fix: typos
wgsquayson Aug 5, 2025
76bc955
chore: rename keyboard dismissible flatlist files
wgsquayson Aug 5, 2025
936b0ef
chore: remove onScroll from report actions list perf test
wgsquayson Aug 5, 2025
eae5057
chore: replicate keyboard dismissal behavior to MoneyRequestReportView
wgsquayson Aug 6, 2025
c6d01ca
fix: import after rebase
wgsquayson Aug 7, 2025
1f7a076
fix: eslint deprecation error
wgsquayson Aug 7, 2025
ad27ece
chore: adjust offline indicator for ios
wgsquayson Aug 7, 2025
110f238
fix: perf test missing function
wgsquayson Aug 7, 2025
a824dab
chore: mock context return values
wgsquayson Aug 8, 2025
59c7746
chore: move useKeyboardDismissibleFlatList hook into a different file…
wgsquayson Aug 8, 2025
489c57f
chore: remove unecessary comment
wgsquayson Aug 8, 2025
c59c731
chore: remove .only from pagination test
wgsquayson Aug 8, 2025
f5c0061
fix: ReportActionsViewTest mock
wgsquayson Aug 8, 2025
b5a8295
chore: return isComposerFullSize prop
wgsquayson Aug 18, 2025
4227acb
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Aug 27, 2025
201c7a7
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Aug 28, 2025
4b6cb99
chore: adjust children type on ActionSheetKeyboardSpace
wgsquayson Aug 28, 2025
0b0b828
chore: destructure children on KeyboardDismissibleFlatListContext
wgsquayson Aug 28, 2025
544365f
chore: move report footer animated styles to style utils
wgsquayson Aug 28, 2025
9ccc275
chore: create chat header base height const
wgsquayson Aug 28, 2025
4c380ce
chore: move offline indicator styles to style utils
wgsquayson Aug 28, 2025
f57f5cf
chore: move report bottom spacing to style utils
wgsquayson Aug 28, 2025
8cb71da
fix: lint
wgsquayson Aug 28, 2025
e7a525e
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Aug 30, 2025
4a628f4
chore: move scroll to bottom handler to dedicated folder
wgsquayson Sep 3, 2025
ff39b15
chore: move scrollToOffset handler to platform specific code
wgsquayson Sep 3, 2025
ce11117
chore: create floating message counter visibility handler
wgsquayson Sep 3, 2025
c0d42fd
chore: use getPlatform calls instead of Platform.OS declarations
wgsquayson Sep 3, 2025
8a57397
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 3, 2025
93463a4
fix: remove JS function from inside a worklet
wgsquayson Sep 3, 2025
d06d6ed
chore: separate offline indicator styles by platform
wgsquayson Sep 5, 2025
c40b16e
chore: separate report screen variables by platform
wgsquayson Sep 5, 2025
63adc8a
chore: separate report footer styles by platform
wgsquayson Sep 5, 2025
c957bb1
chore: separate report padding bottom styles by platform
wgsquayson Sep 6, 2025
eba2240
chore: separate scrolling vertical offset ref usage by platform
wgsquayson Sep 6, 2025
44313f1
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 8, 2025
61adb5e
chore: adjust useScrollingVerticalOffserRef after merge commit
wgsquayson Sep 8, 2025
a0ea5aa
fix: disable keyboard avoiding view on ios
wgsquayson Sep 8, 2025
62ea301
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 8, 2025
93037df
fix: ts error
wgsquayson Sep 8, 2025
7ce5206
chore: remove padding bottom from report on non-ios devices
wgsquayson Sep 8, 2025
753a39c
fix: ts error
wgsquayson Sep 8, 2025
d12d9b6
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 10, 2025
c80c3a5
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 10, 2025
a841aa8
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 15, 2025
14cb458
Merge remote-tracking branch 'upstream/main' into feat/interactive-ke…
wgsquayson Sep 15, 2025
81cae46
Merge branch 'main' into feat/interactive-keyboard-dismissal
chrispader Sep 25, 2025
59355ed
fix: remove outdated forwardRef
chrispader Sep 25, 2025
c6168f5
fix: properly annotate copy of Reanimated FlatList
chrispader Sep 25, 2025
0423655
fix: add back eslint-disable-next-line for createAnimatedComponent(Fl…
chrispader Sep 25, 2025
0dbbfa2
fix: invalid import
chrispader Sep 25, 2025
a28281a
fix: prevent scroll when dragging down keyboard
chrispader Sep 26, 2025
85a568b
refactor: move file
chrispader Sep 26, 2025
6b50c99
refactor: rename `withAnimatedKeyboardHandler` prop to `shouldUseAnim…
chrispader Sep 26, 2025
2c4e753
fix: remove propTypes and add displayName
chrispader Sep 27, 2025
5cd41a2
refactor: move original implementation and mock to sub folder
chrispader Sep 27, 2025
a880f18
refactor: KeyboardDismissableFlatListContext structure
chrispader Sep 27, 2025
fc86471
refactor: extract ListBehavior values to CONST
chrispader Sep 27, 2025
54abab8
refactor: improve comment and rename prop
chrispader Sep 27, 2025
a524698
refactor: usePreventScrollOnKeyboardInteraction hook and usage
chrispader Sep 27, 2025
dfe3a7c
refactor: make enableAnimatedKeyboardDismissal `false` by default and…
chrispader Sep 27, 2025
df21aeb
refactor: CustomAnimatedFlatList and hook imports
chrispader Sep 27, 2025
cf78831
refactor: restructure BaseInvertedFlatListProps
chrispader Sep 27, 2025
0bd0e0a
refactor: remove extra import
chrispader Sep 27, 2025
0626f7b
refactor: rename and refactor offline indicator style chanes
chrispader Sep 27, 2025
dc3d009
refactor: InvertedFlatList event handlers
chrispader Sep 27, 2025
861ddc8
refactor: rename style util function
chrispader Sep 27, 2025
a6359d9
refactor: rename report footer styles
chrispader Sep 27, 2025
cbd8310
Merge branch 'main' into feat/interactive-keyboard-dismissal
chrispader Sep 27, 2025
7786ee8
refactor: scroll handler usage throughout multiple list components
chrispader Sep 27, 2025
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
2 changes: 2 additions & 0 deletions jest/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ jest.mock('react-native-nitro-sqlite', () => ({
open: jest.fn(),
}));

jest.mock('@src/hooks/useKeyboardDismissibleFlatListValues/index.ts');

// Provide a default global fetch mock for tests that do not explicitly set it up
// This avoids ReferenceError: fetch is not defined in CI when coverage is enabled
const globalWithOptionalFetch: typeof globalThis & {fetch?: unknown} = globalThis as typeof globalThis & {fetch?: unknown};
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import FullScreenLoaderContextProvider from './components/FullScreenLoaderContex
import HTMLEngineProvider from './components/HTMLEngineProvider';
import InitialURLContextProvider from './components/InitialURLContextProvider';
import {InputBlurContextProvider} from './components/InputBlurContext';
import {KeyboardDismissibleFlatListContextProvider} from './components/KeyboardDismissibleFlatList/KeyboardDismissibleFlatListContext';
import KeyboardProvider from './components/KeyboardProvider';
import {LocaleContextProvider} from './components/LocaleContextProvider';
import NavigationBar from './components/NavigationBar';
Expand Down Expand Up @@ -115,6 +116,7 @@ function App() {
VideoPopoverMenuContextProvider,
KeyboardProvider,
KeyboardStateProvider,
KeyboardDismissibleFlatListContextProvider,
SearchRouterContextProvider,
ProductTrainingContextProvider,
InputBlurContextProvider,
Expand Down
7 changes: 7 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2045,6 +2045,8 @@ const CONST = {
BIG_SCREEN_SUGGESTION_WIDTH: 300,
},
COMPOSER_MAX_HEIGHT: 125,
// Starts with this value to avoid a big jump while the container height is being calculated in case the screen is first rendered w/ a full size composer. It's based on the perceived concierge header height on the iPhone 16 Pro.
CHAT_HEADER_BASE_HEIGHT: 73,
CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15,
CHAT_FOOTER_SECONDARY_ROW_PADDING: 5,
CHAT_FOOTER_MIN_HEIGHT: 65,
Expand Down Expand Up @@ -7261,6 +7263,11 @@ const CONST = {
REVIEW_WORKSPACE_SETTINGS: 'reviewWorkspaceSettings',
INVITE_ACCOUNTANT: 'inviteAccountant',
},

LIST_BEHAVIOR: {
REGULAR: 'regular',
INVERTED: 'inverted',
},
} as const;

const CONTINUATION_DETECTION_SEARCH_FILTER_KEYS = [
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React, {forwardRef, useCallback} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {ScrollView} from 'react-native';
import Reanimated, {useAnimatedRef, useAnimatedStyle} from 'react-native-reanimated';
import type {AnimatedScrollView} from 'react-native-reanimated/lib/typescript/component/ScrollView';
import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext';
import type {ActionSheetAwareScrollViewProps, RenderActionSheetAwareScrollViewComponent} from './types';
import type {ActionSheetAwareScrollViewProps} from './types';
import useActionSheetKeyboardSpacing from './useActionSheetKeyboardSpacing';
import usePreventScrollOnKeyboardInteraction from './usePreventScrollOnKeyboardInteraction';

const ActionSheetAwareScrollView = forwardRef<ScrollView, ActionSheetAwareScrollViewProps>(({style, children, ...props}, ref) => {
const ActionSheetAwareScrollView = forwardRef<AnimatedScrollView, ActionSheetAwareScrollViewProps>(({style, children, onScroll: onScrollProp, ...props}, ref) => {
const scrollViewAnimatedRef = useAnimatedRef<Reanimated.ScrollView>();

const onRef = useCallback(
Expand All @@ -28,11 +29,14 @@ const ActionSheetAwareScrollView = forwardRef<ScrollView, ActionSheetAwareScroll
paddingTop: spacing.get(),
}));

const {onScroll} = usePreventScrollOnKeyboardInteraction({scrollViewRef: scrollViewAnimatedRef, onScroll: onScrollProp});

return (
<Reanimated.ScrollView
ref={onRef}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={onRef}
onScroll={onScroll}
style={[style, animatedStyle]}
>
{children}
Expand All @@ -47,7 +51,7 @@ export default ActionSheetAwareScrollView;
* @param props - props that will be passed to the ScrollView from FlatList
* @returns - ActionSheetAwareScrollView
*/
const renderScrollComponent: RenderActionSheetAwareScrollViewComponent = (props) => {
const renderScrollComponent = (props: ActionSheetAwareScrollViewProps) => {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ActionSheetAwareScrollView {...props} />;
};
Expand Down
45 changes: 30 additions & 15 deletions src/components/ActionSheetAwareScrollView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,45 @@
// this whole file is just for other platforms
// iOS version has everything implemented
import React, {forwardRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import {ScrollView} from 'react-native';
import type {AnimatedRef} from 'react-native-reanimated';
import Animated, {useAnimatedRef} from 'react-native-reanimated';
import type Reanimated from 'react-native-reanimated';
import type {AnimatedScrollView} from 'react-native-reanimated/lib/typescript/component/ScrollView';
import {Actions, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider} from './ActionSheetAwareScrollViewContext';
import type {ActionSheetAwareScrollViewProps, RenderActionSheetAwareScrollViewComponent} from './types';
import type {ActionSheetAwareScrollViewProps} from './types';
import usePreventScrollOnKeyboardInteraction from './usePreventScrollOnKeyboardInteraction';

const ActionSheetAwareScrollView = forwardRef<ScrollView, ActionSheetAwareScrollViewProps>((props, ref) => (
<ScrollView
ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
{props.children}
</ScrollView>
));
const ActionSheetAwareScrollView = forwardRef<AnimatedScrollView, ActionSheetAwareScrollViewProps>(({onScroll: onScrollProp, ...props}, ref) => {
const fallbackRef = useAnimatedRef<Reanimated.ScrollView>();
const combinedRef = ref ?? fallbackRef;

const {onScroll} = usePreventScrollOnKeyboardInteraction({scrollViewRef: combinedRef as AnimatedRef<Reanimated.ScrollView>, onScroll: onScrollProp});

return (
<Animated.ScrollView
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={combinedRef}
onScroll={onScroll}
>
{props.children}
</Animated.ScrollView>
);
});

export default ActionSheetAwareScrollView;

/**
* This is only used on iOS. On other platforms it's just undefined to be pass a prop to FlatList
* The bottom spacing config for this action sheet is only used on Android and iOS. On other platforms,
* this component will be a default Animated.ScrollView, because the onScroll handler used is from Reanimated.
*
* This function should be used as renderScrollComponent prop for FlatList
* @param {Object} props - props that will be passed to the ScrollView from FlatList
* @returns {React.ReactElement} - ActionSheetAwareScrollView
*/
const renderScrollComponent: RenderActionSheetAwareScrollViewComponent = undefined;

function renderScrollComponent(props: ActionSheetAwareScrollViewProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return <ActionSheetAwareScrollView {...props} />;
}

export {renderScrollComponent, ActionSheetAwareScrollViewContext, ActionSheetAwareScrollViewProvider, Actions};
14 changes: 9 additions & 5 deletions src/components/ActionSheetAwareScrollView/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type {PropsWithChildren} from 'react';
import type {ScrollViewProps} from 'react-native';
import type {ReactNode} from 'react';
import type {AnimatedScrollViewProps, ScrollHandlerProcessed, SharedValue} from 'react-native-reanimated';

type ActionSheetAwareScrollViewProps = PropsWithChildren<ScrollViewProps>;
type RenderActionSheetAwareScrollViewComponent = ((props: ActionSheetAwareScrollViewProps) => React.ReactElement<ScrollViewProps>) | undefined;
export type {ActionSheetAwareScrollViewProps, RenderActionSheetAwareScrollViewComponent};
type ActionSheetAwareScrollViewProps = Omit<AnimatedScrollViewProps, 'onScroll'> & {
children?: ReactNode | SharedValue<ReactNode>;
onScroll?: ScrollHandlerProcessed<Record<string, unknown>>;
};

// eslint-disable-next-line import/prefer-default-export
export type {ActionSheetAwareScrollViewProps};
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import {useKeyboardHandler} from 'react-native-keyboard-controller';
import type {AnimatedRef, ScrollHandlerProcessed} from 'react-native-reanimated';
import {scrollTo, useAnimatedScrollHandler, useComposedEventHandler, useSharedValue} from 'react-native-reanimated';
import type Reanimated from 'react-native-reanimated';

function usePreventScrollOnKeyboardInteraction({
scrollViewRef,
onScroll: onScrollProp,
}: {
scrollViewRef: AnimatedRef<Reanimated.ScrollView>;
onScroll?: ScrollHandlerProcessed<Record<string, unknown>>;
}) {
// Receive the latest scroll position whenever the content is scrolled
const scroll = useSharedValue(0);
const onScrollInternal = useAnimatedScrollHandler({
onScroll: (e) => {
scroll.set(e.contentOffset.y);
},
});

// Scroll to the latest scroll position whenever the keyboard is interacted with,
// to prevent additional scrolling when the keyboard is interactively dismissed.
useKeyboardHandler({
onInteractive: () => {
scrollTo(scrollViewRef, 0, scroll.get(), false);
},
});

const onScroll = useComposedEventHandler([onScrollInternal, onScrollProp ?? null]);

return {onScroll};
}

export default usePreventScrollOnKeyboardInteraction;
122 changes: 122 additions & 0 deletions src/components/AnimatedFlatListWithCellRenderer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* This is a copy of the FlatList implementation from 'react-native-reanimated' in order to implement a custom CellRendererComponent.
* This should be updated when the original implementation updates
* Taken from: https://github.com/software-mansion/react-native-reanimated/blob/main/packages/react-native-reanimated/src/component/FlatList.tsx
*/
import React, {forwardRef, useRef} from 'react';
import type {FlatListProps, NativeScrollEvent, NativeSyntheticEvent, CellRendererProps as RNCellRendererProps} from 'react-native';
import {FlatList} from 'react-native';
import type {AnimatedProps, ILayoutAnimationBuilder, ScrollHandlerProcessed} from 'react-native-reanimated';
import Animated, {LayoutAnimationConfig} from 'react-native-reanimated';

// eslint-disable-next-line deprecation/deprecation
const AnimatedFlatList = Animated.createAnimatedComponent(FlatList);

type CellRendererComponentProps<T> = React.ComponentType<RNCellRendererProps<T>> | null | undefined;

const createCellRendererComponent = <Item,>(CellRendererComponentProp?: CellRendererComponentProps<Item>, itemLayoutAnimationRef?: React.RefObject<ILayoutAnimationBuilder | undefined>) => {
// Make CellRendererComponent specifically use the 'Item' type from its parent scope
function CellRendererComponent(props: RNCellRendererProps<Item>) {
return (
<Animated.View
// TODO TYPESCRIPT This is temporary cast is to get rid of .d.ts file.
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-explicit-any
layout={itemLayoutAnimationRef?.current as any}
onLayout={props.onLayout}
style={CellRendererComponentProp ? undefined : props.style}
>
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
{CellRendererComponentProp ? <CellRendererComponentProp {...props}>{props.children}</CellRendererComponentProp> : props.children}
</Animated.View>
);
}

return CellRendererComponent;
};
type ReanimatedFlatListPropsWithLayout<T> = {
/**
* Lets you pass layout animation directly to the FlatList item.
*/
itemLayoutAnimation?: ILayoutAnimationBuilder;
/**
* Lets you skip entering and exiting animations of FlatList items when on FlatList mount or unmount.
*/
skipEnteringExitingAnimations?: boolean;
} & AnimatedProps<FlatListProps<T>>;

// Since createAnimatedComponent return type is ComponentClass that has the props of the argument,
// but not things like NativeMethods, etc. we need to add them manually by extending the type.
type AnimatedFlatListComplement<T> = {
getNode(): FlatList<T>;
} & FlatList<T>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type AnimatedFlatListWithCellRendererProps<Item = any> = Omit<ReanimatedFlatListPropsWithLayout<Item>, 'CellRendererComponent' | 'onScroll'> & {
CellRendererComponent?: CellRendererComponentProps<Item>;
onScroll?: ScrollHandlerProcessed<Record<string, unknown>>;
additionalOnScrollHandler?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
};

// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function FlatListForwardRefRender<Item = any>(props: AnimatedFlatListWithCellRendererProps<Item>, ref: React.ForwardedRef<FlatList>) {
const {itemLayoutAnimation, skipEnteringExitingAnimations, ...restProps} = props;

// Set default scrollEventThrottle, because user expects
// to have continuous scroll events and
// react-native defaults it to 50 for FlatLists.
// We set it to 1, so we have peace until
// there are 960 fps screens.
if (!('scrollEventThrottle' in restProps)) {
// eslint-disable-next-line react-compiler/react-compiler
restProps.scrollEventThrottle = 1;
}

const itemLayoutAnimationRef = useRef(itemLayoutAnimation);
itemLayoutAnimationRef.current = itemLayoutAnimation;

const CellRendererComponent = React.useMemo(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
() => createCellRendererComponent<any>(props.CellRendererComponent, itemLayoutAnimationRef),
[itemLayoutAnimationRef, props.CellRendererComponent],
);

const animatedFlatList = (
// @ts-expect-error In its current type state, createAnimatedComponent cannot create generic components.
<AnimatedFlatList
ref={ref}
// eslint-disable-next-line react/jsx-props-no-spreading
{...restProps}
CellRendererComponent={CellRendererComponent}
/>
);

if (skipEnteringExitingAnimations === undefined) {
return animatedFlatList;
}

return (
<LayoutAnimationConfig
skipEntering
skipExiting
>
{animatedFlatList}
</LayoutAnimationConfig>
);
}

const AnimatedFlatListWithCellRenderer = forwardRef(FlatListForwardRefRender) as <
// We need explicit any here, because this is the exact same type that is used in React Native types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ItemT = any,
>(
props: AnimatedFlatListWithCellRendererProps<ItemT> & {
ref?: React.ForwardedRef<FlatList>;
},
) => React.ReactElement;

type ReanimatedFlatList<T> = typeof AnimatedFlatList & AnimatedFlatListComplement<T>;

export type {ReanimatedFlatList, AnimatedFlatListWithCellRendererProps};

export default AnimatedFlatListWithCellRenderer;
16 changes: 14 additions & 2 deletions src/components/FlatList/index.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import {useFocusEffect} from '@react-navigation/native';
import React, {useCallback, useRef} from 'react';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {FlatList} from 'react-native';
import type {CustomFlatListProps} from './index';
import KeyboardDismissibleFlatList from '@components/KeyboardDismissibleFlatList';
import type {CustomFlatListProps} from './types';

// FlatList wrapped with the freeze component will lose its scroll state when frozen (only for Android).
// CustomFlatList saves the offset and use it for scrollToOffset() when unfrozen.
function CustomFlatList<T>({ref, ...props}: CustomFlatListProps<T>) {
function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, ...props}: CustomFlatListProps<T>) {
const lastScrollOffsetRef = useRef(0);

const onScreenFocus = useCallback(() => {
Expand All @@ -32,6 +33,17 @@ function CustomFlatList<T>({ref, ...props}: CustomFlatListProps<T>) {
}, [onScreenFocus]),
);

if (enableAnimatedKeyboardDismissal) {
return (
<KeyboardDismissibleFlatList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
ref={ref}
onMomentumScrollEnd={onMomentumScrollEnd}
/>
);
}

return (
<FlatList<T>
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down
Loading
Loading