Skip to content
Draft
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,45 @@
*/
public class InvertedScrollContentView extends ReactViewGroup {

private boolean mIsInvertedContent = false;

public InvertedScrollContentView(android.content.Context context) {
super(context);
}

public void setIsInvertedContent(boolean isInverted) {
mIsInvertedContent = isInverted;
}

@Override
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
super.addChildrenForAccessibility(outChildren);
Collections.reverse(outChildren);
if (mIsInvertedContent) {
Collections.reverse(outChildren);
}
}

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
super.addFocusables(views, direction, focusableMode);
if (mIsInvertedContent) {
// Find indices of focusables that are children of this view
ArrayList<Integer> childIndices = new ArrayList<>();
for (int i = 0; i < views.size(); i++) {
View v = views.get(i);
if (v.getParent() == this) {
childIndices.add(i);
}
}
// Reverse only the sublist of children focusables
int n = childIndices.size();
for (int i = 0; i < n / 2; i++) {
int idx1 = childIndices.get(i);
int idx2 = childIndices.get(n - 1 - i);
View temp = views.get(idx1);
views.set(idx1, views.get(idx2));
views.set(idx2, temp);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.views.view.ReactViewManager;

/**
Expand All @@ -22,4 +23,9 @@ public String getName() {
public InvertedScrollContentView createViewInstance(ThemedReactContext context) {
return new InvertedScrollContentView(context);
}

@ReactProp(name = "isInvertedContent")
public void setIsInvertedContent(InvertedScrollContentView view, boolean isInverted) {
view.setIsInvertedContent(isInverted);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

// When a FlatList is inverted (inverted={true}), React Native uses scaleY: -1 transform which
// visually inverts the list but Android still reports children in array order. This view overrides
// addChildrenForAccessibility to reverse the order so TalkBack matches the visual order.
// addChildrenForAccessibility to reverse the order so TalkBack matches the visual order, and also
// adjusts keyboard/D-pad focus navigation to behave like a non-inverted list.

public class InvertedScrollView extends ReactScrollView {

Expand All @@ -21,11 +22,54 @@ public InvertedScrollView(ReactContext context) {

// Set whether this ScrollView is used for an inverted virtualized list. When true, we reverse the
// accessibility traversal order to match the visual order.

public void setIsInvertedVirtualizedList(boolean isInverted) {
mIsInvertedVirtualizedList = isInverted;
}

@Override
public View focusSearch(View focused, int direction) {
if (mIsInvertedVirtualizedList) {
switch (direction) {
case View.FOCUS_DOWN:
direction = View.FOCUS_UP;
break;
case View.FOCUS_UP:
direction = View.FOCUS_DOWN;
break;
case View.FOCUS_FORWARD:
direction = View.FOCUS_BACKWARD;
break;
case View.FOCUS_BACKWARD:
direction = View.FOCUS_FORWARD;
break;
default:
break;
}
}
return super.focusSearch(focused, direction);
}

@Override
public void addFocusables(ArrayList<View> views, int direction, int focusableMode) {
int initialSize = views.size();

super.addFocusables(views, direction, focusableMode);

if (!mIsInvertedVirtualizedList) {
return;
}

if (direction == View.FOCUS_FORWARD || direction == View.FOCUS_BACKWARD) {
int newSize = views.size();
int addedCount = newSize - initialSize;
if (addedCount > 1) {
java.util.List<View> subList = views.subList(initialSize, newSize);
Collections.reverse(subList);
}
}
}

@Override
public void addChildrenForAccessibility(ArrayList<View> outChildren) {
super.addChildrenForAccessibility(outChildren);
Expand Down
16 changes: 10 additions & 6 deletions app/views/RoomView/List/components/InvertedScrollView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const styles = StyleSheet.create({
});

type ScrollViewPropsWithRef = ScrollViewProps & React.RefAttributes<NativeScrollInstance | null>;
type InvertedScrollViewProps = ScrollViewProps & { inverted?: boolean };
type NativeScrollInstance = React.ComponentRef<NonNullable<typeof NativeInvertedScrollView>>;
interface IScrollableMethods {
scrollTo(options?: { x?: number; y?: number; animated?: boolean }): void;
Expand All @@ -44,11 +45,11 @@ export type InvertedScrollViewRef = NativeScrollInstance & IScrollableMethods;

const NativeInvertedScrollView = requireNativeComponent<ScrollViewProps>('InvertedScrollView');

const NativeInvertedScrollContentView = requireNativeComponent<ViewProps & { removeClippedSubviews?: boolean }>(
'InvertedScrollContentView'
);
const NativeInvertedScrollContentView = requireNativeComponent<
ViewProps & { removeClippedSubviews?: boolean; isInvertedContent?: boolean }
>('InvertedScrollContentView');

const InvertedScrollView = forwardRef<InvertedScrollViewRef, ScrollViewProps>((props, externalRef) => {
const InvertedScrollView = forwardRef<InvertedScrollViewRef, InvertedScrollViewProps>((props, externalRef) => {
const internalRef = useRef<NativeScrollInstance | null>(null);

useLayoutEffect(() => {
Expand Down Expand Up @@ -136,13 +137,16 @@ const InvertedScrollView = forwardRef<InvertedScrollViewRef, ScrollViewProps>((p
return null;
}
const ScrollView = NativeInvertedScrollView as React.ComponentType<ScrollViewPropsWithRef>;
const ContentView = NativeInvertedScrollContentView as React.ComponentType<ViewProps & { removeClippedSubviews?: boolean }>;

const ContentView = NativeInvertedScrollContentView as React.ComponentType<
ViewProps & { removeClippedSubviews?: boolean; isInvertedContent?: boolean }
>;
console.log('props.inverted', props.inverted);
return (
<ScrollView ref={setRef} {...restWithoutStyle} style={StyleSheet.compose(baseStyle, style)} horizontal={horizontal}>
<ContentView
{...contentSizeChangeProps}
removeClippedSubviews={hasStickyHeaders ? false : removeClippedSubviews}
isInvertedContent={!!props.inverted}
collapsable={false}
collapsableChildren={!preserveChildren}
style={contentContainerStyleArray as StyleProp<ViewStyle>}>
Expand Down
45 changes: 41 additions & 4 deletions app/views/RoomView/List/components/List.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Platform, StyleSheet, View } from 'react-native';
import Animated, { runOnJS, useAnimatedScrollHandler } from 'react-native-reanimated';

import { isIOS } from '../../../../lib/methods/helpers';
Expand All @@ -22,6 +22,41 @@ const styles = StyleSheet.create({
const List = ({ listRef, jumpToBottom, ...props }: IListProps) => {
const [visible, setVisible] = useState(false);
const { isAutocompleteVisible } = useRoomContext();
const { data, renderItem, ...flatListProps } = props;

const renderItemWithFocus: IListProps['renderItem'] = info => {
if (!renderItem) {
return null as any;
}

if (Platform.OS !== 'android') {
return renderItem(info);
}

const total = data?.length ?? 0;
const { index } = info;
const itemId = `room-message-${index}`;

const nextFocusUp = index < total - 1 ? `room-message-${index + 1}` : undefined;
const nextFocusDown = index > 0 ? `room-message-${index - 1}` : undefined;

return (
<View
nativeID={itemId}
focusable
{...(Platform.OS === 'android'
? {
// @ts-ignore Android-only props not in ViewProps types
nextFocusUp,
// @ts-ignore Android-only props not in ViewProps types
nextFocusDown
}
: null)}>
{renderItem(info)}
</View>
);
};

const scrollHandler = useAnimatedScrollHandler({
onScroll: event => {
if (event.contentOffset.y > SCROLL_LIMIT) {
Expand All @@ -36,23 +71,25 @@ const List = ({ listRef, jumpToBottom, ...props }: IListProps) => {
<View style={styles.list}>
{/* @ts-ignore */}
<Animated.FlatList
{...flatListProps}
accessibilityElementsHidden={isAutocompleteVisible}
importantForAccessibility={isAutocompleteVisible ? 'no-hide-descendants' : 'yes'}
testID='room-view-messages'
ref={listRef}
keyExtractor={item => item.id}
data={data}
renderItem={renderItemWithFocus}
contentContainerStyle={styles.contentContainer}
style={styles.list}
inverted
renderScrollComponent={isIOS ? undefined : props => <InvertedScrollView {...props} />}
inverted={props.inverted || true}
renderScrollComponent={isIOS ? undefined : scrollProps => <InvertedScrollView {...scrollProps} />}
removeClippedSubviews={isIOS}
initialNumToRender={7}
onEndReachedThreshold={0.5}
maxToRenderPerBatch={5}
windowSize={10}
scrollEventThrottle={16}
onScroll={scrollHandler}
{...props}
{...scrollPersistTaps}
/>
<NavBottomFAB visible={visible} onPress={jumpToBottom} />
Expand Down