Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7fc2c0c
feat: migrate lists to interactive keyboard dismissal
chrispader Sep 28, 2025
2617c04
fix: typo
chrispader Sep 28, 2025
60534f4
Merge branch 'main' into pr/71440
chrispader Oct 16, 2025
6ba7f7d
fix: invalid import
chrispader Oct 16, 2025
41febe0
Merge branch 'main' into pr/71440
chrispader Oct 17, 2025
0dfa732
Merge branch 'main' into pr/71440
chrispader Oct 28, 2025
4762b4c
Merge branch 'main' into pr/71440
chrispader Jan 28, 2026
85942a8
refactor: adapt report footer style types
chrispader Jan 29, 2026
23c95a4
refactor: types of `getReportPaddingBottom`
chrispader Jan 29, 2026
30f12da
refactor: extract variable for more readability
chrispader Jan 29, 2026
4e9dc64
fix: invalid import
chrispader Jan 29, 2026
d32d875
fix: remove unused import
chrispader Jan 29, 2026
6e3b74c
refactor: match condition on main
chrispader Jan 29, 2026
153886d
fix: use platform specific `shouldEnableKeyboardAvoidingViewResult`
chrispader Jan 29, 2026
4ea116f
refactor: evaluate keyboardHeight once in style util function
chrispader Jan 29, 2026
7cf181e
refactor: update relative import
chrispader Jan 29, 2026
63148a9
refactor: `nativeID` properties
chrispader Jan 29, 2026
a4e17d1
refactor: changes from main
chrispader Jan 29, 2026
36ce4da
fix: enable animated keyboard dismissal in ReportActionsList
chrispader Jan 29, 2026
efc4cb0
fix: RNKC invalid `onMove` and `onInteractive` events
chrispader Jan 29, 2026
95a6510
fix: don't completely block `onInteractive` events
chrispader Jan 29, 2026
59d5516
add RNKC issue link
chrispader Jan 29, 2026
51bdf1f
fix: missing `onLayout` prop in tests
chrispader Jan 29, 2026
16e950e
fix: spell check
chrispader Jan 29, 2026
dd6ec32
fix: remove deprecated `runOnJS`
chrispader Jan 29, 2026
3a281e2
fix: worklet callback
chrispader Jan 29, 2026
80e06eb
fix: pass missing `inverted` property on Android
chrispader Jan 29, 2026
1a36843
fix: remove unnecessary web only composer scroll events
chrispader Jan 29, 2026
71daa9d
chore: add `react-native-keyboard-controller` patch for invalid `onIn…
chrispader Jan 30, 2026
ed83e15
fix: remove `onInteractive` workaround
chrispader Jan 30, 2026
a0ffcd7
move patch and add `details.md`
chrispader Jan 30, 2026
eee157d
remove constant
chrispader Jan 30, 2026
6c96aad
Merge branch 'main' into pr/71440
chrispader Feb 3, 2026
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
8 changes: 8 additions & 0 deletions patches/react-native-keyboard-controller/details.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# `react-native-keyboard-controller` patches

### [react-native-keyboard-controller+1.20.7+001+fix-invalid-onInteractive-event-calls.patch](react-native-keyboard-controller+1.20.7+001+fix-invalid-onInteractive-event-calls.patch)

- Reason: Fixes an issue where `react-native-keyboard-controller` sends invalid `onInteractive` events in `useKeyboardHandler`
- Upstream PR/issue: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1298
- E/App issue: 🛑
- PR Introducing Patch: https://github.com/Expensify/App/pull/71440
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
diff --git a/node_modules/react-native-keyboard-controller/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift b/node_modules/react-native-keyboard-controller/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift
index d1ccd45..0695b74 100644
--- a/node_modules/react-native-keyboard-controller/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift
+++ b/node_modules/react-native-keyboard-controller/ios/observers/movement/observer/KeyboardMovementObserver+Interactive.swift
@@ -30,6 +30,10 @@ extension KeyboardMovementObserver {
return
}

+ if KeyboardEventsIgnorer.shared.shouldIgnore {
+ return
+ }
+
let position = keyboardTrackingView.interactive(point: changeValue)

if position == KeyboardTrackingView.invalidPosition {
2 changes: 2 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,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
3 changes: 2 additions & 1 deletion src/components/Composer/implementation/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function Composer({
selection,
value,
isGroupPolicyReport = false,
nativeID,
ref,
...props
}: ComposerProps) {
Expand Down Expand Up @@ -112,7 +113,7 @@ function Composer({

return (
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
id={nativeID ?? CONST.COMPOSER.NATIVE_ID}
multiline
autoComplete="off"
placeholderTextColor={theme.placeholderText}
Expand Down
3 changes: 2 additions & 1 deletion src/components/Composer/implementation/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ function Composer({
onContentSizeChange,
shouldContainScroll = true,
isGroupPolicyReport = false,
nativeID,
ref,
...props
}: ComposerProps) {
Expand Down Expand Up @@ -339,7 +340,7 @@ function Composer({

return (
<RNMarkdownTextInput
id={CONST.COMPOSER.NATIVE_ID}
nativeID={nativeID ?? CONST.COMPOSER.NATIVE_ID}
autoComplete="off"
autoCorrect={!isMobileSafari()}
placeholderTextColor={theme.placeholderText}
Expand Down
7 changes: 4 additions & 3 deletions src/components/FlatList/FlatListWithScrollKey/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type {ForwardedRef} from 'react';
import type {FlatListProps, ListRenderItem, FlatList as RNFlatList} from 'react-native';
import type {ListRenderItem, FlatList as RNFlatList} from 'react-native';
import type {CustomFlatListProps} from '@components/FlatList/types';

type BaseFlatListWithScrollKeyProps<T> = Omit<FlatListProps<T>, 'data' | 'initialScrollIndex' | 'onContentSizeChange'> & {
type BaseFlatListWithScrollKeyProps<T> = Omit<CustomFlatListProps<T>, 'data' | 'initialScrollIndex' | 'onContentSizeChange'> & {
data: T[];
initialScrollKey?: string | null | undefined;
keyExtractor: (item: T, index: number) => string;
Expand All @@ -11,6 +12,6 @@ type BaseFlatListWithScrollKeyProps<T> = Omit<FlatListProps<T>, 'data' | 'initia
ref: ForwardedRef<RNFlatList>;
};

type FlatListWithScrollKeyProps<T> = Omit<BaseFlatListWithScrollKeyProps<T>, 'onContentSizeChange'> & Pick<FlatListProps<T>, 'onContentSizeChange'>;
type FlatListWithScrollKeyProps<T> = Omit<BaseFlatListWithScrollKeyProps<T>, 'onContentSizeChange'> & Pick<CustomFlatListProps<T>, 'onContentSizeChange'>;

export type {FlatListWithScrollKeyProps, BaseFlatListWithScrollKeyProps};
8 changes: 4 additions & 4 deletions src/components/FlatList/index.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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, enableAnimatedKeyboardDismissal = false, onMomentumScrollEnd, shouldHideContent = false, ...props}: CustomFlatListProps<T>) {
function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMomentumScrollEnd, shouldHideContent = false, ...restProps}: CustomFlatListProps<T>) {
const lastScrollOffsetRef = useRef(0);
const styles = useThemeStyles();

Expand Down Expand Up @@ -39,13 +39,13 @@ function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMome
}, [onScreenFocus]),
);

const contentContainerStyle = [props.contentContainerStyle, shouldHideContent && styles.opacity0];
const contentContainerStyle = [restProps.contentContainerStyle, shouldHideContent && styles.opacity0];

if (enableAnimatedKeyboardDismissal) {
return (
<KeyboardDismissibleFlatList
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
{...restProps}
ref={ref}
onMomentumScrollEnd={handleScrollEnd}
contentContainerStyle={contentContainerStyle}
Expand All @@ -56,7 +56,7 @@ function CustomFlatList<T>({ref, enableAnimatedKeyboardDismissal = false, onMome
return (
<FlatList<T>
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
{...restProps}
ref={ref}
onMomentumScrollEnd={handleScrollEnd}
contentContainerStyle={contentContainerStyle}
Expand Down
12 changes: 1 addition & 11 deletions src/components/FlatList/index.ios.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, {useCallback, useState} from 'react';
import type {NativeScrollEvent, NativeSyntheticEvent} from 'react-native';
import {FlatList} from 'react-native';
import KeyboardDismissibleFlatList from '@components/KeyboardDismissibleFlatList';
import useEmitComposerScrollEvents from '@hooks/useEmitComposerScrollEvents';
import useThemeStyles from '@hooks/useThemeStyles';
import type {CustomFlatListProps} from './types';

Expand Down Expand Up @@ -37,15 +36,6 @@ function CustomFlatList<T>({
[onMomentumScrollEnd],
);

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: !enableAnimatedKeyboardDismissal, inverted: restProps.inverted});
const handleScroll = useCallback(
(e: NativeSyntheticEvent<NativeScrollEvent>) => {
onScrollProp?.(e);
emitComposerScrollEvents();
},
[emitComposerScrollEvents, onScrollProp],
);

const maintainVisibleContentPosition = isScrolling || shouldDisableVisibleContentPosition ? undefined : maintainVisibleContentPositionProp;

const contentContainerStyle = [restProps.contentContainerStyle, shouldHideContent && styles.opacity0];
Expand All @@ -72,7 +62,7 @@ function CustomFlatList<T>({
{...restProps}
ref={ref}
maintainVisibleContentPosition={maintainVisibleContentPosition}
onScroll={handleScroll}
onScroll={onScrollProp}
onMomentumScrollBegin={handleScrollBegin}
onMomentumScrollEnd={handleScrollEnd}
contentContainerStyle={contentContainerStyle}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,18 @@ function KeyboardDismissibleFlatListContextProvider({children}: PropsWithChildre
const contentSizeHeight = useSharedValue(0);
const layoutMeasurementHeight = useSharedValue(0);

const isKeyboardOpening = useSharedValue(false);

useKeyboardHandler({
onStart: (e) => {
'worklet';

const scrollYValueAtStart = scrollY.get();
const prevHeight = height.get();

height.set(e.height);

const willKeyboardOpen = e.progress === 1;
isKeyboardOpening.set(willKeyboardOpen);


if (willKeyboardOpen) {
if (e.height > 0) {
Expand Down Expand Up @@ -90,6 +92,7 @@ function KeyboardDismissibleFlatListContextProvider({children}: PropsWithChildre
onInteractive: (e) => {
'worklet';


height.set(e.height);

if (listBehavior === CONST.LIST_BEHAVIOR.REGULAR) {
Expand All @@ -101,6 +104,12 @@ function KeyboardDismissibleFlatListContextProvider({children}: PropsWithChildre
onMove: (e) => {
'worklet';

// This is to fix an issue with react-native-keyboard-controller, where an `onMove` event is triggered with an invalid height value when the keyboard is opened
// react-native-keyboard-controller issue: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1298
if (isKeyboardOpening.get() && e.height < height.get()) {
return;
}

height.set(e.height);
},
onEnd: (e) => {
Expand Down
12 changes: 3 additions & 9 deletions src/components/KeyboardDismissibleFlatList/index.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
import React from 'react';
import {useAnimatedScrollHandler, useComposedEventHandler} from 'react-native-reanimated';
import {useComposedEventHandler} from 'react-native-reanimated';
import type {AnimatedFlatListWithCellRendererProps} from '@components/AnimatedFlatListWithCellRenderer';
import AnimatedFlatListWithCellRenderer from '@components/AnimatedFlatListWithCellRenderer';
import useEmitComposerScrollEvents from '@hooks/useEmitComposerScrollEvents';
import {useKeyboardDismissibleFlatListActions} from './KeyboardDismissibleFlatListContext';

function KeyboardDismissibleFlatList<T>({onScroll: onScrollProp, inverted, ref, ...restProps}: AnimatedFlatListWithCellRendererProps<T>) {
const {onScroll: onScrollHandleKeyboard} = useKeyboardDismissibleFlatListActions();

const emitComposerScrollEvents = useEmitComposerScrollEvents({enabled: true, inverted});

const additionalOnScroll = useAnimatedScrollHandler({
onScroll: emitComposerScrollEvents,
});

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

return (
<AnimatedFlatListWithCellRenderer
// eslint-disable-next-line react/jsx-props-no-spreading
{...restProps}
inverted={inverted}
ref={ref}
onScroll={onScroll}
/>
Expand Down
Loading
Loading