Skip to content

Commit

Permalink
feat!: add a direction prop to NavigationContainer to specify rtl…
Browse files Browse the repository at this point in the history
…/ltr

BREAKING CHANGE: Previously the navigators tried to detect RTL automatically and
adjust the UI. However this is problematic since we cannot detect RTL in all cases
(e.g. on Web).

This adds an optional `direction` prop to `NavigationContainer` instead
so that user can specify when React Navigation's UI needs to be adjusted for RTL.
  • Loading branch information
satya164 committed Sep 4, 2023
1 parent 2c0f604 commit 8170b2d
Show file tree
Hide file tree
Showing 26 changed files with 224 additions and 131 deletions.
5 changes: 0 additions & 5 deletions example/src/Restart.native.tsx

This file was deleted.

1 change: 0 additions & 1 deletion example/src/Restart.tsx

This file was deleted.

8 changes: 7 additions & 1 deletion example/src/Screens/SimpleStack.tsx
@@ -1,6 +1,7 @@
import type { ParamListBase } from '@react-navigation/native';
import {
createStackNavigator,
HeaderStyleInterpolators,
StackNavigationOptions,
StackScreenProps,
} from '@react-navigation/stack';
Expand Down Expand Up @@ -137,7 +138,12 @@ export function SimpleStack({
}, [navigation]);

