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
72 changes: 72 additions & 0 deletions docs/docs/guides/10-rtl.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
title: RTL Support
---

# RTL Support

React Native Paper supports right-to-left (RTL) layouts for languages such as Arabic and Hebrew.

## How it works

On React Native, the writing direction is normally controlled by `I18nManager.forceRTL`. React Native Paper reads this automatically i.e. no configuration is needed for native apps that already set up RTL via `I18nManager`.

However, `I18nManager` is a no-op on **React Native Web**, which means RTL layouts break silently in web apps. The `direction` prop on `PaperProvider` (and the `LocaleProvider` component) lets you explicitly control the writing direction so Paper behaves correctly on all platforms.

:::note
The `direction` prop informs React Native Paper about the text direction in the app i.e. it doesn't change the text direction by itself. If you intend to support RTL languages, it's important to set this prop to the correct value that's configured in the app. If it doesn't match the actual text direction, the layout might be incorrect.
:::

## Setting direction for the whole app

Pass the `direction` prop to `PaperProvider`. Defaults to `'rtl'` when `I18nManager.getConstants().isRTL` returns `true`, otherwise `'ltr'`.

Supported values:

- `'ltr'`: Left-to-right text direction for languages like English, French etc.
- `'rtl'`: Right-to-left text direction for languages like Arabic, Hebrew etc.

```js
import * as React from 'react';
import { PaperProvider } from 'react-native-paper';
import App from './src/App';

export default function Main() {
return (
<PaperProvider direction="rtl">
<App />
</PaperProvider>
);
}
```

## Overriding direction for a subtree

Use `LocaleProvider` to override the direction for a specific part of the tree without affecting the rest of the app:

```js
import * as React from 'react';
import { LocaleProvider } from 'react-native-paper';

export default function ArabicSection() {
return (
<LocaleProvider direction="rtl">
{/* Components here will use RTL layout */}
</LocaleProvider>
);
}
```

## Reading the current direction

The direction is available in your own components via the `useLocale` hook:

```js
import * as React from 'react';
import { useLocale } from 'react-native-paper';

function MyComponent() {
const { direction } = useLocale();

// Use the direction
}
```
9 changes: 6 additions & 3 deletions src/components/Appbar/AppbarBackIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as React from 'react';
import { I18nManager, Image, Platform, StyleSheet, View } from 'react-native';
import { Image, Platform, StyleSheet, View } from 'react-native';

import { useLocale } from '../../core/locale';
import MaterialCommunityIcon from '../MaterialCommunityIcon';

const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
const { direction } = useLocale();
const isRTL = direction === 'rtl';
const iosIconSize = size - 3;

