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

Inverted FlatList accessibility order #30373

Closed
zarubond opened this issue Nov 12, 2020 · 28 comments
Closed

Inverted FlatList accessibility order #30373

zarubond opened this issue Nov 12, 2020 · 28 comments
Labels
Accessibility Team - Evaluated Accessibility Component: FlatList Needs: Triage 🔍 Stale There has been a lack of activity on this issue and it may be closed soon.

Comments

@zarubond
Copy link

Description

In normal FlatList the accessibility focus order is from top to bottom but applying inverted property makes the order swap to bottom to top. This breaks the accessibility navigation flow as it is assumed that it should go from top of the screen to the bottom.

React Native version:

RN 0.63 and Android 9 with enabled TalkBack

Steps To Reproduce

Use FlatList component like

<FlatList ... inverted={ true } />

Expected Results

The order of items should stay inverted but the accessibility order should be maintained top to bottom.

Snack, code example, screenshot, or link to a repository:

Please provide a Snack (https://snack.expo.io/), a link to a repository on GitHub, or provide a minimal code example that reproduces the problem.
You may provide a screenshot of the application if you think it is relevant to your bug report.
Here are some tips for providing a minimal example: https://stackoverflow.com/help/mcve

@teod
Copy link

teod commented Jan 28, 2021

are there any updates for this ticket ?

@grgr-dkrk
Copy link
Contributor

I had the same problem.

The VirtualizedList implementation seems to styling transform: [{scaleY: -1}] to reverse the order.
This does not guarantee the reading order of the screen reader as it only changes the visual order.

const inversionStyle = this.props.inverted
? this.props.horizontal
? styles.horizontallyInverted
: styles.verticallyInverted
: null;

I don't understand the implementation context, but changing this to flexDirection: column-reverse seems to solve it.
Is it impossible?

@teod
Copy link

teod commented Feb 8, 2021

I had the same problem.

The VirtualizedList implementation seems to styling transform: [{scaleY: -1}] to reverse the order.
This does not guarantee the reading order of the screen reader as it only changes the visual order.

const inversionStyle = this.props.inverted
? this.props.horizontal
? styles.horizontallyInverted
: styles.verticallyInverted
: null;

I don't understand the implementation context, but changing this to flexDirection: column-reverse seems to solve it.
Is it impossible?

unfortunately this will "break" scroll and infinite loading pagination

@mallenexpensify
Copy link

We're interested in getting this issue fixed and willing to compensate to do so. View issue Expensify/App#1341

@sfuqua
Copy link

sfuqua commented Jan 20, 2022

Is it possible to add the "Accessibility" tag to this issue? Not sure how closely the task board is still being tracked but this is a pretty fundamental issue for Android in an important control.

This breaks Android screen reader support in any inverted list which is common e.g. for chat scenarios.

@MaksDubois
Copy link

I had the same problem. Are there any updates for this ticket ?

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 4, 2022

One possible solution would be not using transform to visually invert the list.
The solution may also fix existing issues #17553 (comment) (using transform causes unexpected behavior for ex. showing activity indicator at the bottom instead of the top of the flatlist).

Android/iOS Solution

  • finding existing native Android/iOS API to reverse the ScrollView
  • reverse the list in java/objc and start from the bottom

Javascript Solution

tasks - planning a solution for the issue

  • Create an initial example of the solution with Expo.
  • inverting the order of the list in Javascript
    for ex. [1,2,3].reverse() ==> [3,2,1]
    [{ key: 1, text: "1"}, { key: 2, text: "2"}, { key: 3, text: "3"}] =>
    [{ key: 3, text: "3"}, { key: 2, text: "2"}, { key: 1, text: "1"}]
  • starting position needs to be the end of the FlatList (initialScrollIndex)

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 4, 2022

FlatList inverted - The accessibility focus order is from top to bottom, but applying FlatList inverted property makes the order swap to bottom to top

Example
https://snack.expo.dev/@fabrizio.bertoglio/inverted-flatlist

Inverted set to false

inverted_false_480.mp4

Inverted set to true

inverted_true_480.mp4

Manually inverted FlatList - does not cause issues with TalkBack

https://snack.expo.dev/@fabrizio.bertoglio/manually-inverting-flatlist

invert_manually_in_js_480.mp4

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 4, 2022

Evaluate the performance implications of using transform vs JavaScript vs Native Solution.

Transform
The implementation of transform includes:

Reverse Array instead of using Transform
O(n) time and O(1) space

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 4, 2022

implement scroll to top optimization for inverted FlatList

The normal FlatList keeps the first initialNumToRender items always rendered (first page) and immediately renders the items starting at this initial index. The same optimization needs to be implemented for inverted FlatList.

Instead of starting at the top with the first item, start at initialScrollIndex. This disables the "scroll to top" optimization that keeps the first initialNumToRender items always rendered and immediately renders the items starting at this initial index. Requires getItemLayout to be implemented.

if there is a initialNumToRender, the below optimization (scroll to top) is disabled by passing -1 as last.

const lastInitialIndex = this.props.initialScrollIndex
? -1
: initialNumToRenderOrDefault(this.props.initialNumToRender) - 1;
const {first, last} = this.state;
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
0,
lastInitialIndex,
inversionStyle,
);

