Skip to content

feat(Split): add RTL support#3958

Open
sgaczol wants to merge 8 commits intomainfrom
@sgaczol/split-rtl
Open

feat(Split): add RTL support#3958
sgaczol wants to merge 8 commits intomainfrom
@sgaczol/split-rtl

Conversation

@sgaczol
Copy link
Copy Markdown
Collaborator

@sgaczol sgaczol commented Apr 30, 2026

Description

This PR introduces RTL support to SplitHost.

The approach differs on iOS versions prior to iOS 17 and newer ones.

On < iOS 17 versions, we use setOverrideTraitCollection(_: forChild:) on the parent view controller to propagate the layout direction, accordingly to https://github.com/software-mansion/react-native-screens-labs/blob/main-issue-tracker/rfcs/0996-rtl-and-dark-mode.md

There is a specific edge case on those iOS versions: if the layout direction is changed while a column is currently open/visible, the UI becomes inconsistent. The sidebar remains on its original side, while the display mode button moves to the new side. The layout only synchronizes correctly after the column is closed and reopened. I checked this issue and it's a native bug: https://github.com/software-mansion/react-native-screens-labs/issues/1237

On ≥ iOS 17 versions, we use traitOverrides API on the view controller.

Closes https://github.com/issues/assigned?issue=software-mansion%7Creact-native-screens-labs%7C1027

Currently blocked by https://github.com/software-mansion/react-native-screens-labs/issues/1429 - it needs to be fixed in order to make a stable test.

Changes

  • add direction prop to SplitHost
  • handle layoutDirection on iOS side
  • add test-split-direction.tsx

After - visual documentation

< iOS 17 ≥ iOS 17
iOS.Below.17.RTL.mov
iOS17.RTL.mov

inherit option also works properly for locations with RTL direction as a default:

default.RTL.location.mov

Test plan

Run test-split-direction.

Checklist

  • Included code example that can be used to test this change.
  • For visual changes, included screenshots / GIFs / recordings documenting the change.
  • For API changes, updated relevant public types.
  • Ensured that CI passes

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds RTL/LTR layout direction control to the experimental SplitHost API, with iOS-version-specific native implementations to ensure correct trait propagation across the split view controller hierarchy.

Changes:

  • Introduces a new direction prop on SplitHost and wires it to the native layoutDirection trait override.
  • Implements iOS handling using traitOverrides.layoutDirection on iOS 17+ and parent setOverrideTraitCollection propagation below iOS 17 (with a pending-update path until the controller is parented).
  • Adds a new single-feature test scenario for exercising direction changes.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/fabric/gamma/split/SplitHostNativeComponent.ts Adds layoutDirection native prop (codegen) and layout direction enum.
src/components/gamma/split/index.ts Re-exports new SplitHostDirection type.
src/components/gamma/split/SplitHost.types.ts Adds SplitHostDirection type and documents new direction prop.
src/components/gamma/split/SplitHost.tsx Passes JS direction through to native layoutDirection.
ios/gamma/split/RNSSplitHostController.swift Adds below-iOS17 layout direction update signaling and pending application once parented.
ios/gamma/split/RNSSplitHostComponentView.mm Tracks prop changes and applies layout direction via iOS17 trait overrides or below-iOS17 update path.
ios/gamma/split/RNSSplitHostComponentView.h Exposes layoutDirection for Swift-side applicator access.
ios/gamma/split/RNSSplitAppearanceUpdateFlags.swift Adds an appearance update flag for below-iOS17 direction updates.
ios/gamma/split/RNSSplitAppearanceApplicator.swift Applies below-iOS17 trait override update via parent controller.
ios/conversion/RNSConversions.h Declares conversion helper for SplitHost layout direction enum.
ios/conversion/RNSConversions-SplitView.mm Implements conversion to UITraitEnvironmentLayoutDirection.
apps/src/tests/single-feature-tests/split/test-split-direction.tsx Adds a manual scenario to toggle direction at runtime.
apps/src/tests/single-feature-tests/split/index.ts Registers the new split direction scenario.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 101 to 105
// This enables us to fully recreate the Split when necessary, ensuring the correct column configuration is always applied.
key={`columns-${columns.length}-inspectors-${inspectors.length}`}
{...props}
layoutDirection={direction}
style={styles.container}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is right here, I had the same exact bug in Tabs and I've spent too much time trying to figure this out. We should destructure props and keep the rest of the props in filteredProps and only spread the filteredProps, as is done in e.g. TabsHost.ios.tsx.

