Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add a direction prop to NavigationContainer to specify rtl #11393

Merged
merged 4 commits into from
Sep 4, 2023
Merged
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
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
Original file line number Diff line number Diff line change
@@ -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
83 changes: 62 additions & 21 deletions example/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
} from '@react-navigation/stack';
import { createURL } from 'expo-linking';
import * as SplashScreen from 'expo-splash-screen';
import { reloadAsync } from 'expo-updates';
import * as React from 'react';
import {
I18nManager,
Expand All @@ -40,21 +41,36 @@ 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();

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)[];

let previousDirection = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr';

if (Platform.OS === 'web') {
if (
typeof localStorage !== 'undefined' &&
typeof document !== 'undefined' &&
document.documentElement
) {
const direction = localStorage.getItem(DIRECTION_PERSISTENCE_KEY);

if (direction !== null) {
previousDirection = direction;
document.documentElement.dir = direction;
}
}
}

export function App() {
const [theme, setTheme] = React.useState(DefaultTheme);

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

const [isRTL, setIsRTL] = React.useState(previousDirection === 'rtl');

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

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

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

setIsReady(true);
}
};

restoreState();
}, []);

React.useEffect(() => {
AsyncStorage.setItem(THEME_PERSISTENCE_KEY, theme.dark ? 'dark' : 'light');
}, [theme.dark]);

React.useEffect(() => {
const direction = isRTL ? 'rtl' : 'ltr';

AsyncStorage.setItem(DIRECTION_PERSISTENCE_KEY, direction);

if (Platform.OS === 'web') {
document.documentElement.dir = direction;
localStorage.setItem(DIRECTION_PERSISTENCE_KEY, direction);
}

if (isRTL !== I18nManager.getConstants().isRTL) {
I18nManager.forceRTL(isRTL);

if (Platform.OS !== 'web') {
reloadAsync();
}
}
}, [isRTL]);

const paperTheme = React.useMemo(() => {
const t = theme.dark ? PaperDarkTheme : PaperLightTheme;

Expand Down Expand Up @@ -135,12 +186,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 +266,7 @@ export function App() {
>
{() => (
<Drawer.Navigator
useLegacyImplementation={false}
screenOptions={{
drawerType: isLargeScreen ? 'permanent' : undefined,
}}
Expand All @@ -239,28 +292,16 @@ export function App() {
<SafeAreaView edges={['right', 'bottom', 'left']}>
<SettingsItem
label="Right to left"
value={I18nManager.getConstants().isRTL}
onValueChange={() => {
I18nManager.forceRTL(
!I18nManager.getConstants().isRTL
);
restartApp();
}}
value={isRTL}
onValueChange={() => setIsRTL((rtl) => !rtl)}
/>
<Divider />
<SettingsItem
label="Dark theme"
value={theme.dark}
onValueChange={() => {
AsyncStorage?.setItem(
THEME_PERSISTENCE_KEY,
theme.dark ? 'light' : 'dark'
);

setTheme((t) =>
t.dark ? DefaultTheme : DarkTheme
);
}}
onValueChange={() =>
setTheme((t) => (t.dark ? DefaultTheme : DarkTheme))
}
/>
<Divider />
{SCREEN_NAMES.map((name) => (
Expand Down
16 changes: 7 additions & 9 deletions packages/drawer/src/views/DrawerContentScrollView.tsx
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import * as React from 'react';

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

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

LocaleDirContext.displayName = 'LocaleDirContext';
Loading
Loading