Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/src/tests/single-feature-tests/split/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import type { ScenarioGroup } from '@apps/tests/shared/helpers';
import TestTopColumnForCollapsing from './test-top-column-for-collapsing';
import TestCommandShowColumn from './test-command-show-column';
import TestDirection from './test-split-direction';

const scenarios = { TestTopColumnForCollapsing, TestCommandShowColumn };
const scenarios = {
TestTopColumnForCollapsing,
TestCommandShowColumn,
TestDirection,
};

const SplitScenarioGroup: ScenarioGroup<keyof typeof scenarios> = {
name: 'Split',
Expand Down
102 changes: 102 additions & 0 deletions apps/src/tests/single-feature-tests/split/test-split-direction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, { useState } from 'react';
import type { ScenarioDescription } from '@apps/tests/shared/helpers';
import { createScenario } from '@apps/tests/shared/helpers';
import { Split, SplitHostDirection } from 'react-native-screens/experimental';
import { StyleSheet, Text, View, Button } from 'react-native';

const scenarioDescription: ScenarioDescription = {
name: 'Prop: direction',
key: 'test-split-direction',
details: `
Test the direction prop in Split component.
`,
platforms: ['ios'],
};

export function App() {
const [direction, setDirection] = useState<SplitHostDirection>('inherit');

return (
<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>
);
}

export function ColumnContent(props: {
columnTitle: string;
currentDirection?: SplitHostDirection;
onChangeDirection?: (dir: SplitHostDirection) => void;
}) {
return (
<View style={styles.container}>
<Text style={styles.columnTitle}>{props.columnTitle}</Text>

{props.onChangeDirection && (
<View style={styles.controlPanel}>
<Text style={styles.statusText}>
Current direction:{' '}
<Text style={styles.bold}>{props.currentDirection}</Text>
</Text>

<View style={styles.buttonRow}>
<Button
title="LTR"
onPress={() => props.onChangeDirection?.('ltr')}
/>
<Button
title="RTL"
onPress={() => props.onChangeDirection?.('rtl')}
/>
<Button
title="INHERIT"
onPress={() => props.onChangeDirection?.('inherit')}
/>
</View>
</View>
)}
</View>
);
}

const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
height: '100%',
alignItems: 'center',
justifyContent: 'center',
},
columnTitle: {
fontSize: 24,
fontWeight: 'bold',
},
controlPanel: {
marginTop: 20,
alignItems: 'center',
},
statusText: {
fontSize: 16,
marginBottom: 12,
},
bold: {
fontWeight: 'bold',
textTransform: 'uppercase',
},
buttonRow: {
flexDirection: 'row',
gap: 10,
justifyContent: 'center',
},
});

export default createScenario(App, scenarioDescription);
17 changes: 17 additions & 0 deletions ios/conversion/RNSConversions-SplitView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,23 @@ UISplitViewControllerDisplayModeButtonVisibility SplitViewDisplayModeButtonVisib
}
}

UITraitEnvironmentLayoutDirection UITraitEnvironmentLayoutDirectionFromSplitHostCppEquivalent(
react::RNSSplitHostLayoutDirection layoutDirection)
{
using enum facebook::react::RNSSplitHostLayoutDirection;
switch (layoutDirection) {
case Inherit:
return UITraitEnvironmentLayoutDirectionUnspecified;
case Ltr:
return UITraitEnvironmentLayoutDirectionLeftToRight;
case Rtl:
return UITraitEnvironmentLayoutDirectionRightToLeft;
default:
RCTLogError(@"[RNScreens] unsupported layout direction");
return UITraitEnvironmentLayoutDirectionUnspecified;
}
}

RNSOrientation RNSOrientationFromRNSSplitHostOrientation(react::RNSSplitHostOrientation orientation)
{
using enum facebook::react::RNSSplitHostOrientation;
Expand Down
3 changes: 3 additions & 0 deletions ios/conversion/RNSConversions.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,9 @@ std::optional<UISplitViewControllerColumn> SplitViewTopColumnForCollapsingFromHo

RNSOrientation RNSOrientationFromRNSSplitHostOrientation(react::RNSSplitHostOrientation orientation);

UITraitEnvironmentLayoutDirection UITraitEnvironmentLayoutDirectionFromSplitHostCppEquivalent(
react::RNSSplitHostLayoutDirection layoutDirection);

#pragma mark SplitScreen props

RNSSplitScreenColumnType RNSSplitScreenColumnTypeFromScreenProp(react::RNSSplitScreenColumnType columnType);
Expand Down
18 changes: 18 additions & 0 deletions ios/gamma/split/RNSSplitAppearanceApplicator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class RNSSplitAppearanceApplicator {
appearanceCoordinator.updateIfNeeded(.orientationUpdate) { [] in
RNSScreenWindowTraits.enforceDesiredDeviceOrientation()
}

appearanceCoordinator.updateIfNeeded(.layoutDirectionUpdateBelowIOS17) {
updateLayoutDirectionBelowIOS17(splitHost, splitHostController)
}
}

///
Expand Down Expand Up @@ -191,4 +195,18 @@ class RNSSplitAppearanceApplicator {
"[RNScreens] Split column constraints are invalid: minWidth \(minWidth) cannot be greater than maxWidth \(maxWidth)"
)
}

