Skip to content

Commit dd41747

Browse files
hristototovazizbecha
authored andcommitted
feat: add direction prop to PaperProvider and useLocale hook for RTL support (callstack#4921)
1 parent f8e0cee commit dd41747

71 files changed

Lines changed: 342 additions & 189 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/docs/guides/10-rtl.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
---
2+
title: RTL Support
3+
---
4+
5+
# RTL Support
6+
7+
React Native Paper supports right-to-left (RTL) layouts for languages such as Arabic and Hebrew.
8+
9+
## How it works
10+
11+
By default, React Native Paper reads the writing direction from `I18nManager.getConstants().isRTL` on native platforms. So it will use your existing RTL setup on initial render.
12+
13+
See [I18nManager](http://reactnative.dev/docs/i18nmanager) docs and [Enabling RTL support in Expo](https://docs.expo.dev/guides/localization/#enabling-rtl-support) to configure your app properly.
14+
15+
On the Web, the RTL value is not set globally, unlike native platforms. `I18nManager.getConstants().isRTL` is a no-op on [React Native Web](https://necolas.github.io/react-native-web/). To enable RTL globally, you can specify `dir` attribute on the `html` element:
16+
17+
```html
18+
<html dir="rtl">
19+
<!-- App content -->
20+
</html>
21+
```
22+
23+
Then, let `react-native-paper` know about it by using the `direction` prop on `PaperProvider` or the `LocaleProvider` component to match the writing direction in your app.
24+
25+
:::note
26+
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.
27+
:::
28+
29+
## Setting direction for the whole app
30+
31+
Pass the `direction` prop to `PaperProvider`. Defaults to `'rtl'` when `I18nManager.getConstants().isRTL` returns `true`, otherwise `'ltr'`.
32+
33+
Supported values:
34+
35+
- `'ltr'`: Left-to-right text direction for languages like English, French etc.
36+
- `'rtl'`: Right-to-left text direction for languages like Arabic, Hebrew etc.
37+
38+
```js
39+
import * as React from 'react';
40+
import { PaperProvider } from 'react-native-paper';
41+
import App from './src/App';
42+
43+
export default function Main() {
44+
return (
45+
<PaperProvider direction="rtl">
46+
<App />
47+
</PaperProvider>
48+
);
49+
}
50+
```
51+
52+
## Overriding direction for a subtree
53+
54+
Use `LocaleProvider` to override the direction for a specific part of the tree without affecting the rest of the app:
55+
56+
```js
57+
import * as React from 'react';
58+
import { LocaleProvider } from 'react-native-paper';
59+
60+
export default function ArabicSection() {
61+
return (
62+
<LocaleProvider direction="rtl">
63+
{/* Components here will use RTL layout */}
64+
</LocaleProvider>
65+
);
66+
}
67+
```
68+
69+
## Reading the current direction
70+
71+
The direction is available in your own components via the `useLocale` hook:
72+
73+
```js
74+
import * as React from 'react';
75+
import { useLocale } from 'react-native-paper';
76+
77+
function MyComponent() {
78+
const { direction } = useLocale();
79+
80+
// Use the direction
81+
}
82+
```

src/components/Appbar/AppbarBackIcon.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import * as React from 'react';
2-
import { I18nManager, Image, Platform, StyleSheet, View } from 'react-native';
2+
import { Image, Platform, StyleSheet, View } from 'react-native';
33

4+
import { useLocale } from '../../core/locale';
45
import MaterialCommunityIcon from '../MaterialCommunityIcon';
56

67
const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
8+
const { direction } = useLocale();
9+
const isRTL = direction === 'rtl';
710
const iosIconSize = size - 3;
811

912
return Platform.OS === 'ios' ? (
@@ -13,7 +16,7 @@ const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
1316
{
1417
width: size,
1518
height: size,
16-
transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }],
19+
transform: [{ scaleX: isRTL ? -1 : 1 }],
1720
},
1821
]}
1922
>
@@ -31,7 +34,7 @@ const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => {
3134
name="arrow-left"
3235
color={color}
3336
size={size}
34-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
37+
direction={direction}
3538
/>
3639
);
3740
};

src/components/DataTable/DataTablePagination.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
11
import * as React from 'react';
2-
import {
3-
I18nManager,
4-
StyleProp,
5-
StyleSheet,
6-
View,
7-
ViewStyle,
8-
} from 'react-native';
2+
import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
93

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

