feat(virtualized-list): follow the reading direction on a mirrored layout - #228
Open
jmonsellier wants to merge 1 commit into
Open
jmonsellier wants to merge 1 commit into
jmonsellier wants to merge 1 commit into
Conversation
…yout
A horizontal virtualized list lays its items out from the right edge and
scrolls leftwards when the layout is mirrored. Two signs, decided once per
list from a new `rtl` prop that defaults to `I18nManager.isRTL`: the items
stack leftwards from the anchor React Native already moved to the right, and
the container slides the other way. The sign is flipped in the animation
hook, the single point common to the three scroll behaviours.
A horizontal list pinned inside a `direction: 'ltr'` subtree (playback
controls, a time axis) passes `rtl={false}`, the library having no way to see
the resolved direction of a subtree from JS.
Vertical lists and the non-mirrored path are unchanged by construction.
Closes bamlab#172
jmonsellier
force-pushed
the
feat/rtl-virtualized-list
branch
from
September 10, 2026 12:52
3ec55b7 to
77eb96e
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Closes #172.
The problem
On a mirrored layout (
I18nManager.isRTL === true, an app translated into Arabic, Hebrew, Farsior Urdu), a horizontal
SpatialNavigationVirtualizedListlays its items out off screen, and therow scrolls the wrong way.
As you noted in #172, the list positions everything in JS: each item is
position: 'absolute', left: 0plus atranslateXofindex × size, and the list scrolls by translating its containerby
-offset. On a mirrored tree React Native rewrites thatleft: 0anchor intoright: 0onits own, but the two translations are geometric: the items are pushed rightwards from the right
anchor, and the container slides away from the focused item.
The change
Two signs, decided once per list.
translateX: -(index × size).+offsetinstead of-offset.The reading direction is read in one place,
VirtualizedList, from a newrtlprop that defaultsto
I18nManager.isRTL;ItemContainerWithAnimatedStyleanduseVirtualizedListAnimationonlyreceive it. The animation hook is the single point common to the three
scrollBehaviors, which iswhy the sign is flipped there rather than in
computeTranslation, whose helpers do not know theorientation of the list.
The non-mirrored path is unchanged by construction, and a vertical list is never touched: it
translates on the Y axis, which no reading direction mirrors.
The default focus needs nothing: index 0 is drawn at the right edge on a mirrored layout, which is
the first item in reading order: the 😬 in your answer to #172 solves itself once the layout
follows the direction.
Why a prop and not the global flag alone
A horizontal list can live in a subtree pinned with
direction: 'ltr': playback controls, whichfollow the direction of the tape, or a time axis (an EPG grid, a timeline), which flows to the
right in every culture. React Native decides the
left/rightrewriting once, at the root of thesurface, so inside such a subtree the items keep their
left: 0anchor on the left. The librarycannot see the resolved direction of a subtree from JS, so the app tells it with
rtl={false},same contract as
@legendapp/list. We hit this on the date strip of a channel timeline, and it isthe only reason the decision is not a plain global read.
Web
I18nManagercarries no direction on react-native-web (it comes from the DOM), so a list defaultsto a left-to-right layout there and a web app passes
rtlexplicitly.useWebVirtualizedListAnimationflips the same sign, since react-native-web swaps
leftandrightwhen the writing direction ofthe subtree is right-to-left.
Tests
SpatialNavigationVirtualizedListRTL.test.tsx, 10 cases through the public component and the realremote control path: items stacked leftwards, container sliding the other way on each of the three
scrollBehaviors and on the web animation path, virtualization unchanged,rtl={false}keepingthe left-to-right layout inside a mirrored app, vertical list untouched, and the two non-mirrored
cases including
rtl={true}in a left-to-right app.Six of them fail on
mainand pass with the change; the others are there to pin the paths thatmust not move.
yarn testis green (lint, types, lib, example).Docs
docs/rtl.md, linked from the README and from thertlrow of the API table. It also covers thehalf this library does not own: LRUD navigates by logical index, so
RIGHTmeans "next sibling",which a mirrored layout draws on the left; the app has to swap the two horizontal directions in
its
configureRemoteControlmapping. Snippet included.Out of scope
A horizontal
SpatialNavigationScrollViewderives its scroll frommeasureLayout, whosecoordinates are physical, so it likely needs its own fix on a mirrored layout. I left it alone: I
have no mirrored surface using one to check a fix against, and #172 is about the lists. Happy to
open a separate issue if you want it tracked.
Open question
styles.itemkeeps itsleft: 0anchor, so this relies on the defaultI18nManager.swapLeftAndRightInRTL(true). Moving it tostart: 0would make the anchor follow thedirection resolved by Yoga for the subtree, independently of that flag, and would keep working
inside an
ltrisland. Happy to make that change if you prefer it; I keptleftas the smallerchange, and the one this branch has been exercised with.
I can also add an RTL toggle to the example app if that helps you review.