public func updateLayoutDirectionBelowIOS17(
_ splitHost: RNSSplitHostComponentView,
_ splitHostController: RNSSplitHostController,
) {
assert(
splitHostController.parent != nil,
"[RNScreens] Expected non-null parent view controller for layout direction update."
)
splitHostController.parent?.setOverrideTraitCollection(
UITraitCollection(layoutDirection: splitHost.layoutDirection),
forChild: splitHostController
)
}
}
1 change: 1 addition & 0 deletions ios/gamma/split/RNSSplitAppearanceUpdateFlags.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ struct RNSSplitAppearanceUpdateFlags: OptionSet {
static let secondaryScreenNavBarUpdate = RNSSplitAppearanceUpdateFlags(rawValue: 1 << 1)
static let displayModeUpdate = RNSSplitAppearanceUpdateFlags(rawValue: 1 << 2)
static let orientationUpdate = RNSSplitAppearanceUpdateFlags(rawValue: 1 << 3)
static let layoutDirectionUpdateBelowIOS17 = RNSSplitAppearanceUpdateFlags(rawValue: 1 << 4)
}
1 change: 1 addition & 0 deletions ios/gamma/split/RNSSplitHostComponentView.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ NS_ASSUME_NONNULL_BEGIN
#endif // RNS_IPHONE_OS_VERSION_AVAILABLE(26_0)

@property (nonatomic, readonly) RNSOrientation orientation;
@property (nonatomic, readonly) UITraitEnvironmentLayoutDirection layoutDirection;

@end

Expand Down
19 changes: 19 additions & 0 deletions ios/gamma/split/RNSSplitHostComponentView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ @implementation RNSSplitHostComponentView {
bool _needsSplitSecondaryScreenNavBarUpdate;
bool _needsSplitDisplayModeUpdate;
bool _needsSplitOrientationUpdate;
bool _needsSplitLayoutDirectionUpdate;
// We need this information to warn users about dynamic changes to behavior being currently unsupported.
bool _isShowSecondaryToggleButtonSet;
}
Expand All @@ -53,6 +54,7 @@ - (void)initState
_needsSplitSecondaryScreenNavBarUpdate = false;
_needsSplitDisplayModeUpdate = false;
_needsSplitOrientationUpdate = false;
_needsSplitLayoutDirectionUpdate = false;
_reactSubviews = [NSMutableArray new];
}

Expand Down Expand Up @@ -92,6 +94,7 @@ - (void)resetProps
_topColumnForCollapsingColumn = UISplitViewControllerColumnPrimary;

_orientation = RNSOrientationInherit;
_layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified;

_isShowSecondaryToggleButtonSet = false;
}
Expand Down Expand Up @@ -330,6 +333,12 @@ - (void)updateProps:(const facebook::react::Props::Shared &)props
_orientation = rnscreens::conversion::RNSOrientationFromRNSSplitHostOrientation(newComponentProps.orientation);
}

if (oldComponentProps.layoutDirection != newComponentProps.layoutDirection) {
_needsSplitLayoutDirectionUpdate = true;
_layoutDirection = rnscreens::conversion::UITraitEnvironmentLayoutDirectionFromSplitHostCppEquivalent(
newComponentProps.layoutDirection);
}

// This flag is set to true when showsSecondaryOnlyButton prop is assigned for the first time.
// This allows us to identify any subsequent changes to this prop,
// enabling us to warn users that dynamic changes are not supported.
Expand Down Expand Up @@ -365,6 +374,16 @@ - (void)requestSplitHostControllerForAppearanceUpdate
_needsSplitOrientationUpdate = false;
[_controller setNeedsOrientationUpdate];
}

if (_needsSplitLayoutDirectionUpdate && _controller != nil) {
_needsSplitLayoutDirectionUpdate = false;
#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];
}
}

#pragma mark - RCTMountingTransactionObserving
Expand Down
20 changes: 20 additions & 0 deletions ios/gamma/split/RNSSplitHostController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class RNSSplitHostController: UISplitViewController, ReactMountingTransac
RNSOrientationProvidingSwift
{
private var needsChildViewControllersUpdate = false
private var isLayoutDirectionUpdatePending = false

private var splitAppearanceCoordinator: RNSSplitAppearanceCoordinator
private var splitAppearanceApplicator: RNSSplitAppearanceApplicator
Expand Down Expand Up @@ -94,6 +95,15 @@ public class RNSSplitHostController: UISplitViewController, ReactMountingTransac
splitAppearanceCoordinator.needs(.orientationUpdate)
}