return Platform.OS === 'ios' ? (
Expand All @@ -13,7 +16,7 @@ const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
{
width: size,
height: size,
transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }],
transform: [{ scaleX: isRTL ? -1 : 1 }],
},
]}
>
Expand All @@ -31,7 +34,7 @@ const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
name="arrow-left"
color={color}
size={size}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
);
};
Expand Down
18 changes: 7 additions & 11 deletions src/components/DataTable/DataTablePagination.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import * as React from 'react';
import {
I18nManager,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native';
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';

import type { ThemeProp } from 'src/types';

import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import Button from '../Button/Button';
import IconButton from '../IconButton/IconButton';
Expand Down Expand Up @@ -92,6 +87,7 @@ const PaginationControls = ({
theme: themeOverrides,
}: PaginationControlsProps) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();

const textColor = theme.colors.onSurface;

Expand All @@ -104,7 +100,7 @@ const PaginationControls = ({
name="page-first"
color={color}
size={size}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
)}
iconColor={textColor}
Expand All @@ -120,7 +116,7 @@ const PaginationControls = ({
name="chevron-left"
color={color}
size={size}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
)}
iconColor={textColor}
Expand All @@ -135,7 +131,7 @@ const PaginationControls = ({
name="chevron-right"
color={color}
size={size}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
)}
iconColor={textColor}
Expand All @@ -151,7 +147,7 @@ const PaginationControls = ({
name="page-last"
color={color}
size={size}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
)}
iconColor={textColor}
Expand Down
7 changes: 4 additions & 3 deletions src/components/DataTable/DataTableTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from 'react';
import {
Animated,
GestureResponderEvent,
I18nManager,
PixelRatio,
Pressable,
StyleProp,
Expand All @@ -11,6 +10,7 @@ import {
ViewStyle,
} from 'react-native';

import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../types';
import MaterialCommunityIcon from '../MaterialCommunityIcon';
Expand Down Expand Up @@ -91,6 +91,7 @@ const DataTableTitle = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();
const { current: spinAnim } = React.useRef<Animated.Value>(
new Animated.Value(sortDirection === 'ascending' ? 0 : 1)
);
Expand Down Expand Up @@ -118,7 +119,7 @@ const DataTableTitle = ({
name="arrow-up"
size={16}
color={textColor}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
</Animated.View>
) : null;
Expand All @@ -140,7 +141,7 @@ const DataTableTitle = ({
// if numberOfLines causes wrap, center is lost. Align directly, sensitive to numeric and RTL
numberOfLines > 1
? numeric
? I18nManager.getConstants().isRTL
? direction === 'rtl'
? styles.leftText
: styles.rightText
: styles.centerText
Expand Down
6 changes: 4 additions & 2 deletions src/components/FAB/AnimatedFAB.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
Animated,
Easing,
GestureResponderEvent,
I18nManager,
Platform,
ScrollView,
StyleProp,
Expand All @@ -20,6 +19,7 @@ import {
} from 'react-native';

import { getCombinedStyles, getFABColors, getLabelSizeWeb } from './utils';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import type { $Omit, $RemoveChildren, ThemeProp } from '../../types';
import type { IconSource } from '../Icon';
Expand Down Expand Up @@ -219,12 +219,13 @@ const AnimatedFAB = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();
const uppercase: boolean = uppercaseProp ?? false;
const isIOS = Platform.OS === 'ios';
const isWeb = Platform.OS === 'web';
const isAnimatedFromRight = animateFrom === 'right';
const isIconStatic = iconMode === 'static';
const { isRTL } = I18nManager;
const isRTL = direction === 'rtl';
const labelRef = React.useRef<Text & HTMLElement>(null);
const { current: visibility } = React.useRef<Animated.Value>(
new Animated.Value(visible ? 1 : 0)
Expand Down Expand Up @@ -342,6 +343,7 @@ const AnimatedFAB = ({
const combinedStyles = getCombinedStyles({
isAnimatedFromRight,
isIconStatic,
isRTL,
distance,
animFAB,
});
Expand Down
12 changes: 3 additions & 9 deletions src/components/FAB/utils.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import { MutableRefObject } from 'react';
import {
Animated,
ColorValue,
I18nManager,
Platform,
ViewStyle,
} from 'react-native';
import { Animated, ColorValue, Platform, ViewStyle } from 'react-native';

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

type GetCombinedStylesProps = {
isAnimatedFromRight: boolean;
isIconStatic: boolean;
isRTL: boolean;
distance: number;
animFAB: Animated.Value;
};
Expand All @@ -32,11 +27,10 @@ type BaseProps = {
export const getCombinedStyles = ({
isAnimatedFromRight,
isIconStatic,
isRTL,
distance,
animFAB,
}: GetCombinedStylesProps): CombinedStyles => {
const { isRTL } = I18nManager;

const defaultPositionStyles = { left: -distance, right: undefined };

const combinedStyles: CombinedStyles = {
Expand Down
13 changes: 4 additions & 9 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import * as React from 'react';
import {
I18nManager,
Image,
ImageSourcePropType,
Platform,
} from 'react-native';
import { Image, ImageSourcePropType, Platform } from 'react-native';

import { accessibilityProps } from './MaterialCommunityIcon';
import { useLocale } from '../core/locale';
import { Consumer as SettingsConsumer } from '../core/settings';
import { useInternalTheme } from '../core/theming';
import type { ThemeProp } from '../types';
Expand Down Expand Up @@ -109,12 +105,11 @@ const Icon = ({
...rest
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction: layoutDirection } = useLocale();
const direction =
typeof source === 'object' && source.direction && source.source
? source.direction === 'auto'
? I18nManager.getConstants().isRTL
? 'rtl'
: 'ltr'
? layoutDirection
: source.direction
: null;

Expand Down
5 changes: 3 additions & 2 deletions src/components/List/ListAccordion.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
import {
GestureResponderEvent,
I18nManager,
NativeSyntheticEvent,
StyleProp,
StyleSheet,
Expand All @@ -16,6 +15,7 @@ import {
import { ListAccordionGroupContext } from './ListAccordionGroup';
import type { ListChildProps, Style } from './utils';
import { getAccordionColors, getLeftStyles } from './utils';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import type { ThemeProp } from '../../types';
import MaterialCommunityIcon from '../MaterialCommunityIcon';
Expand Down Expand Up @@ -198,6 +198,7 @@ const ListAccordion = ({
hitSlop,
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();
const [expanded, setExpanded] = React.useState<boolean>(
expandedProp || false
);
Expand Down Expand Up @@ -316,7 +317,7 @@ const ListAccordion = ({
name={isExpanded ? 'chevron-up' : 'chevron-down'}
color={descriptionColor}
size={24}
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
direction={direction}
/>
)}
</View>
Expand Down
5 changes: 3 additions & 2 deletions src/components/Menu/Menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {
Dimensions,
Easing,
EmitterSubscription,
I18nManager,
Keyboard,
KeyboardEvent as RNKeyboardEvent,
LayoutRectangle,
Expand All @@ -22,6 +21,7 @@ import {
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import MenuItem from './MenuItem';
import { useLocale } from '../../core/locale';
import { useInternalTheme } from '../../core/theming';
import type { MD3Elevation, MD3Theme, ThemeProp } from '../../types';
import { ElevationLevels } from '../../types';
Expand Down Expand Up @@ -196,6 +196,7 @@ const Menu = ({
keyboardShouldPersistTaps,
}: Props) => {
const theme = useInternalTheme(themeOverrides);
const { direction } = useLocale();
const { colors: md3Colors } = theme as MD3Theme;
const insets = useSafeAreaInsets();
const [rendered, setRendered] = React.useState(visible);
Expand Down Expand Up @@ -626,7 +627,7 @@ const Menu = ({
top: isCoordinate(anchor)
? topTransformation
: topTransformation + additionalVerticalValue,
...(I18nManager.getConstants().isRTL
...(direction === 'rtl'
? { right: leftTransformation }
: { left: leftTransformation }),
};
Expand Down
Loading
Loading