fabOnReact@28aaa88

== [Lists] Add initialScrollIndex prop

Makes it easy to load a VirtualizedList at a location in the middle of the content without
wasting time rendering initial rows that aren't relevant, for example when opening an infinite calendar
view to "today".

**Test Plan: **
With debug overlay, set initialScrollIndex={52} prop in FlatListExample and
and see it immediately render a full screen of items with item 52 aligned at the top of the screen. Note
no initial items are mounted per debug overlay. Scroll around a bunch and everything else seems to work
as normal.

No SectionList impl since getItemLayout isn't easy to use there.

how initialScrollIndex changes the items rendered on the screen

initialScrollIndex is used to calculate this.state.first and last.

  • first is either item index 0 or initialScrollIndex
  • last is first + 10

let initialState = {
first: this.props.initialScrollIndex || 0,
last:
Math.min(
this.props.getItemCount(this.props.data),
(this.props.initialScrollIndex || 0) +
initialNumToRenderOrDefault(this.props.initialNumToRender),
) - 1,
};
if (this._isNestedWithSameOrientation()) {

the items between first and last are added in the cells variables which is called on render

for (let ii = first; ii <= last; ii++) {
const item = getItem(data, ii);
const key = this._keyExtractor(item, ii);
this._indicesToKeys.set(ii, key);
if (stickyIndicesFromProps.has(ii + stickyOffset)) {
stickyHeaderIndices.push(cells.length);
}
cells.push(
<CellRenderer
CellRendererComponent={CellRendererComponent}
ItemSeparatorComponent={ii < end ? ItemSeparatorComponent : undefined}
ListItemComponent={ListItemComponent}
cellKey={key}
debug={debug}
fillRateHelper={this._fillRateHelper}
getItemLayout={getItemLayout}
horizontal={horizontal}
index={ii}
inversionStyle={inversionStyle}
item={item}
key={key}
prevCellKey={prevCellKey}
onCellLayout={this._onCellLayout}
onUpdateSeparators={this._onUpdateSeparators}
onUnmount={this._onCellUnmount}
ref={ref => {
this._cellRefs[key] = ref;
}}
renderItem={renderItem}
/>,
);

In the CellRenderer class onLayout is undefined if we use getItemLayout. There is no need to use onLayout to calculate the cell height/width.

const onLayout =
(getItemLayout && !debug && !fillRateHelper.enabled()) ||
!this.props.onCellLayout
? undefined
: this._onLayout;
// NOTE: that when this is a sticky header, `onLayout` will get automatically extracted and
// called explicitly by `ScrollViewStickyHeader`.

  • review implementation of getItemLayout in rn-tester
  • review test case for initialScrollIndex
  • read implementation of initialScrollIndex in VirtualizedList
  • read implementation on state.first and state.last in VirtualizedList
  • find logic that implements the scroll to top optimization in FlatList

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 6, 2022

use initialScrollIndex to start from bottom, but keep scrollToBottom optimization

the android ScrollView widget method fullScroll allow to start the ScrollView from the Button

https://stackoverflow.com/a/30224427/7295772
https://github.com/aosp-mirror/platform_frameworks_base/blob/19e53cfdc8a5c6ef45c0adf2dd239576ddce5822/core/java/android/widget/ScrollView.java#L1187

my current solution uses this.scrollToEnd({animated: false}), as initialScrollIndex requires a static layout.

solved with fabOnReact@5b2cb47

scroll to top optimization (from main)

const lastInitialIndex = this.props.initialScrollIndex
? -1
: initialNumToRenderOrDefault(this.props.initialNumToRender) - 1;
const {first, last} = this.state;
this._pushCells(
cells,
stickyHeaderIndices,
stickyIndicesFromProps,
0,
lastInitialIndex,
inversionStyle,
);

scroll top bottom optimization (from pr)

https://github.com/facebook/react-native/pull/34141/files#diff-7481ec8ed5532798b075df637e805a8e439807aa2ce671208c24068c286361e8R1010-R1020

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 6, 2022

nested FlatList with different orientation does not scroll to the end when inverted

nested FlatList with different orientation does not scroll to the end when inverted

scrollToEnd does not work because this._scrollRef is null

the scrollToEnd does not work with nested FlatList with different orientation (children is horizontal)
the issue is caused by this._scrollRef being null

scrollToOffset(params: {animated?: ?boolean, offset: number, ...}) {
const {animated, offset} = params;
if (this._scrollRef == null) {
return;
}

The ref is set here

{
ref: this._captureScrollRef,
},

using componentDidMount fixes this issue, but this.scrollToEnd does not correctly work.

The offset is 0 when using scrollToEnd.

const offset = Math.max(
0,
frame.offset +
frame.length +
this._footerLength -
this._scrollMetrics.visibleLength,
);

seems caused by this line

this._scrollMetrics.contentLength = this._selectLength({height, width});

when use trigger scrollToEnd, frame.offset is 0

const frame = this.__getFrameMetricsApprox(veryLast);

if you wait 1 second, _onContentSize changes and frame.offset is valorized (for ex. 1200 px).

Related https://stackoverflow.com/a/72906476/7295772 https://stackoverflow.com/questions/44418990/using-a-flatlist-with-scrolltoend

Tasks

scrollToEnd does not work:

  • scrollToEnd does not work because this._scrollRef is null

Understand why frame.offset and frame.length are equal to 0 instead of 1300:

  • try to set initialNumToRender to number higher then data.length
  • debug __getFrameMetricsApprox. Offset is 0 at index 9, but it's 77.5 at index 8 and 155 at index 7. Probably offset is measuring the space between index 1-9 till index 9, and not the end of the list.
  • consider using an alternative solution, as triggering scrollToBottom or Top may not be very performance-wise (and does not work in the above use case). Read into the implementation of initialScrollIndex ==> the implementation uses scrollToIndex which should be used wit getItemLayout
  • add android method fullScroll
  • check if it is a regression with main. Verify that scrollToEnd works in the main branch for this use case.
  • try to set static width/height to the parent/nested flatlist
  • compare differences with the value of offset with parent and child FlatList. Understand the reason offset results to 0 in the child.
  • search online discussion for this issue and potential solutions
  • try to reproduce this issue on the main branch with a non nested horizontal flatlist
  • understand why adding timeout of 1second fixes this issue
    read VirtualizedList logic to find out logic responsible to trigger FlatList scrollToTop
  • use alternative method (initialScrollIndex) and add condition to avoid error message with inverted flatlist
  • read github history to understand how method was implemented
  • consider reporting the issue in a github issue

Low Priority

@fabOnReact
Copy link
Contributor

main branch - scrollToEnd does not work for nested FlatList with different orientations (horizontal/vertical)

In the example above the FlatList is not inverted.

SourceCode of the example

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */
import React from 'react';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
} from 'react-native';

const DATA = [
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
    title: 'First Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
    title: 'Second Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29d72',
    title: 'Third Item',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb',
    title: 'Fourth Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97676',
    title: 'Fifth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e27234',
    title: 'Sixth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29234',
    title: 'Seven Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571429234',
    title: 'Eight Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-115571429234',
    title: 'Nine Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1155h1429234',
    title: 'Ten Item',
  },
];

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

const renderFlatList = ({item}) => <NestedFlatList item={item} />;

function NestedFlatList(props) {
  let flatlist = React.useRef(0);
  React.useEffect(() => {
    // flatlist.scrollToOffset({offset: 697.5, animated: false});
    flatlist.scrollToEnd({animated: false});
  }, []);
  return (
    <View>
      <Text>Flatlist {props.item}</Text>
      <FlatList
        // inverted={true}
        ref={ref => (flatlist = ref)}
        renderItem={renderItem}
        data={DATA}
      />
    </View>
  );
}

const FlatList_nested = () => {
  let flatlist = React.useRef(0);
  React.useEffect(() => {
    // flatlist.scrollToOffset({offset: 1300, animated: false});
    // flatlist.scrollToEnd({animated: false})
  }, []);
  return (
    <FlatList
      ref={ref => (flatlist = ref)}
      data={[1]}
      horizontal
      inverted={false}
      renderItem={renderFlatList}
      keyExtractor={item => item.toString()}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Nested',
  name: 'nested',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

Screen.Recording.2022-07-08.at.09.34.43.mov

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 8, 2022

Main - Normal FlatList behaviour scroll position adding new items

sourcecode example

*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */
import React from 'react';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from 'react-native';

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Text style={styles.item}>{item.title}</Text>;
const ITEM_HEIGHT = 50;

const renderFlatList = ({item}) => <NestedFlatList item={item} />;

const FlatList_nested = () => {
  let flatlist = React.useRef(null);
  const [items, addItem] = React.useState(DATA);
  return (
    <>
      <Button
        title="add an item"
        onPress={() => addItem([...items, {title: 'new item'}])}
      />
      <FlatList
        // inverted={true}
        horizontal
        onContentSizeChange={() => flatlist.scrollToEnd()}
        ref={ref => (flatlist = ref)}
        renderItem={renderItem}
        data={items}
      />
    </>
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Nested',
  name: 'nested',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

const DATA = [
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
    title: 'First Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
    title: 'Second Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29d72',
    title: 'Third Item',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb',
    title: 'Fourth Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97676',
    title: 'Fifth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e27234',
    title: 'Sixth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29234',
    title: 'Seven Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571429234',
    title: 'Eight Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-115571429234',
    title: 'Nine Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1155h1429234',
    title: 'Ten Item',
  },
];

Screen.Recording.2022-07-08.at.12.07.38.mov
Main - Inverted FlatList behaviour scroll position adding new items

sourcecode example

*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */
import React from 'react';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from 'react-native';

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Text style={styles.item}>{item.title}</Text>;
const ITEM_HEIGHT = 50;

const renderFlatList = ({item}) => <NestedFlatList item={item} />;

const FlatList_nested = () => {
  let flatlist = React.useRef(null);
  const [items, addItem] = React.useState(DATA);
  return (
    <>
      <Button
        title="add an item"
        onPress={() => addItem([...items, {title: 'new item'}])}
      />
      <FlatList
        inverted={true}
        horizontal
        onContentSizeChange={() => flatlist.scrollToEnd()}
        ref={ref => (flatlist = ref)}
        renderItem={renderItem}
        data={items}
      />
    </>
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Nested',
  name: 'nested',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

const DATA = [
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
    title: 'First Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
    title: 'Second Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29d72',
    title: 'Third Item',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb',
    title: 'Fourth Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97676',
    title: 'Fifth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e27234',
    title: 'Sixth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29234',
    title: 'Seven Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571429234',
    title: 'Eight Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-115571429234',
    title: 'Nine Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1155h1429234',
    title: 'Ten Item',
  },
];

Screen.Recording.2022-07-08.at.12.12.07.mov
Branch - Inverted FlatList behaviour scroll position adding new items

sourcecode example

import React from "react";
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from "react-native";

const DATA = [
  {
    id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
    title: "First Item",
  },
  {
    id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
    title: "Second Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e29d72",
    title: "Third Item",
  },
  {
    id: "bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb",
    title: "Fourth Item",
  },
  {
    id: "3ac68afc-c605-48d3-a4f8-fbd91aa97676",
    title: "Fifth Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e27234",
    title: "Sixth Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e29234",
    title: "Seven Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571429234",
    title: "Eight Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-115571429234",
    title: "Nine Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-1155h1429234",
    title: "Ten Item",
  },
];

const Item = ({ title }) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({ item }) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

const renderFlatList = ({ item }) => (
  <NestedFlatList item={item} />
);

function NestedFlatList(props) {
  const [items, addItem] = React.useState(DATA);
  return (
    <View>
      <Button
      title="add an item"
      onPress={() => addItem([...items, {title: 'new item'}])}
      />
      <Text>Flatlist</Text>
      <FlatList
        style={{height: 400}}
        inverted={true}
        renderItem={renderItem} data={items} />
    </View>
  )
}

const FlatList_nested = () => {
  let flatlist = React.useRef(0);
  return (
    <FlatList
      ref={(ref) => flatlist = ref}
      data={[1,2,3]}
      horizontal
      renderItem={renderFlatList}
      keyExtractor={(item) => item.toString()}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: "#f9c2ff",
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Nested',
  name: 'nested',
  description: 'nested FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

Screen.Recording.2022-07-08.at.12.42.42.mov

fabOnReact added a commit to fabOnReact/react-native that referenced this issue Jul 8, 2022
addresses issues explained in facebook#30373 (comment)

handles cases when adding new item to inverted flatlist (flatlist has to
scroll up to the new item)

test cases
facebook#30373 (comment)
@fabOnReact
Copy link
Contributor

Testing TalkBack navigation in an inverted nested Flatlist

sourcecode example

import React from "react";
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from "react-native";

const DATA = [
  {
    id: "bd7acbea-c1b1-46c2-aed5-3ad53abb28ba",
    title: "First Item",
  },
  {
    id: "3ac68afc-c605-48d3-a4f8-fbd91aa97f63",
    title: "Second Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e29d72",
    title: "Third Item",
  },
  {
    id: "bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb",
    title: "Fourth Item",
  },
  {
    id: "3ac68afc-c605-48d3-a4f8-fbd91aa97676",
    title: "Fifth Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e27234",
    title: "Sixth Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571e29234",
    title: "Seven Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-145571429234",
    title: "Eight Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-115571429234",
    title: "Nine Item",
  },
  {
    id: "58694a0f-3da1-471f-bd96-1155h1429234",
    title: "Ten Item",
  },
];

const Item = ({ title }) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({ item }) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

const renderFlatList = ({ item }) => (
  <NestedFlatList item={item} />
);

function NestedFlatList(props) {
  const [items, setItems] = React.useState(DATA);
  return (
    <View>
      <Button
      title="add an item"
      onPress={() => setItems([...items, {title: 'new item'}])}
      />
      <Button
        title="remove an item"
        onPress={() => {
          const newItems = [...items];
          newItems.splice(items.length - 1, 1)
          setItems(newItems);
        }}
      />
      <Text>Flatlist</Text>
      <FlatList
        style={{height: 400}}
        inverted={true}
        renderItem={renderItem} data={items} />
    </View>
  )
}

const FlatList_nested = () => {
  let flatlist = React.useRef(0);
  return (
    <FlatList
      ref={(ref) => flatlist = ref}
      data={[1,2,3]}
      horizontal
      renderItem={renderFlatList}
      keyExtractor={(item) => item.toString()}
    />
  );
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: "#f9c2ff",
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Nested',
  name: 'nested',
  description: 'nested FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

Screen.Recording.2022-07-08.at.14.49.01.mov

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 11, 2022

iOS - Test not-nested inverted FlatList

RecordIt-D03B66C7-7232-40C7-A557-9FBB1AE9247D.mp4

iOS - Test nested inverted FlatList

RecordIt-B49349EC-626C-4311-8409-DAA8EA636225.mp4

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 11, 2022

nested horizontal inverted flatlist starts scrolled to the end

  • nested horizontal inverted flatlist starts scrolled to the end
  • adding/removing new items move the scroll to top
Screen.Recording.2022-07-11.at.21.45.29.mov

@fabOnReact
Copy link
Contributor

children should not be inverted if nested in a FlatList with prop inverted

Screen.Recording.2022-07-11.at.21.52.53.mov

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 12, 2022

FlatList automatically scrolls after prepending new items #25239 (Android)

Documented in #25239. The issue already present in main.

click to open example

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

import * as React from 'react';
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from 'react-native';

const DATA = [
  {
    id: 1,
    title: 'First Item',
  },
  {
    id: 2,
    title: 'Second Item',
  },
  {
    id: 3,
    title: 'Third Item',
  },
  {
    id: 4,
    title: 'Fourth Item',
  },
  {
    id: 5,
    title: 'Fifth Item',
  },
  {
    id: 6,
    title: 'Sixth Item',
  },
  {
    id: 7,
    title: 'Seven Item',
  },
  {
    id: 8,
    title: 'Eight Item',
  },
  {
    id: 9,
    title: 'Nine Item',
  },
  {
    id: 10,
    title: 'Ten Item',
  },
];

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

function NestedFlatList(props) {
  const [items, setItems] = React.useState(DATA);
  const [index, setIndex] = React.useState(11);
  return (
    <View>
      <Button
        title="add an item"
        onPress={() => {
          setItems([{id: index, title: 'new item'}, ...items]);
          setIndex(index + 1);
        }}
      />
      <Button
        title="remove an item"
        onPress={() => {
          const newItems = [...items];
          newItems.splice(items.length - 1, 1);
          setItems(newItems);
        }}
      />
      <Text>Flatlist</Text>
      <FlatList
        style={{height: 400}}
        keyExtractor={(item, index) => item.id}
        renderItem={renderItem}
        data={items}
      />
    </View>
  );
}

const FlatList_nested = (): React.Node => {
  return <NestedFlatList />;
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Inverted',
  name: 'inverted',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

Main Branch

Screen.Recording.2022-07-12.at.22.32.15.mov

PR Branch

Screen.Recording.2022-07-12.at.23.10.27.mov

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 12, 2022

FlatList does not scroll after appending new items (Android)

This is the expected behavior on Android.

click to open sourcecode

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */

import * as React from 'react';
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from 'react-native';

const DATA = [
  {
    id: 1,
    title: 'First Item',
  },
  {
    id: 2,
    title: 'Second Item',
  },
  {
    id: 3,
    title: 'Third Item',
  },
  {
    id: 4,
    title: 'Fourth Item',
  },
  {
    id: 5,
    title: 'Fifth Item',
  },
  {
    id: 6,
    title: 'Sixth Item',
  },
  {
    id: 7,
    title: 'Seven Item',
  },
  {
    id: 8,
    title: 'Eight Item',
  },
  {
    id: 9,
    title: 'Nine Item',
  },
  {
    id: 10,
    title: 'Ten Item',
  },
];

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

function NestedFlatList(props) {
  const [items, setItems] = React.useState(DATA);
  const [index, setIndex] = React.useState(11);
  return (
    <View>
      <Button
        title="add an item"
        onPress={() => {
          setItems([...items, {id: index, title: 'new item'}]);
          setIndex(index + 1);
        }}
      />
      <Button
        title="remove an item"
        onPress={() => {
          const newItems = [...items];
          newItems.splice(items.length - 1, 1);
          setItems(newItems);
        }}
      />
      <Text>Flatlist</Text>
      <FlatList
        style={{height: 400}}
        keyExtractor={(item, index) => item.id}
        renderItem={renderItem}
        data={items}
      />
    </View>
  );
}

const FlatList_nested = (): React.Node => {
  return <NestedFlatList />;
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Inverted',
  name: 'inverted',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}: RNTesterModuleExample);

Main

Screen.Recording.2022-07-12.at.22.46.44.mov

Branch

Screen.Recording.2022-07-12.at.23.13.25.mov

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 14, 2022

when items are appended to the end of the list, the view needs to stay in the same position

Related to #34141 (comment) #34141 (comment)

Option 1) Over-ride this behavior on inverted FlatList when TalkBack enabled

  • Find out the Android Java code responsible for triggering the scroll to the Top when items are appended (prepended in a not inverted list)

Option 2) Find the x/y coordinate and scroll to that position after the item is appended

Option 3) Store the x,y coordinates of each child to trigger scrollToOffeset(first.coodinates.x)

Option 4) Support for ScrollView.maintainVisibleContentPosition on Android

Option 5) Use not optimized scrollToItem(first) only when TalkBack is Enabled

@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 15, 2022

Option 2) Find the x/y coordinate and scroll to that position after the item is appended

when items are appended to the end of the list, the view needs to stay in the same position (javascript draft solution)

Draft solution only using JavaScript. It is not optimized but will be useful to implement the solution in Java/Javascript step by step.

click to open sourcecode of the example

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow strict-local
 * @format
 */
import * as React from 'react';
import {useState, useRef} from 'react';
import type {RNTesterModuleExample} from '../../types/RNTesterTypes';
import {
  SafeAreaView,
  View,
  FlatList,
  StyleSheet,
  Text,
  StatusBar,
  Button,
} from 'react-native';

const DATA = [
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb28ba',
    title: 'First Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97f63',
    title: 'Second Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29d72',
    title: 'Third Item',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abb8bbb',
    title: 'Fourth Item',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa97676',
    title: 'Fifth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e27234',
    title: 'Sixth Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571e29234',
    title: 'Seven Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571429234',
    title: 'Eight Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-115571429234',
    title: 'Nine Item',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1155h1429234',
    title: 'Ten Item',
  },
];

const Item = ({title}) => (
  <Text style={[styles.item, styles.title]}>{title}</Text>
);

const renderItem = ({item}) => <Item title={item.title} />;
const ITEM_HEIGHT = 50;

const renderFlatList = ({item}) => <NestedFlatList item={item} />;

function NestedFlatList(props) {
  let flatlist = useRef(null);
  let bottomY = 0;
  const [bottomHeight, setBottomHeight] = useState(0);
  const [height, setHeight] = useState(undefined);
  const [items, setItems] = useState(DATA);
  const [disabled, setDisabled] = useState(false);
  const [index, setIndex] = useState(DATA.length + 1);
  const [resetScrollPosition, setResetScrollPosition] = useState(false);
  React.useEffect(() => {
    if (resetScrollPosition && bottomHeight && flatlist) {
      flatlist.scrollToOffset({
        offset: height - bottomHeight,
        animated: false,
      });
      setResetScrollPosition(false);
    }
  }, [resetScrollPosition, bottomHeight, height]);
  return (
    <View>
      <Button
        title="append an item"
        onPress={() => {
          setBottomHeight(bottomY);
          setItems([...items, {title: `new item ${index}`}]);
          setIndex(index + 1);
        }}
      />
      <Button
        title="prepend an item"
        onPress={() => {
          setItems([{title: `new item ${index}`}, ...items]);
          setIndex(index + 1);
        }}
      />
      <Button
        title="remove an item"
        onPress={() => {
          const newItems = [...items];
          newItems.splice(items.length - 1, 1);
          setItems(newItems);
        }}
      />
      <Text>Flatlist</Text>
      <FlatList
        ref={ref => (flatlist = ref)}
        inverted
        style={{height: 400}}
        renderItem={renderItem}
        data={items}
        onScroll={e => {
          const scrollY = e.nativeEvent.contentOffset.y;
          const height = e.nativeEvent.contentSize.height;
          bottomY = height - scrollY;
        }}
        onContentSizeChange={(width, height) => {
          setHeight(height);
          setResetScrollPosition(true);
        }}
      />
    </View>
  );
}

const FlatList_nested = (): React.Node => {
  return <NestedFlatList />;
};

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 16,
  },
});