6+
import { useLocale } from '../../core/locale';
127
import { useInternalTheme } from '../../core/theming';
138
import Button from '../Button/Button';
149
import IconButton from '../IconButton/IconButton';
@@ -92,6 +87,7 @@ const PaginationControls = ({
9287
theme: themeOverrides,
9388
}: PaginationControlsProps) => {
9489
const theme = useInternalTheme(themeOverrides);
90+
const { direction } = useLocale();
9591

9692
const textColor = theme.colors.onSurface;
9793

@@ -104,7 +100,7 @@ const PaginationControls = ({
104100
name="page-first"
105101
color={color}
106102
size={size}
107-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
103+
direction={direction}
108104
/>
109105
)}
110106
iconColor={textColor}
@@ -120,7 +116,7 @@ const PaginationControls = ({
120116
name="chevron-left"
121117
color={color}
122118
size={size}
123-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
119+
direction={direction}
124120
/>
125121
)}
126122
iconColor={textColor}
@@ -135,7 +131,7 @@ const PaginationControls = ({
135131
name="chevron-right"
136132
color={color}
137133
size={size}
138-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
134+
direction={direction}
139135
/>
140136
)}
141137
iconColor={textColor}
@@ -151,7 +147,7 @@ const PaginationControls = ({
151147
name="page-last"
152148
color={color}
153149
size={size}
154-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
150+
direction={direction}
155151
/>
156152
)}
157153
iconColor={textColor}

src/components/DataTable/DataTableTitle.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import * as React from 'react';
22
import {
33
Animated,
44
GestureResponderEvent,
5-
I18nManager,
65
PixelRatio,
76
Pressable,
87
StyleProp,
@@ -11,6 +10,7 @@ import {
1110
ViewStyle,
1211
} from 'react-native';
1312

13+
import { useLocale } from '../../core/locale';
1414
import { useInternalTheme } from '../../core/theming';
1515
import type { ThemeProp } from '../../types';
1616
import MaterialCommunityIcon from '../MaterialCommunityIcon';
@@ -91,6 +91,7 @@ const DataTableTitle = ({
9191
...rest
9292
}: Props) => {
9393
const theme = useInternalTheme(themeOverrides);
94+
const { direction } = useLocale();
9495
const { current: spinAnim } = React.useRef<Animated.Value>(
9596
new Animated.Value(sortDirection === 'ascending' ? 0 : 1)
9697
);
@@ -118,7 +119,7 @@ const DataTableTitle = ({
118119
name="arrow-up"
119120
size={16}
120121
color={textColor}
121-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
122+
direction={direction}
122123
/>
123124
</Animated.View>
124125
) : null;
@@ -140,7 +141,7 @@ const DataTableTitle = ({
140141
// if numberOfLines causes wrap, center is lost. Align directly, sensitive to numeric and RTL
141142
numberOfLines > 1
142143
? numeric
143-
? I18nManager.getConstants().isRTL
144+
? direction === 'rtl'
144145
? styles.leftText
145146
: styles.rightText
146147
: styles.centerText

src/components/FAB/AnimatedFAB.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
Animated,
1010
Easing,
1111
GestureResponderEvent,
12-
I18nManager,
1312
Platform,
1413
ScrollView,
1514
StyleProp,
@@ -20,6 +19,7 @@ import {
2019
} from 'react-native';
2120

2221
import { getCombinedStyles, getFABColors, getLabelSizeWeb } from './utils';
22+
import { useLocale } from '../../core/locale';
2323
import { useInternalTheme } from '../../core/theming';
2424
import type { $Omit, $RemoveChildren, ThemeProp } from '../../types';
2525
import type { IconSource } from '../Icon';
@@ -219,12 +219,13 @@ const AnimatedFAB = ({
219219
...rest
220220
}: Props) => {
221221
const theme = useInternalTheme(themeOverrides);
222+
const { direction } = useLocale();
222223
const uppercase: boolean = uppercaseProp ?? false;
223224
const isIOS = Platform.OS === 'ios';
224225
const isWeb = Platform.OS === 'web';
225226
const isAnimatedFromRight = animateFrom === 'right';
226227
const isIconStatic = iconMode === 'static';
227-
const { isRTL } = I18nManager;
228+
const isRTL = direction === 'rtl';
228229
const labelRef = React.useRef<Text & HTMLElement>(null);
229230
const { current: visibility } = React.useRef<Animated.Value>(
230231
new Animated.Value(visible ? 1 : 0)
@@ -342,6 +343,7 @@ const AnimatedFAB = ({
342343
const combinedStyles = getCombinedStyles({
343344
isAnimatedFromRight,
344345
isIconStatic,
346+
isRTL,
345347
distance,
346348
animFAB,
347349
});

src/components/FAB/utils.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
import { MutableRefObject } from 'react';
2-
import {
3-
Animated,
4-
ColorValue,
5-
I18nManager,
6-
Platform,
7-
ViewStyle,
8-
} from 'react-native';
2+
import { Animated, ColorValue, Platform, ViewStyle } from 'react-native';
93

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

126
type GetCombinedStylesProps = {
137
isAnimatedFromRight: boolean;
148
isIconStatic: boolean;
9+
isRTL: boolean;
1510
distance: number;
1611
animFAB: Animated.Value;
1712
};
@@ -32,11 +27,10 @@ type BaseProps = {
3227
export const getCombinedStyles = ({
3328
isAnimatedFromRight,
3429
isIconStatic,
30+
isRTL,
3531
distance,
3632
animFAB,
3733
}: GetCombinedStylesProps): CombinedStyles => {
38-
const { isRTL } = I18nManager;
39-
4034
const defaultPositionStyles = { left: -distance, right: undefined };
4135

4236
const combinedStyles: CombinedStyles = {

src/components/Icon.tsx

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import * as React from 'react';
2-
import {
3-
I18nManager,
4-
Image,
5-
ImageSourcePropType,
6-
Platform,
7-
} from 'react-native';
2+
import { Image, ImageSourcePropType, Platform } from 'react-native';
83

94
import { accessibilityProps } from './MaterialCommunityIcon';
5+
import { useLocale } from '../core/locale';
106
import { Consumer as SettingsConsumer } from '../core/settings';
117
import { useInternalTheme } from '../core/theming';
128
import type { ThemeProp } from '../types';
@@ -109,12 +105,11 @@ const Icon = ({
109105
...rest
110106
}: Props) => {
111107
const theme = useInternalTheme(themeOverrides);
108+
const { direction: layoutDirection } = useLocale();
112109
const direction =
113110
typeof source === 'object' && source.direction && source.source
114111
? source.direction === 'auto'
115-
? I18nManager.getConstants().isRTL
116-
? 'rtl'
117-
: 'ltr'
112+
? layoutDirection
118113
: source.direction
119114
: null;
120115

src/components/List/ListAccordion.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import * as React from 'react';
22
import {
33
GestureResponderEvent,
4-
I18nManager,
54
NativeSyntheticEvent,
65
StyleProp,
76
StyleSheet,
@@ -16,6 +15,7 @@ import {
1615
import { ListAccordionGroupContext } from './ListAccordionGroup';
1716
import type { ListChildProps, Style } from './utils';
1817
import { getAccordionColors, getLeftStyles } from './utils';
18+
import { useLocale } from '../../core/locale';
1919
import { useInternalTheme } from '../../core/theming';
2020
import type { ThemeProp } from '../../types';
2121
import MaterialCommunityIcon from '../MaterialCommunityIcon';
@@ -198,6 +198,7 @@ const ListAccordion = ({
198198
hitSlop,
199199
}: Props) => {
200200
const theme = useInternalTheme(themeOverrides);
201+
const { direction } = useLocale();
201202
const [expanded, setExpanded] = React.useState<boolean>(
202203
expandedProp || false
203204
);
@@ -316,7 +317,7 @@ const ListAccordion = ({
316317
name={isExpanded ? 'chevron-up' : 'chevron-down'}
317318
color={descriptionColor}
318319
size={24}
319-
direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'}
320+
direction={direction}
320321
/>
321322
)}
322323
</View>

src/components/Menu/Menu.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
Dimensions,
55
Easing,
66
EmitterSubscription,
7-
I18nManager,
87
Keyboard,
98
KeyboardEvent as RNKeyboardEvent,
109
LayoutRectangle,
@@ -22,6 +21,7 @@ import {
2221
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2322

2423
import MenuItem from './MenuItem';
24+
import { useLocale } from '../../core/locale';
2525
import { useInternalTheme } from '../../core/theming';
2626
import type { Elevation, Theme, ThemeProp } from '../../types';
2727
import { addEventListener } from '../../utils/addEventListener';
@@ -200,6 +200,7 @@ const Menu = ({
200200
keyboardShouldPersistTaps,
201201
}: Props) => {
202202
const theme = useInternalTheme(themeOverrides);
203+
const { direction } = useLocale();
203204
const { colors: md3Colors } = theme as Theme;
204205
const insets = useSafeAreaInsets();
205206
const [rendered, setRendered] = React.useState(visible);
@@ -630,7 +631,7 @@ const Menu = ({
630631
top: isCoordinate(anchor)
631632
? topTransformation
632633
: topTransformation + additionalVerticalValue,
633-
...(I18nManager.getConstants().isRTL
634+
...(direction === 'rtl'
634635
? { right: leftTransformation }
635636
: { left: leftTransformation }),
636637
};

0 commit comments

Comments
 (0)