-
Notifications
You must be signed in to change notification settings - Fork 1.4k
fix(android): correct accessibility focus order in inverted FlatList #6934
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
Open
OtavioStasiak
wants to merge
29
commits into
develop
Choose a base branch
from
test.flatlist-inverted-content
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+297
−0
Open
Changes from all commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
6f30628
patch package to not invert the a11y order
OtavioStasiak 8d73ba9
feat: a11yInvertedView manager
OtavioStasiak 3db3a52
fix(a11y): Fix accessibility traversal order for inverted FlatList on…
OtavioStasiak 8362a92
fix: use the default a11y behavior
OtavioStasiak 8ac5ccf
Merge branch 'develop' into test.flatlist-inverted-content
OtavioStasiak c3da7dd
fix: build
OtavioStasiak 37c7701
lock
OtavioStasiak 4d4d572
timeout
OtavioStasiak 98f7dc9
Merge branch 'develop' into test.flatlist-inverted-content
OtavioStasiak df97db6
feat: created flatlist inverted native module
OtavioStasiak 1467be6
remove previous solution
OtavioStasiak e7d809c
fix: revert timeout build android
OtavioStasiak b25af00
fix: list components style
OtavioStasiak d6b0f23
patch-package
OtavioStasiak 62c6a83
revert package.json updates
OtavioStasiak bbc5ee5
revert package.json updates
OtavioStasiak 4eed5d9
chore: format code and fix lint issues [skip ci]
OtavioStasiak e6c7e16
code improvements
OtavioStasiak c23670b
code improvements
OtavioStasiak b2b40bb
chore: format code and fix lint issues [skip ci]
OtavioStasiak 556d404
update comments
OtavioStasiak ed43d9e
use foward ref
OtavioStasiak b106234
use FowardRef
OtavioStasiak 195df51
fix: invertedScrollView ref
OtavioStasiak d7a0a5e
cleanup
OtavioStasiak 4837b1a
chore: format code and fix lint issues [skip ci]
OtavioStasiak 51d984e
fix: invertedScrollView
OtavioStasiak 151aebb
code improvements
OtavioStasiak ead814b
chore: format code and fix lint issues [skip ci]
OtavioStasiak File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentView.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| package chat.rocket.reactnative.scroll; | ||
|
|
||
| import android.view.View; | ||
| import com.facebook.react.views.view.ReactViewGroup; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
|
|
||
| /** | ||
| * Content view for inverted FlatLists. Reports its children to accessibility in reversed order so | ||
| * TalkBack traversal matches the visual order (newest-first) when used inside InvertedScrollView. | ||
| */ | ||
| public class InvertedScrollContentView extends ReactViewGroup { | ||
|
|
||
| public InvertedScrollContentView(android.content.Context context) { | ||
| super(context); | ||
| } | ||
|
|
||
| @Override | ||
| public void addChildrenForAccessibility(ArrayList<View> outChildren) { | ||
| super.addChildrenForAccessibility(outChildren); | ||
| Collections.reverse(outChildren); | ||
| } | ||
| } |
25 changes: 25 additions & 0 deletions
25
...id/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollContentViewManager.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| package chat.rocket.reactnative.scroll; | ||
|
|
||
| import com.facebook.react.module.annotations.ReactModule; | ||
| import com.facebook.react.uimanager.ThemedReactContext; | ||
| import com.facebook.react.views.view.ReactViewManager; | ||
|
|
||
| /** | ||
| * View manager for InvertedScrollContentView. Behaves like a View but reports children in reversed | ||
| * order for accessibility so TalkBack matches the visual order in inverted lists. | ||
| */ | ||
| @ReactModule(name = InvertedScrollContentViewManager.REACT_CLASS) | ||
| public class InvertedScrollContentViewManager extends ReactViewManager { | ||
|
|
||
| public static final String REACT_CLASS = "InvertedScrollContentView"; | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return REACT_CLASS; | ||
| } | ||
|
|
||
| @Override | ||
| public InvertedScrollContentView createViewInstance(ThemedReactContext context) { | ||
| return new InvertedScrollContentView(context); | ||
| } | ||
| } |
24 changes: 24 additions & 0 deletions
24
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollPackage.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package chat.rocket.reactnative.scroll; | ||
|
|
||
| import com.facebook.react.ReactPackage; | ||
| import com.facebook.react.bridge.NativeModule; | ||
| import com.facebook.react.bridge.ReactApplicationContext; | ||
| import com.facebook.react.uimanager.ViewManager; | ||
| import java.util.Collections; | ||
| import java.util.List; | ||
|
|
||
| public class InvertedScrollPackage implements ReactPackage { | ||
|
|
||
| @Override | ||
| public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) { | ||
| return Collections.emptyList(); | ||
| } | ||
|
|
||
| @Override | ||
| public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) { | ||
| List<ViewManager> managers = new java.util.ArrayList<>(); | ||
| managers.add(new InvertedScrollViewManager()); | ||
| managers.add(new InvertedScrollContentViewManager()); | ||
| return managers; | ||
| } | ||
| } |
36 changes: 36 additions & 0 deletions
36
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollView.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package chat.rocket.reactnative.scroll; | ||
|
|
||
| import android.view.View; | ||
| import com.facebook.react.bridge.ReactContext; | ||
| import com.facebook.react.views.scroll.ReactScrollView; | ||
| import java.util.ArrayList; | ||
| import java.util.Collections; | ||
|
|
||
| // 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. | ||
|
|
||
| public class InvertedScrollView extends ReactScrollView { | ||
|
|
||
| private boolean mIsInvertedVirtualizedList = false; | ||
|
|
||
| public InvertedScrollView(ReactContext context) { | ||
| super(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 void addChildrenForAccessibility(ArrayList<View> outChildren) { | ||
| super.addChildrenForAccessibility(outChildren); | ||
| if (mIsInvertedVirtualizedList) { | ||
| Collections.reverse(outChildren); | ||
| } | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
android/app/src/main/java/chat/rocket/reactnative/scroll/InvertedScrollViewManager.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package chat.rocket.reactnative.scroll; | ||
|
|
||
| import com.facebook.react.module.annotations.ReactModule; | ||
| import com.facebook.react.uimanager.ThemedReactContext; | ||
| import com.facebook.react.views.scroll.ReactScrollViewManager; | ||
|
|
||
| /** | ||
| * View manager for {@link InvertedScrollView}. Registers as "InvertedScrollView" to avoid | ||
| * collision with core RCTScrollView. Inherits all ScrollView props from ReactScrollViewManager; | ||
| * FlatList passes isInvertedVirtualizedList when inverted, which is applied by the parent setter. | ||
| */ | ||
| @ReactModule(name = InvertedScrollViewManager.REACT_CLASS) | ||
| public class InvertedScrollViewManager extends ReactScrollViewManager { | ||
|
|
||
| public static final String REACT_CLASS = "InvertedScrollView"; | ||
|
|
||
| @Override | ||
| public String getName() { | ||
| return REACT_CLASS; | ||
| } | ||
|
|
||
| @Override | ||
| public InvertedScrollView createViewInstance(ThemedReactContext context) { | ||
| return new InvertedScrollView(context); | ||
| } | ||
| } |
159 changes: 159 additions & 0 deletions
159
app/views/RoomView/List/components/InvertedScrollView.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import React, { forwardRef, useRef, useLayoutEffect } from 'react'; | ||
| import { | ||
| findNodeHandle, | ||
| requireNativeComponent, | ||
| StyleSheet, | ||
| UIManager, | ||
| type StyleProp, | ||
| type ViewStyle, | ||
| type LayoutChangeEvent, | ||
| type ScrollViewProps, | ||
| type ViewProps | ||
| } from 'react-native'; | ||
|
|
||
| import { isAndroid } from '../../../../lib/methods/helpers'; | ||
|
|
||
| const COMMAND_SCROLL_TO = 1; | ||
| const COMMAND_SCROLL_TO_END = 2; | ||
| const COMMAND_FLASH_SCROLL_INDICATORS = 3; | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| baseVertical: { | ||
| flexGrow: 1, | ||
| flexShrink: 1, | ||
| flexDirection: 'column', | ||
| overflow: 'scroll' | ||
| }, | ||
| baseHorizontal: { | ||
| flexGrow: 1, | ||
| flexShrink: 1, | ||
| flexDirection: 'row', | ||
| overflow: 'scroll' | ||
| } | ||
| }); | ||
|
|
||
| type ScrollViewPropsWithRef = ScrollViewProps & React.RefAttributes<NativeScrollInstance | null>; | ||
| type NativeScrollInstance = React.ComponentRef<NonNullable<typeof NativeInvertedScrollView>>; | ||
| interface IScrollableMethods { | ||
| scrollTo(options?: { x?: number; y?: number; animated?: boolean }): void; | ||
| scrollToEnd(options?: { animated?: boolean }): void; | ||
| flashScrollIndicators(): void; | ||
| getScrollRef(): NativeScrollInstance | null; | ||
| setNativeProps(props: object): void; | ||
| } | ||
|
|
||
| export type InvertedScrollViewRef = NativeScrollInstance & IScrollableMethods; | ||
|
|
||
| const NativeInvertedScrollView = isAndroid ? requireNativeComponent<ScrollViewProps>('InvertedScrollView') : null; | ||
|
|
||
| const NativeInvertedScrollContentView = isAndroid | ||
| ? requireNativeComponent<ViewProps & { removeClippedSubviews?: boolean }>('InvertedScrollContentView') | ||
| : null; | ||
|
|
||
| const InvertedScrollView = forwardRef<InvertedScrollViewRef, ScrollViewProps>((props, externalRef) => { | ||
| const internalRef = useRef<NativeScrollInstance | null>(null); | ||
|
|
||
| useLayoutEffect(() => { | ||
| const node = internalRef.current as any; | ||
|
|
||
| if (node) { | ||
| // 1. Implementation of scrollTo | ||
| node.scrollTo = (options?: { x?: number; y?: number; animated?: boolean }) => { | ||
| const tag = findNodeHandle(node); | ||
| if (tag != null) { | ||
| const x = options?.x || 0; | ||
| const y = options?.y || 0; | ||
| const animated = options?.animated !== false; | ||
| UIManager.dispatchViewManagerCommand(tag, COMMAND_SCROLL_TO, [x, y, animated]); | ||
| } | ||
| }; | ||
|
|
||
| // 2. Implementation of scrollToEnd | ||
| node.scrollToEnd = (options?: { animated?: boolean }) => { | ||
| const tag = findNodeHandle(node); | ||
| if (tag != null) { | ||
| const animated = options?.animated !== false; | ||
| UIManager.dispatchViewManagerCommand(tag, COMMAND_SCROLL_TO_END, [animated]); | ||
| } | ||
| }; | ||
|
|
||
| // 3. Implementation of flashScrollIndicators | ||
| node.flashScrollIndicators = () => { | ||
| const tag = findNodeHandle(node as any); | ||
| if (tag !== null) { | ||
| UIManager.dispatchViewManagerCommand(tag, COMMAND_FLASH_SCROLL_INDICATORS, []); | ||
| } | ||
| }; | ||
|
|
||
| node.getScrollRef = () => node; | ||
| const originalSetNativeProps = (node as any).setNativeProps; | ||
| if (typeof originalSetNativeProps !== 'function') { | ||
| node.setNativeProps = (_nativeProps: object) => {}; | ||
| } | ||
| } | ||
| }, []); | ||
|
|
||
| // Callback Ref to handle merging internal and external refs | ||
| const setRef = (node: NativeScrollInstance | null) => { | ||
| internalRef.current = node; | ||
|
|
||
| if (typeof externalRef === 'function') { | ||
| externalRef(node as InvertedScrollViewRef); | ||
| } else if (externalRef) { | ||
| (externalRef as React.MutableRefObject<NativeScrollInstance | null>).current = node; | ||
| } | ||
| }; | ||
|
|
||
| const { | ||
| children, | ||
| contentContainerStyle, | ||
| onContentSizeChange, | ||
| removeClippedSubviews, | ||
| maintainVisibleContentPosition, | ||
| snapToAlignment, | ||
| stickyHeaderIndices, | ||
| ...rest | ||
| } = props; | ||
|
|
||
| const preserveChildren = maintainVisibleContentPosition != null || (isAndroid && snapToAlignment != null); | ||
| const hasStickyHeaders = Array.isArray(stickyHeaderIndices) && stickyHeaderIndices.length > 0; | ||
|
|
||
| const contentContainerStyleArray = [props.horizontal ? { flexDirection: 'row' as const } : null, contentContainerStyle]; | ||
|
|
||
| const contentSizeChangeProps = | ||
| onContentSizeChange == null | ||
| ? undefined | ||
| : { | ||
| onLayout: (e: LayoutChangeEvent) => { | ||
| const { width, height } = e.nativeEvent.layout; | ||
| onContentSizeChange(width, height); | ||
| } | ||
| }; | ||
|
|
||
| const horizontal = !!props.horizontal; | ||
| const baseStyle = horizontal ? styles.baseHorizontal : styles.baseVertical; | ||
| const { style, ...restWithoutStyle } = rest; | ||
|
|
||
| if (!NativeInvertedScrollView || !NativeInvertedScrollContentView) { | ||
| return null; | ||
| } | ||
| const ScrollView = NativeInvertedScrollView as React.ComponentType<ScrollViewPropsWithRef>; | ||
| const ContentView = NativeInvertedScrollContentView as React.ComponentType<ViewProps & { removeClippedSubviews?: boolean }>; | ||
|
|
||
| return ( | ||
| <ScrollView ref={setRef} {...restWithoutStyle} style={StyleSheet.compose(baseStyle, style)} horizontal={horizontal}> | ||
| <ContentView | ||
| {...contentSizeChangeProps} | ||
| removeClippedSubviews={isAndroid && hasStickyHeaders ? false : removeClippedSubviews} | ||
| collapsable={false} | ||
| collapsableChildren={!preserveChildren} | ||
| style={contentContainerStyleArray as StyleProp<ViewStyle>}> | ||
| {children} | ||
| </ContentView> | ||
| </ScrollView> | ||
| ); | ||
| }); | ||
|
|
||
| InvertedScrollView.displayName = 'InvertedScrollView'; | ||
|
|
||
| export default InvertedScrollView; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.