export default ({
  title: 'Inverted',
  name: 'inverted',
  description: 'Test inverted prop on FlatList',
  render: () => <FlatList_nested />,
}

video of the test case - with fix to keep scroll position

2022-07-15.13-09-15.mp4

video of the test case - without fix

2022-07-15.13-39-56.mp4

  • calculate offset correctly for inverted flastlist (using distance from the bottom instead of top)
// onScroll callback we save bottomY variable
const scrollY = e.nativeEvent.contentOffset.y;
const height = e.nativeEvent.contentSize.height;
bottomY = height - scrollY; 

fabOnReact added a commit to fabOnReact/react-native that referenced this issue Jul 15, 2022
…s to stay in the same position

it works when _onScroll is triggered:
- the user scroll the list
- the bottomY state is updated
- appending item to the list will not change position

still does not work when the user appends a second item

Related facebook#30373 (comment)
Was implemented on a Javascript example above, which had similar issues
- did not work if no scroll was triggered
- did not work if triggering many times fast appends of items

Reason for this issues needs to be investigated, but the solution only
be for TalkBack users to avoid using transform
@fabOnReact
Copy link
Contributor

fabOnReact commented Jul 18, 2022

  • trigger a scroll to that coordinate
  • fix issue no initial scroll sets bottomY value
  • test solution with pre-pending items
  • implement the solution in VirtualizedList
  • add check isScreenreaderEnabled
  • adapt solution to horizontal flatlist
  • adapt solution to use with initialIndex
  • add check on Platform Android
  • add a summary to Pull Request explaining the changes
  • review pull request and add improvements
  • circleci tests failures
  • onEndReached triggered when scrolling to the end of the FlatList
  • fix issue with scrollPosition onEndReached
  • add links to relevant tests in the Pull Request summary
  • review PR and move to READY FOR REVIEW

fabOnReact added a commit to fabOnReact/react-native that referenced this issue Jul 18, 2022
…y in the same position

>when items are appended to the end of the list, the view needs to stay in the same position

This functionality will be introduced with a separate PR with the
following improvement
(OPTIONAL) instead of using onScroll to save scroll x/y coordinates, use the screenreaderFocus
facebook#30373 (comment)

Related
facebook#34141 (comment)
fabOnReact added a commit to fabOnReact/react-native that referenced this issue Sep 26, 2022
- Import improvements from PR facebook#26444 _maybeCallOnEndReached:
https://github.com/facebook/react-native/pull/26444/files#diff-7481ec8ed5532798b075df637e805a8e439807aa2ce671208c24068c286361e8L1374-R1413
https://github.com/facebook/react-native/blob/2d3f6ca9801ef92b446458a2efc795db4ec17021/Libraries/Lists/VirtualizedList.js#L1372-L1414
- Additional check _hasDoneFirstScroll for maybeCallOnEndReached (added in onScroll callback)
- Add improved logic start/endPositionReached and isScrollingForward
- Add threeshold instead of 2
- Use default threeshold 30 for iOS
- Add other improvements from method _maybeCallOnEndReached
@fabOnReact
Copy link
Contributor

@mallenexpensify

I quote Eric Rozell #34141 (comment)

@fabriziobertoglio1987 In case you're interested, I recently worked on a fix for react-native-windows list inversion. It uses the flexDirection: "column-reverse" approach described here: #30373 (comment)

Here is the PR: microsoft/react-native-windows#8440

The PR for Windows also addresses the issues with infinite scroll and virtualization described here: #30373 (comment)

I wrote up a more detailed requirement for the platform requirements and what they map to on Windows here: https://gist.github.com/rozele/47a2ad44f6d44561da1d293454ae8418

The most important bits are the view and edge anchoring behaviors, i.e., when a new item is prepended to the list, the ScrollView should synchronously scroll it into view (i.e., drawn to view port and scrolled to bottom in same frame), and when items are appended to the end of the list, the view needs to stay in the same position. Windows has specific features to implement these behaviors, but I imagine it's possible to achieve this in Android with something like the maintainVisibleContentPosition API (which there is a WIP draft for Android): #29466

Now that PR Add fabric support for maintainVisibleContentPosition on Android #35994 is merged, I can build the above functionalities for the inverted flatlist.

Issues Expensify/App#1341 and Expensify/App#3381 were closed.
Should I restart working on this issue? Thanks

Copy link

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

@github-actions github-actions bot added the Stale There has been a lack of activity on this issue and it may be closed soon. label Dec 13, 2023
Copy link

This issue was closed because it has been stalled for 7 days with no activity.

@brandonhenry
Copy link

@fabOnReact yes, we would love if this was re-opened and worked on! This is still actively affecting our project.

See Expensify/App#35946

@brandonhenry
Copy link

Any updates? @react-native-bot

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Accessibility Team - Evaluated Accessibility Component: FlatList Needs: Triage 🔍 Stale There has been a lack of activity on this issue and it may be closed soon.
Projects
None yet