@objc
public func setNeedsLayoutDirectionUpdateBelowIOS17() {
if self.parent != nil {
splitAppearanceCoordinator.needs(.layoutDirectionUpdateBelowIOS17)
} else {
isLayoutDirectionUpdatePending = true
}
}

// MARK: Updating

@objc
Expand Down Expand Up @@ -552,4 +562,14 @@ extension RNSSplitHostController: UISplitViewControllerDelegate {
}
return proposedTopColumn
}

public override func didMove(toParent parent: UIViewController?) {
super.didMove(toParent: parent)

if parent != nil && isLayoutDirectionUpdatePending {
isLayoutDirectionUpdatePending = false
splitAppearanceApplicator.updateLayoutDirectionBelowIOS17(
self.splitHostComponentView, self)
}
}
}
3 changes: 2 additions & 1 deletion src/components/gamma/split/SplitHost.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const isValidDisplayModeForSplitBehavior = (
* EXPERIMENTAL API, MIGHT CHANGE W/O ANY NOTICE
*/
function SplitHost({ ref, ...props }: SplitHostProps) {
const { preferredDisplayMode, preferredSplitBehavior } = props;
const { direction, preferredDisplayMode, preferredSplitBehavior } = props;
const nativeRef = React.useRef<NativeRef>(null);

React.useImperativeHandle(
Expand Down Expand Up @@ -101,6 +101,7 @@ function SplitHost({ ref, ...props }: SplitHostProps) {
// 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}>
Comment on lines 101 to 105
{props.children}
</SplitHostNativeComponent>
Expand Down
25 changes: 24 additions & 1 deletion src/components/gamma/split/SplitHost.types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { NativeSyntheticEvent, ViewProps } from 'react-native';
import type { InterfaceOrientation } from '../../shared/types';
import type { Direction, InterfaceOrientation } from '../../shared/types';

// eslint-disable-next-line @typescript-eslint/ban-types
type GenericEmptyEvent = Readonly<{}>;
Expand All @@ -26,6 +26,8 @@ export type SplitDisplayMode =
| 'twoOverSecondary'
| 'twoDisplaceSecondary';

export type SplitHostDirection = Direction | 'inherit';

export type SplitHostOrientation = InterfaceOrientation | 'inherit';

export interface SplitColumnMetrics {
Expand Down Expand Up @@ -116,6 +118,27 @@ export interface SplitHostProps extends ViewProps {
children?: React.ReactNode;
ref?: React.Ref<SplitHostCommands>;

/**
* @summary Specifies the layout direction of the native container, its views and child containers.
*
* The following values are currently supported:
*
* - `inherit` - uses parent's layout direction,
* - `ltr` - forces left-to-right layout direction,
* - `rtl` - forces right-to-left layout direction.
*
* On iOS, this property sets `layoutDirection` trait override for the
* native split view controller. Property is propagated via the native trait
* system. The value will fallback to direction of the **native** app
* (`userInterfaceLayoutDirection`), potentially ignoring `react-native`'s
* override (e.g. when `forceRTL` is used). To mitigate this, you can pass
* `ltr`/`rtl` to this property depending on the value of `I18nManager.isRTL`.
*
* @default inherit
*
* @platform ios
*/
direction?: SplitHostDirection;
/**
* @summary An object describing bounds for column widths.
*
Expand Down
1 change: 1 addition & 0 deletions src/components/gamma/split/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type {
SplitPrimaryEdge,
SplitPrimaryBackgroundStyle,
SplitDisplayMode,
SplitHostDirection,
SplitHostOrientation,
SplitColumnMetrics,
SplitNavigableColumn,
Expand Down
3 changes: 3 additions & 0 deletions src/fabric/gamma/split/SplitHostNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ type SplitViewOrientation =

type SplitViewPrimaryBackgroundStyle = 'default' | 'none' | 'sidebar';

type LayoutDirection = 'inherit' | 'ltr' | 'rtl';

type SplitViewTopColumnForCollapsing =
| 'default'
| 'primary'
Expand All @@ -68,6 +70,7 @@ interface ColumnMetrics {
interface NativeProps extends ViewProps {
// Appearance

layoutDirection?: CT.WithDefault<LayoutDirection, 'inherit'>;
preferredDisplayMode?: CT.WithDefault<SplitViewDisplayMode, 'automatic'>;
preferredSplitBehavior?: CT.WithDefault<SplitViewSplitBehavior, 'automatic'>;
primaryEdge?: CT.WithDefault<SplitViewPrimaryEdge, 'leading'>;
Expand Down
Loading