return (
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Navigator
screenOptions={{
...screenOptions,
headerStyleInterpolator: HeaderStyleInterpolators.forUIKit,
}}
>
<Stack.Screen
name="Article"
component={ArticleScreen}
Expand Down
32 changes: 25 additions & 7 deletions example/src/index.tsx
Expand Up @@ -40,18 +40,20 @@ import {
} from 'react-native-paper';
import { SafeAreaView } from 'react-native-safe-area-context';

import { restartApp } from './Restart';
import { RootDrawerParamList, RootStackParamList, SCREENS } from './screens';
import { NotFound } from './Screens/NotFound';
import { SettingsItem } from './Shared/SettingsItem';

SplashScreen.preventAutoHideAsync();

I18nManager.forceRTL(false);

const Drawer = createDrawerNavigator<RootDrawerParamList>();
const Stack = createStackNavigator<RootStackParamList>();

const NAVIGATION_PERSISTENCE_KEY = 'NAVIGATION_STATE';
const THEME_PERSISTENCE_KEY = 'THEME_TYPE';
const DIRECTION_PERSISTENCE_KEY = 'DIRECTION';

const SCREEN_NAMES = Object.keys(SCREENS) as (keyof typeof SCREENS)[];

Expand All @@ -63,6 +65,8 @@ export function App() {
InitialState | undefined
>();

const [isRTL, setIsRTL] = React.useState(false);

React.useEffect(() => {
const restoreState = async () => {
try {
Expand All @@ -88,6 +92,16 @@ export function App() {
// Ignore
}

try {
const direction = await AsyncStorage?.getItem(
DIRECTION_PERSISTENCE_KEY
);

setIsRTL(direction === 'rtl');
} catch (e) {
// Ignore
}

setIsReady(true);
}
};
Expand Down Expand Up @@ -135,12 +149,13 @@ export function App() {
SplashScreen.hideAsync();
}}
onStateChange={(state) =>
AsyncStorage?.setItem(
AsyncStorage.setItem(
NAVIGATION_PERSISTENCE_KEY,
JSON.stringify(state)
)
}
theme={theme}
direction={isRTL ? 'rtl' : 'ltr'}
linking={{
// To test deep linking on, run the following in the Terminal:
// Android: adb shell am start -a android.intent.action.VIEW -d "exp://127.0.0.1:19000/--/simple-stack"
Expand Down Expand Up @@ -214,6 +229,7 @@ export function App() {
>
{() => (
<Drawer.Navigator
useLegacyImplementation={false}
screenOptions={{
drawerType: isLargeScreen ? 'permanent' : undefined,
}}
Expand All @@ -239,20 +255,22 @@ export function App() {
<SafeAreaView edges={['right', 'bottom', 'left']}>
<SettingsItem
label="Right to left"
value={I18nManager.getConstants().isRTL}
value={isRTL}
onValueChange={() => {
I18nManager.forceRTL(
!I18nManager.getConstants().isRTL
AsyncStorage.setItem(
DIRECTION_PERSISTENCE_KEY,
isRTL ? 'ltr' : 'rtl'
);
restartApp();

setIsRTL((rtl) => !rtl);
}}
/>
<Divider />
<SettingsItem
label="Dark theme"
value={theme.dark}
onValueChange={() => {
AsyncStorage?.setItem(
AsyncStorage.setItem(
THEME_PERSISTENCE_KEY,
theme.dark ? 'light' : 'dark'
);
Expand Down
16 changes: 7 additions & 9 deletions packages/drawer/src/views/DrawerContentScrollView.tsx
@@ -1,10 +1,6 @@
import { useLocale } from '@react-navigation/native';
import * as React from 'react';
import {
I18nManager,
ScrollView,
ScrollViewProps,
StyleSheet,
} from 'react-native';
import { ScrollView, ScrollViewProps, StyleSheet } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import { DrawerPositionContext } from '../utils/DrawerPositionContext';
Expand All @@ -19,10 +15,12 @@ function DrawerContentScrollViewInner(
) {
const drawerPosition = React.useContext(DrawerPositionContext);
const insets = useSafeAreaInsets();
const { direction } = useLocale();

const isRight = I18nManager.getConstants().isRTL
? drawerPosition === 'left'
: drawerPosition === 'right';
const isRight =
direction === 'rtl'
? drawerPosition === 'left'
: drawerPosition === 'right';

return (
<ScrollView
Expand Down
7 changes: 5 additions & 2 deletions packages/drawer/src/views/DrawerView.tsx
Expand Up @@ -9,10 +9,11 @@ import {
DrawerNavigationState,
DrawerStatus,
ParamListBase,
useLocale,
useTheme,
} from '@react-navigation/native';
import * as React from 'react';
import { BackHandler, I18nManager, Platform, StyleSheet } from 'react-native';
import { BackHandler, Platform, StyleSheet } from 'react-native';
import { Drawer } from 'react-native-drawer-layout';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import useLatestCallback from 'use-latest-callback';
Expand Down Expand Up @@ -51,10 +52,12 @@ function DrawerViewBase({
Platform.OS === 'android' ||
Platform.OS === 'ios',
}: Props) {
const { direction } = useLocale();

const focusedRouteKey = state.routes[state.index].key;
const {
drawerHideStatusBarOnOpen,
drawerPosition = I18nManager.getConstants().isRTL ? 'right' : 'left',
drawerPosition = direction === 'rtl' ? 'right' : 'left',
drawerStatusBarAnimation,
drawerStyle,
drawerType,
Expand Down
19 changes: 12 additions & 7 deletions packages/elements/src/Header/HeaderBackButton.tsx
@@ -1,8 +1,7 @@
import { useTheme } from '@react-navigation/native';
import { useLocale, useTheme } from '@react-navigation/native';
import * as React from 'react';
import {
Animated,
I18nManager,
Image,
LayoutChangeEvent,
Platform,
Expand Down Expand Up @@ -34,6 +33,7 @@ export function HeaderBackButton({
style,
}: HeaderBackButtonProps) {
const { colors, fonts } = useTheme();
const { direction } = useLocale();

const [initialLabelWidth, setInitialLabelWidth] = React.useState<
undefined | number
Expand All @@ -50,7 +50,11 @@ export function HeaderBackButton({
const handleLabelLayout = (e: LayoutChangeEvent) => {
onLabelLayout?.(e);

setInitialLabelWidth(e.nativeEvent.layout.x + e.nativeEvent.layout.width);
const { layout } = e.nativeEvent;

setInitialLabelWidth(
(direction === 'rtl' ? layout.y : layout.x) + layout.width
);
};

const shouldTruncateLabel = () => {
Expand All @@ -71,6 +75,7 @@ export function HeaderBackButton({
<Image
style={[
styles.icon,
direction === 'rtl' && styles.flip,
Boolean(labelVisible) && styles.iconWithLabel,
Boolean(tintColor) && { tintColor },
]}
Expand Down Expand Up @@ -131,7 +136,7 @@ export function HeaderBackButton({
<View style={styles.iconMaskContainer}>
<Image
source={require('../assets/back-icon-mask.png')}
style={styles.iconMask}
style={[styles.iconMask, direction === 'rtl' && styles.flip]}
/>
<View style={styles.iconMaskFillerRect} />
</View>
Expand Down Expand Up @@ -211,14 +216,12 @@ const styles = StyleSheet.create({
marginRight: 22,
marginVertical: 12,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }],
},
default: {
height: 24,
width: 24,
margin: 3,
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }],
},
}),
iconWithLabel:
Expand All @@ -243,6 +246,8 @@ const styles = StyleSheet.create({
marginVertical: 12,
alignSelf: 'center',
resizeMode: 'contain',
transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }],
},
flip: {
transform: [{ scaleX: -1 }],
},
});
13 changes: 4 additions & 9 deletions packages/native-stack/src/views/HeaderConfig.tsx
@@ -1,13 +1,7 @@
import { getHeaderTitle, HeaderTitle } from '@react-navigation/elements';
import { Route, useTheme } from '@react-navigation/native';
import { Route, useLocale, useTheme } from '@react-navigation/native';
import * as React from 'react';
import {
I18nManager,
Platform,
StyleSheet,
TextStyle,
View,
} from 'react-native';
import { Platform, StyleSheet, TextStyle, View } from 'react-native';
import {
isSearchBarAvailableForCurrentPlatform,
ScreenStackHeaderBackButtonImage,
Expand Down Expand Up @@ -59,6 +53,7 @@ export function HeaderConfig({
title,
canGoBack,
}: Props): JSX.Element {
const { direction } = useLocale();
const { colors, fonts } = useTheme();
const tintColor =
headerTintColor ?? (Platform.OS === 'ios' ? colors.primary : colors.text);
Expand Down Expand Up @@ -202,7 +197,7 @@ export function HeaderConfig({
backTitleFontSize={backTitleFontSize}
blurEffect={headerBlurEffect}
color={tintColor}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
disableBackButtonMenu={headerBackButtonMenuEnabled === false}
hidden={headerShown === false}
hideBackButton={headerBackVisible === false}
Expand Down
7 changes: 7 additions & 0 deletions packages/native/src/LocaleDirContext.tsx
@@ -0,0 +1,7 @@
import * as React from 'react';

import type { LocaleDirection } from './types';

export const LocaleDirContext = React.createContext<LocaleDirection>('ltr');

LocaleDirContext.displayName = 'LocaleDirContext';
35 changes: 23 additions & 12 deletions packages/native/src/NavigationContainer.tsx
Expand Up @@ -11,9 +11,15 @@ import {
import * as React from 'react';

import { LinkingContext } from './LinkingContext';
import { LocaleDirContext } from './LocaleDirContext';
import { DefaultTheme } from './theming/DefaultTheme';
import { ThemeProvider } from './theming/ThemeProvider';
import type { DocumentTitleOptions, LinkingOptions, Theme } from './types';
import type {
DocumentTitleOptions,
LinkingOptions,
LocaleDirection,
Theme,
} from './types';
import { useBackButton } from './useBackButton';
import { useDocumentTitle } from './useDocumentTitle';
import { useLinking } from './useLinking';
Expand All @@ -29,6 +35,7 @@ declare global {
global.REACT_NAVIGATION_DEVTOOLS = new WeakMap();

type Props<ParamList extends {}> = NavigationContainerProps & {
direction?: LocaleDirection;
theme?: Theme;
linking?: LinkingOptions<ParamList>;
fallback?: React.ReactNode;
Expand All @@ -43,6 +50,7 @@ type Props<ParamList extends {}> = NavigationContainerProps & {
* @param props.onReady Callback which is called after the navigation tree mounts.
* @param props.onStateChange Callback which is called with the latest navigation state when it changes.
* @param props.onUnhandledAction Callback which is called when an action is not handled.
* @param props.direction Text direction of the components. Defaults to `'ltr'`.
* @param props.theme Theme object for the navigators.
* @param props.linking Options for deep linking. Deep link handling is enabled when this prop is provided, unless `linking.enabled` is `false`.
* @param props.fallback Fallback component to render until we have finished getting initial state when linking is enabled. Defaults to `null`.
Expand All @@ -52,6 +60,7 @@ type Props<ParamList extends {}> = NavigationContainerProps & {
*/
function NavigationContainerInner(
{
direction = 'ltr',
theme = DefaultTheme,
linking,
fallback = null,
Expand Down Expand Up @@ -114,17 +123,19 @@ function NavigationContainerInner(
}

return (
<LinkingContext.Provider value={linkingContext}>
<ThemeProvider value={theme}>
<BaseNavigationContainer
{...rest}
initialState={
rest.initialState == null ? initialState : rest.initialState
}
ref={refContainer}
/>
</ThemeProvider>
</LinkingContext.Provider>
<LocaleDirContext.Provider value={direction}>
<LinkingContext.Provider value={linkingContext}>
<ThemeProvider value={theme}>
<BaseNavigationContainer
{...rest}
initialState={
rest.initialState == null ? initialState : rest.initialState
}
ref={refContainer}
/>
</ThemeProvider>
</LinkingContext.Provider>
</LocaleDirContext.Provider>
);
}

Expand Down
1 change: 1 addition & 0 deletions packages/native/src/index.tsx
Expand Up @@ -10,5 +10,6 @@ export { useTheme } from './theming/useTheme';
export * from './types';
export { useLinkProps } from './useLinkProps';
export { useLinkTools } from './useLinkTools';
export { useLocale } from './useLocale';
export { useScrollToTop } from './useScrollToTop';
export * from '@react-navigation/core';
2 changes: 2 additions & 0 deletions packages/native/src/types.tsx
Expand Up @@ -6,6 +6,8 @@ import type {
Route,
} from '@react-navigation/core';

export type LocaleDirection = 'ltr' | 'rtl';

type FontStyle = {
fontFamily: string;
fontWeight:
Expand Down

0 comments on commit 8170b2d

Please sign in to comment.