cc @t0maboro

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done:
e342c38

Comment thread apps/src/tests/single-feature-tests/split/test-split-direction.tsx Outdated
sgaczol and others added 2 commits April 30, 2026 15:29
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment on lines +380 to +385
#if RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
if (@available(iOS 17.0, *)) {
_controller.traitOverrides.layoutDirection = _layoutDirection;
} else
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(17_0)
[_controller setNeedsLayoutDirectionUpdateBelowIOS17];
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In tabs this is also done in similar way but maybe for split we should keep the clean flow where in updateProps we set only the props and flags, here we set only the flag on controller (so more general [_controller setNeedsLayoutDirectionUpdate] and then at the level of appearance applicator we have some updateLayoutDirection function that will run updateLayoutDirectionBelowIOS17 / updateLayoutDirectionAboveIOS17 (traitOverrides.layoutDirection = _layoutDirection) depending on iOS version. What do you think?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It makes sense. However, after change in b6470e1, it's controller who detects, which iOS version we're currently at (not the applicator). Such approach seems better for me, especially if we are below iOS17 and the controller.parent is still nil.

Comment on lines 101 to 105
// This enables us to fully recreate the Split when necessary, ensuring the correct column configuration is always applied.
key={`columns-${columns.length}-inspectors-${inspectors.length}`}
{...props}
layoutDirection={direction}
style={styles.container}>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot is right here, I had the same exact bug in Tabs and I've spent too much time trying to figure this out. We should destructure props and keep the rest of the props in filteredProps and only spread the filteredProps, as is done in e.g. TabsHost.ios.tsx.

cc @t0maboro

return proposedTopColumn
}

public override func didMove(toParent parent: UIViewController?) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this should be in UISplitViewControllerDelegate extension block. It should probably be in main block with // MARK: UIKit callbacks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done:
814ab05

Comment on lines +20 to +31
<Split.Host direction={direction}>
<Split.Column>
<ColumnContent
columnTitle="Primary column"
currentDirection={direction}
onChangeDirection={setDirection}
/>
</Split.Column>
<Split.Column>
<ColumnContent columnTitle="Secondary column" />
</Split.Column>
</Split.Host>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From what I know, the SplitView layout can be more complicated, you can have more columns, there is an inspector column. Should we maybe test with a more complicated Split layout to test more cases?

cc @t0maboro

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1, afair changing number of columns should work even in runtime, so even conditional rendering could be possilbe to have a setup with 2 or 3 columns

);
}

export function ColumnContent(props: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the column cut off below iOS 26? It doesn't look centered.

Also, there seems to be some kind of bug with the shadow? Do you know about this, @t0maboro?

split_iOS_18_6.mov

Copy link
Copy Markdown
Contributor

@t0maboro t0maboro May 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the native offset of this column on iOS < 26 has an offset around -100px on X axis, so that's fine; for shadow, not sure about the origin of this issue, will take a look while reviewing this PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the shadow is because the Primary column hasn't backgroundColor set, when it's transparent, it samples the color from the content behind, natively it works the same

sv-native.mov

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's so weird but okay. Would SafeAreaView help with the content being cut off 100px?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we should probably use it here if we can @sgaczol

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that we should probably follow new test structure (separate directory, scenario.md file) for new tests.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this test I think that we're missing checking how our configuration behaves with react-native's I18nManager. Downstream implementations will probably use I18nManager.isRTL as suggested in the docs and how it's done in TabsContainer. I know that we don't have something like SplitContainer but we should probably add an option I18nManager.isRTL in test in addition to current inherit/ltr/rtl. We should also have a way to configure forceRTL and allowRTL.

sgaczol added 3 commits May 5, 2026 15:39
now SplitHostComponentView only passes info to the controller, which decides what changes are about to be applied by the applicator
fixes an issue where we passed direction prop to SplitHostNativeComponent (which is unknown on native side)
@sgaczol
Copy link
Copy Markdown
Collaborator Author

sgaczol commented May 6, 2026

PR is currently blocked by https://github.com/software-mansion/react-native-screens-labs/issues/1429 - it needs to be fixed in order to make a stable test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants