Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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: layoutDirection',
key: 'test-split-layout-direction',
Comment thread
sgaczol marked this conversation as resolved.
Outdated
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
22 changes: 21 additions & 1 deletion 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 @@ -93,6 +94,15 @@ public class RNSSplitHostController: UISplitViewController, ReactMountingTransac
public func setNeedsOrientationUpdate() {
splitAppearanceCoordinator.needs(.orientationUpdate)
}

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

// MARK: Updating

Expand Down Expand Up @@ -140,7 +150,7 @@ public class RNSSplitHostController: UISplitViewController, ReactMountingTransac

needsChildViewControllersUpdate = false
}

func updateSplitAppearanceIfNeeded() {
splitAppearanceApplicator.updateAppearanceIfNeeded(
self.splitHostComponentView, self, self.splitAppearanceCoordinator)
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