From 0cfb39b498025d0977b824785d4f2d19bd91eba3 Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Tue, 5 May 2026 12:23:25 +0300 Subject: [PATCH 1/8] feat: add useLocale hook and direction prop for RTL support --- src/components/Appbar/AppbarBackIcon.tsx | 9 ++++-- .../DataTable/DataTablePagination.tsx | 11 +++---- src/components/DataTable/DataTableTitle.tsx | 7 +++-- src/components/FAB/AnimatedFAB.tsx | 6 ++-- src/components/FAB/utils.ts | 12 ++------ src/components/Icon.tsx | 13 +++----- src/components/List/ListAccordion.tsx | 5 ++-- src/components/Menu/Menu.tsx | 5 ++-- src/components/ProgressBar.tsx | 5 ++-- src/components/Searchbar.tsx | 10 ++++--- src/components/Snackbar.tsx | 7 ++--- src/components/TextInput/TextInputFlat.tsx | 25 +++++----------- .../TextInput/TextInputOutlined.tsx | 15 ++++------ src/components/Typography/AnimatedText.tsx | 12 ++------ src/components/Typography/Text.tsx | 4 +-- .../Appbar/__snapshots__/Appbar.test.tsx.snap | 2 +- src/components/__tests__/TextInput.test.tsx | 30 +++++++++---------- .../__snapshots__/Searchbar.test.tsx.snap | 6 ++-- src/core/PaperProvider.tsx | 8 ++++- src/core/locale.tsx | 23 ++++++++++++++ src/index.tsx | 2 ++ .../views/MaterialBottomTabView.tsx | 6 ++-- 22 files changed, 119 insertions(+), 104 deletions(-) create mode 100644 src/core/locale.tsx diff --git a/src/components/Appbar/AppbarBackIcon.tsx b/src/components/Appbar/AppbarBackIcon.tsx index 7579a3f1a6..35eb98d18f 100644 --- a/src/components/Appbar/AppbarBackIcon.tsx +++ b/src/components/Appbar/AppbarBackIcon.tsx @@ -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' ? ( @@ -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 }], }, ]} > @@ -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} /> ); }; diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx index c7a5375f82..abd2ae319a 100644 --- a/src/components/DataTable/DataTablePagination.tsx +++ b/src/components/DataTable/DataTablePagination.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { ColorValue, - I18nManager, StyleProp, StyleSheet, View, @@ -11,6 +10,7 @@ import { import color from 'color'; 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'; @@ -107,6 +107,7 @@ const PaginationControls = ({ paginationControlRippleColor, }: PaginationControlsProps) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const textColor = theme.colors.onSurface; @@ -119,7 +120,7 @@ const PaginationControls = ({ name="page-first" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -136,7 +137,7 @@ const PaginationControls = ({ name="chevron-left" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -152,7 +153,7 @@ const PaginationControls = ({ name="chevron-right" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -169,7 +170,7 @@ const PaginationControls = ({ name="page-last" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} diff --git a/src/components/DataTable/DataTableTitle.tsx b/src/components/DataTable/DataTableTitle.tsx index c427ac26b4..4745fe4738 100644 --- a/src/components/DataTable/DataTableTitle.tsx +++ b/src/components/DataTable/DataTableTitle.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Animated, GestureResponderEvent, - I18nManager, PixelRatio, Pressable, StyleProp, @@ -13,6 +12,7 @@ import { import color from 'color'; +import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import MaterialCommunityIcon from '../MaterialCommunityIcon'; @@ -93,6 +93,7 @@ const DataTableTitle = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { current: spinAnim } = React.useRef( new Animated.Value(sortDirection === 'ascending' ? 0 : 1) ); @@ -120,7 +121,7 @@ const DataTableTitle = ({ name="arrow-up" size={16} color={textColor} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> ) : null; @@ -142,7 +143,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 diff --git a/src/components/FAB/AnimatedFAB.tsx b/src/components/FAB/AnimatedFAB.tsx index 6b6964a4a9..f3dbdb4a22 100644 --- a/src/components/FAB/AnimatedFAB.tsx +++ b/src/components/FAB/AnimatedFAB.tsx @@ -10,7 +10,6 @@ import { Animated, Easing, GestureResponderEvent, - I18nManager, Platform, ScrollView, StyleProp, @@ -23,6 +22,7 @@ import { import color from 'color'; 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'; @@ -232,12 +232,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(null); const { current: visibility } = React.useRef( new Animated.Value(visible ? 1 : 0) @@ -359,6 +360,7 @@ const AnimatedFAB = ({ const combinedStyles = getCombinedStyles({ isAnimatedFromRight, isIconStatic, + isRTL, distance, animFAB, }); diff --git a/src/components/FAB/utils.ts b/src/components/FAB/utils.ts index 42dd05ff7e..41e598e127 100644 --- a/src/components/FAB/utils.ts +++ b/src/components/FAB/utils.ts @@ -1,11 +1,5 @@ import { MutableRefObject } from 'react'; -import { - Animated, - ColorValue, - I18nManager, - Platform, - ViewStyle, -} from 'react-native'; +import { Animated, ColorValue, Platform, ViewStyle } from 'react-native'; import color from 'color'; @@ -14,6 +8,7 @@ import type { InternalTheme } from '../../types'; type GetCombinedStylesProps = { isAnimatedFromRight: boolean; isIconStatic: boolean; + isRTL: boolean; distance: number; animFAB: Animated.Value; }; @@ -35,11 +30,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 = { diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index b886e06fe2..a3d2d28570 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -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'; @@ -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; diff --git a/src/components/List/ListAccordion.tsx b/src/components/List/ListAccordion.tsx index ace8810a71..f1ed9bab63 100644 --- a/src/components/List/ListAccordion.tsx +++ b/src/components/List/ListAccordion.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { ColorValue, GestureResponderEvent, - I18nManager, NativeSyntheticEvent, StyleProp, StyleSheet, @@ -17,6 +16,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'; @@ -204,6 +204,7 @@ const ListAccordion = ({ hitSlop, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const [expanded, setExpanded] = React.useState( expandedProp || false ); @@ -324,7 +325,7 @@ const ListAccordion = ({ name={isExpanded ? 'chevron-up' : 'chevron-down'} color={descriptionColor} size={24} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 47727f7e10..2b53e2f146 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -4,7 +4,6 @@ import { Dimensions, Easing, EmitterSubscription, - I18nManager, Keyboard, KeyboardEvent as RNKeyboardEvent, LayoutRectangle, @@ -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'; @@ -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); @@ -626,7 +627,7 @@ const Menu = ({ top: isCoordinate(anchor) ? topTransformation : topTransformation + additionalVerticalValue, - ...(I18nManager.getConstants().isRTL + ...(direction === 'rtl' ? { right: leftTransformation } : { left: leftTransformation }), }; diff --git a/src/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index c307df2479..561f68b7af 100644 --- a/src/components/ProgressBar.tsx +++ b/src/components/ProgressBar.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Animated, - I18nManager, LayoutChangeEvent, Platform, StyleProp, @@ -10,6 +9,7 @@ import { ViewStyle, } from 'react-native'; +import { useLocale } from '../core/locale'; import { useInternalTheme } from '../core/theming'; import type { ThemeProp } from '../types'; @@ -53,7 +53,6 @@ export type Props = React.ComponentPropsWithRef & { const INDETERMINATE_DURATION = 2000; const INDETERMINATE_MAX_WIDTH = 0.6; -const { isRTL } = I18nManager; /** * Progress bar is an indicator used to present progress of some activity in the app. @@ -84,6 +83,8 @@ const ProgressBar = ({ }: Props) => { const isWeb = Platform.OS === 'web'; const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); + const isRTL = direction === 'rtl'; const { current: timer } = React.useRef( new Animated.Value(0) ); diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx index 6377899280..94941030d1 100644 --- a/src/components/Searchbar.tsx +++ b/src/components/Searchbar.tsx @@ -3,7 +3,6 @@ import { Animated, ColorValue, GestureResponderEvent, - I18nManager, Platform, StyleProp, StyleSheet, @@ -22,6 +21,7 @@ import type { IconSource } from './Icon'; import IconButton from './IconButton/IconButton'; import MaterialCommunityIcon from './MaterialCommunityIcon'; import Surface from './Surface'; +import { useLocale } from '../core/locale'; import { useInternalTheme } from '../core/theming'; import type { MD3Theme, ThemeProp } from '../types'; import { forwardRef } from '../utils/forwardRef'; @@ -208,6 +208,7 @@ const Searchbar = forwardRef( ref ) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { colors, fonts } = theme as MD3Theme; const root = React.useRef(null); @@ -248,6 +249,7 @@ const Searchbar = forwardRef( }; const isBarMode = mode === 'bar'; + const inputTextAlign = direction === 'rtl' ? 'right' : 'left'; const shouldRenderTraileringIcon = isBarMode && traileringIcon && @@ -283,7 +285,7 @@ const Searchbar = forwardRef( name="magnify" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -298,6 +300,7 @@ const Searchbar = forwardRef( color: textColor, ...font, ...Platform.select({ web: { outline: 'none' } }), + textAlign: inputTextAlign, }, isBarMode ? styles.barModeInput : styles.viewModeInput, inputStyle, @@ -345,7 +348,7 @@ const Searchbar = forwardRef( name="close" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -396,7 +399,6 @@ const styles = StyleSheet.create({ fontSize: 18, paddingLeft: 8, alignSelf: 'stretch', - textAlign: I18nManager.getConstants().isRTL ? 'right' : 'left', minWidth: 0, }, barModeInput: { diff --git a/src/components/Snackbar.tsx b/src/components/Snackbar.tsx index 22b55bb02a..b77ce7a254 100644 --- a/src/components/Snackbar.tsx +++ b/src/components/Snackbar.tsx @@ -3,7 +3,6 @@ import { Animated, ColorValue, Easing, - I18nManager, StyleProp, StyleSheet, View, @@ -19,6 +18,7 @@ import IconButton from './IconButton/IconButton'; import MaterialCommunityIcon from './MaterialCommunityIcon'; import Surface from './Surface'; import Text from './Typography/Text'; +import { useLocale } from '../core/locale'; import { useInternalTheme } from '../core/theming'; import type { $Omit, $RemoveChildren, MD3Theme, ThemeProp } from '../types'; @@ -166,6 +166,7 @@ const Snackbar = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { bottom, right, left } = useSafeAreaInsets(); const { current: opacity } = React.useRef( @@ -360,9 +361,7 @@ const Snackbar = ({ name="close" color={color} size={size} - direction={ - I18nManager.getConstants().isRTL ? 'rtl' : 'ltr' - } + direction={direction} /> ); }) diff --git a/src/components/TextInput/TextInputFlat.tsx b/src/components/TextInput/TextInputFlat.tsx index ee8e438fb4..ca63338af0 100644 --- a/src/components/TextInput/TextInputFlat.tsx +++ b/src/components/TextInput/TextInputFlat.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { - I18nManager, Platform, StyleSheet, TextInput as NativeTextInput, @@ -41,6 +40,7 @@ import { } from './helpers'; import InputLabel from './Label/InputLabel'; import type { ChildTextInputProps, RenderProps } from './types'; +import { useLocale } from '../../core/locale'; const TextInputFlat = ({ disabled = false, @@ -78,6 +78,8 @@ const TextInputFlat = ({ ...rest }: ChildTextInputProps) => { const isAndroid = Platform.OS === 'android'; + const { direction } = useLocale(); + const isRTL = direction === 'rtl'; const { colors, roundness } = theme; const font = theme.fonts.bodyLarge; const hasActiveOutline = parentState.focused || error; @@ -168,11 +170,8 @@ const TextInputFlat = ({ const labelHalfHeight = labelHeight / 2; const baseLabelTranslateX = - (I18nManager.getConstants().isRTL ? 1 : -1) * - (labelHalfWidth - (labelScale * labelWidth) / 2) + - (1 - labelScale) * - (I18nManager.getConstants().isRTL ? -1 : 1) * - paddingLeft; + (isRTL ? 1 : -1) * (labelHalfWidth - (labelScale * labelWidth) / 2) + + (1 - labelScale) * (isRTL ? -1 : 1) * paddingLeft; const minInputHeight = dense ? (label ? MIN_DENSE_HEIGHT_WL : MIN_DENSE_HEIGHT) - LABEL_PADDING_TOP_DENSE @@ -276,13 +275,9 @@ const TextInputFlat = ({ labelScale, wiggleOffsetX: LABEL_WIGGLE_X_OFFSET, topPosition, - paddingLeft: isAndroid - ? I18nManager.isRTL - ? paddingRight - : paddingLeft - : paddingLeft, + paddingLeft: isAndroid ? (isRTL ? paddingRight : paddingLeft) : paddingLeft, paddingRight: isAndroid - ? I18nManager.isRTL + ? isRTL ? paddingLeft : paddingRight : paddingRight, @@ -415,11 +410,7 @@ const TextInputFlat = ({ fontWeight, color: inputTextColor, textAlignVertical: multiline ? 'top' : 'center', - textAlign: textAlign - ? textAlign - : I18nManager.getConstants().isRTL - ? 'right' - : 'left', + textAlign: textAlign ? textAlign : isRTL ? 'right' : 'left', minWidth: Math.min( parentState.labelTextLayout.width + 2 * FLAT_INPUT_OFFSET, MIN_WIDTH diff --git a/src/components/TextInput/TextInputOutlined.tsx b/src/components/TextInput/TextInputOutlined.tsx index 0bd85fb8d9..8e45855345 100644 --- a/src/components/TextInput/TextInputOutlined.tsx +++ b/src/components/TextInput/TextInputOutlined.tsx @@ -4,7 +4,6 @@ import { View, TextInput as NativeTextInput, StyleSheet, - I18nManager, Platform, TextStyle, ColorValue, @@ -41,6 +40,7 @@ import { import InputLabel from './Label/InputLabel'; import LabelBackground from './Label/LabelBackground'; import type { RenderProps, ChildTextInputProps } from './types'; +import { useLocale } from '../../core/locale'; const TextInputOutlined = ({ disabled = false, @@ -80,6 +80,8 @@ const TextInputOutlined = ({ ...rest }: ChildTextInputProps) => { const adornmentConfig = getAdornmentConfig({ left, right }); + const { direction } = useLocale(); + const isRTL = direction === 'rtl'; const { colors, roundness } = theme; const font = theme.fonts.bodyLarge; @@ -131,7 +133,7 @@ const TextInputOutlined = ({ const labelHalfHeight = labelHeight / 2; const baseLabelTranslateX = - (I18nManager.getConstants().isRTL ? 1 : -1) * + (isRTL ? 1 : -1) * (labelHalfWidth - (labelScale * labelWidth) / 2 - (fontSize - MINIMIZED_LABEL_FONT_SIZE) * labelScale); @@ -148,8 +150,7 @@ const TextInputOutlined = ({ if (isAdornmentLeftIcon) { labelTranslationXOffset = - (I18nManager.getConstants().isRTL ? -1 : 1) * ADORNMENT_SIZE + - ADORNMENT_OFFSET; + (isRTL ? -1 : 1) * ADORNMENT_SIZE + ADORNMENT_OFFSET; } const minInputHeight = @@ -400,11 +401,7 @@ const TextInputOutlined = ({ fontWeight, color: inputTextColor, textAlignVertical: multiline ? 'top' : 'center', - textAlign: textAlign - ? textAlign - : I18nManager.getConstants().isRTL - ? 'right' - : 'left', + textAlign: textAlign ? textAlign : isRTL ? 'right' : 'left', paddingHorizontal: INPUT_PADDING_HORIZONTAL, minWidth: Math.min( parentState.labelTextLayout.width + diff --git a/src/components/Typography/AnimatedText.tsx b/src/components/Typography/AnimatedText.tsx index 19872e2d93..5da34b474d 100644 --- a/src/components/Typography/AnimatedText.tsx +++ b/src/components/Typography/AnimatedText.tsx @@ -1,15 +1,9 @@ import * as React from 'react'; import { ReactNode } from 'react'; -import { - Animated, - I18nManager, - StyleProp, - StyleSheet, - TextStyle, - Text, -} from 'react-native'; +import { Animated, StyleProp, StyleSheet, TextStyle, Text } from 'react-native'; import type { VariantProp } from './types'; +import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import { forwardRef } from '../../utils/forwardRef'; @@ -48,7 +42,7 @@ const AnimatedText = forwardRef>( ref ) { const theme = useInternalTheme(themeOverrides); - const writingDirection = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'; + const { direction: writingDirection } = useLocale(); if (variant) { const font = theme.fonts[variant]; diff --git a/src/components/Typography/Text.tsx b/src/components/Typography/Text.tsx index c80d4399e4..4ed4ddd529 100644 --- a/src/components/Typography/Text.tsx +++ b/src/components/Typography/Text.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { ReactNode } from 'react'; import { - I18nManager, StyleProp, StyleSheet, Text as NativeText, @@ -10,6 +9,7 @@ import { import AnimatedText from './AnimatedText'; import type { VariantProp } from './types'; +import { useLocale } from '../../core/locale'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import { forwardRef } from '../../utils/forwardRef'; @@ -87,7 +87,7 @@ const Text = ( const root = React.useRef(null); // FIXME: destructure it in TS 4.6+ const theme = useInternalTheme(initialTheme); - const writingDirection = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'; + const { direction: writingDirection } = useLocale(); React.useImperativeHandle(ref, () => ({ setNativeProps: (args: Object) => root.current?.setNativeProps(args), diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 14afac63f3..c02cd17692 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -228,7 +228,6 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -237,6 +236,7 @@ exports[`Appbar does not pass any additional props to Searchbar 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index c92ed138c4..4dc7e0519e 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -1,10 +1,11 @@ /* eslint-disable react-native/no-inline-styles */ import * as React from 'react'; -import { I18nManager, Platform, StyleSheet, Text, View } from 'react-native'; +import { Platform, StyleSheet, Text, View } from 'react-native'; import { fireEvent, render } from '@testing-library/react-native'; import color from 'color'; +import PaperProvider from '../../core/PaperProvider'; import { DefaultTheme, getTheme, ThemeProvider } from '../../core/theming'; import { red500 } from '../../styles/themes/v2/colors'; import { @@ -257,28 +258,27 @@ it('renders input placeholder initially with transparent placeholderTextColor', it('correctly applies padding offset to input label on Android when RTL', () => { Platform.OS = 'android'; - I18nManager.isRTL = true; const { getByTestId } = render( - - } - right={ - - } - /> + + + } + right={ + + } + /> + ); expect(getByTestId('text-input-flat-label-active')).toHaveStyle({ paddingLeft: 56, paddingRight: 16, }); - - I18nManager.isRTL = false; }); it('correctly applies padding offset to input label on Android when LTR', () => { diff --git a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap index f30e19f3f8..c3144e9a5a 100644 --- a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap @@ -187,7 +187,6 @@ exports[`activity indicator snapshot test 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -196,6 +195,7 @@ exports[`activity indicator snapshot test 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, @@ -597,7 +597,6 @@ exports[`renders with placeholder 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -606,6 +605,7 @@ exports[`renders with placeholder 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, @@ -958,7 +958,6 @@ exports[`renders with text 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -967,6 +966,7 @@ exports[`renders with text 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, diff --git a/src/core/PaperProvider.tsx b/src/core/PaperProvider.tsx index 01a80fdcd9..83d48f861c 100644 --- a/src/core/PaperProvider.tsx +++ b/src/core/PaperProvider.tsx @@ -6,6 +6,7 @@ import { NativeEventSubscription, } from 'react-native'; +import { getDefaultDirection, LocaleProvider, type Direction } from './locale'; import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import { Provider as SettingsProvider, Settings } from './settings'; import { defaultThemes, ThemeProvider } from './theming'; @@ -18,12 +19,15 @@ export type Props = { children: React.ReactNode; theme?: ThemeProp; settings?: Settings; + direction?: Direction; }; const PaperProvider = (props: Props) => { const colorSchemeName = (!props.theme && Appearance?.getColorScheme()) || 'light'; + const direction = props.direction ?? getDefaultDirection(); + const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(false); const [colorScheme, setColorScheme] = @@ -101,7 +105,9 @@ const PaperProvider = (props: Props) => { - {children} + + {children} + diff --git a/src/core/locale.tsx b/src/core/locale.tsx new file mode 100644 index 0000000000..4380b1cf5f --- /dev/null +++ b/src/core/locale.tsx @@ -0,0 +1,23 @@ +import * as React from 'react'; +import { I18nManager } from 'react-native'; + +/** + * Writing direction of the app. Defaults to the value from `I18nManager`. + * Use this to override RTL/LTR on platforms where `I18nManager` is a no-op (e.g. React Native Web). + */ +export type Direction = 'ltr' | 'rtl'; + +export type LocaleContextValue = { + direction: Direction; +}; + +export const getDefaultDirection = (): Direction => + I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr'; + +export const LocaleContext = React.createContext({ + direction: getDefaultDirection(), +}); + +export const { Provider: LocaleProvider } = LocaleContext; + +export const useLocale = () => React.useContext(LocaleContext); diff --git a/src/index.tsx b/src/index.tsx index ec0c4d4786..095b3eb202 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,6 +8,8 @@ export { adaptNavigationTheme, } from './core/theming'; +export { useLocale } from './core/locale'; + export * from './styles/themes'; export { default as Provider } from './core/PaperProvider'; diff --git a/src/react-navigation/views/MaterialBottomTabView.tsx b/src/react-navigation/views/MaterialBottomTabView.tsx index 98378e0c79..07d3644d16 100644 --- a/src/react-navigation/views/MaterialBottomTabView.tsx +++ b/src/react-navigation/views/MaterialBottomTabView.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { I18nManager, Platform, StyleSheet } from 'react-native'; +import { Platform, StyleSheet } from 'react-native'; import { CommonActions, @@ -12,6 +12,7 @@ import { import BottomNavigation from '../../components/BottomNavigation/BottomNavigation'; import MaterialCommunityIcon from '../../components/MaterialCommunityIcon'; +import { useLocale } from '../../core/locale'; import type { MaterialBottomTabDescriptorMap, MaterialBottomTabNavigationConfig, @@ -30,6 +31,7 @@ export default function MaterialBottomTabView({ ...rest }: Props) { const buildLink = useLinkBuilder(); + const { direction } = useLocale(); return ( Date: Thu, 7 May 2026 13:19:54 +0300 Subject: [PATCH 2/8] fix: memoize locale context value in PaperProvider --- src/core/PaperProvider.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/PaperProvider.tsx b/src/core/PaperProvider.tsx index 83d48f861c..7cd2b832e9 100644 --- a/src/core/PaperProvider.tsx +++ b/src/core/PaperProvider.tsx @@ -92,6 +92,8 @@ const PaperProvider = (props: Props) => { const { children, settings } = props; + const localeValue = React.useMemo(() => ({ direction }), [direction]); + const settingsValue = React.useMemo( () => ({ icon: MaterialCommunityIcon, @@ -105,7 +107,7 @@ const PaperProvider = (props: Props) => { - + {children} From 6c03baa0fd552f9c56183e2b606194a18e79c9a3 Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Fri, 8 May 2026 17:35:21 +0300 Subject: [PATCH 3/8] docs: add RTL guide and improve locale API --- docs/docs/guides/10-rtl.md | 72 ++++++++++++++++++++++++++++++++ src/components/Portal/Portal.tsx | 29 ++++++++----- src/core/PaperProvider.tsx | 6 +-- src/core/locale.tsx | 33 +++++++++++---- src/index.tsx | 2 +- 5 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 docs/docs/guides/10-rtl.md diff --git a/docs/docs/guides/10-rtl.md b/docs/docs/guides/10-rtl.md new file mode 100644 index 0000000000..bfef7137c9 --- /dev/null +++ b/docs/docs/guides/10-rtl.md @@ -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 ( + + + + ); +} +``` + +## 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 ( + + {/* Components here will use RTL layout */} + + ); +} +``` + +## 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 +} +``` diff --git a/src/components/Portal/Portal.tsx b/src/components/Portal/Portal.tsx index d44caf96d6..39aedc7d08 100644 --- a/src/components/Portal/Portal.tsx +++ b/src/components/Portal/Portal.tsx @@ -4,6 +4,7 @@ import type { InternalTheme } from 'src/types'; import PortalConsumer from './PortalConsumer'; import PortalHost, { PortalContext, PortalMethods } from './PortalHost'; +import { LocaleContext, LocaleProvider } from '../../core/locale'; import { Consumer as SettingsConsumer, Provider as SettingsProvider, @@ -49,19 +50,25 @@ class Portal extends React.Component { const { children, theme } = this.props; return ( - - {(settings) => ( - - {(manager) => ( - - - {children} - - + + {(locale) => ( + + {(settings) => ( + + {(manager) => ( + + + + {children} + + + + )} + )} - + )} - + ); } } diff --git a/src/core/PaperProvider.tsx b/src/core/PaperProvider.tsx index 7cd2b832e9..cdb2d5d8a9 100644 --- a/src/core/PaperProvider.tsx +++ b/src/core/PaperProvider.tsx @@ -26,8 +26,6 @@ const PaperProvider = (props: Props) => { const colorSchemeName = (!props.theme && Appearance?.getColorScheme()) || 'light'; - const direction = props.direction ?? getDefaultDirection(); - const [reduceMotionEnabled, setReduceMotionEnabled] = React.useState(false); const [colorScheme, setColorScheme] = @@ -92,7 +90,7 @@ const PaperProvider = (props: Props) => { const { children, settings } = props; - const localeValue = React.useMemo(() => ({ direction }), [direction]); + const direction = props.direction ?? getDefaultDirection(); const settingsValue = React.useMemo( () => ({ @@ -107,7 +105,7 @@ const PaperProvider = (props: Props) => { - + {children} diff --git a/src/core/locale.tsx b/src/core/locale.tsx index 4380b1cf5f..d22e17b3e3 100644 --- a/src/core/locale.tsx +++ b/src/core/locale.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { I18nManager } from 'react-native'; /** - * Writing direction of the app. Defaults to the value from `I18nManager`. + * Writing direction of the app. * Use this to override RTL/LTR on platforms where `I18nManager` is a no-op (e.g. React Native Web). */ export type Direction = 'ltr' | 'rtl'; @@ -11,13 +11,32 @@ export type LocaleContextValue = { direction: Direction; }; -export const getDefaultDirection = (): Direction => - I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr'; - export const LocaleContext = React.createContext({ - direction: getDefaultDirection(), + direction: I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr', }); -export const { Provider: LocaleProvider } = LocaleContext; +export type LocaleProviderProps = { + direction: Direction; + children: React.ReactNode; +}; -export const useLocale = () => React.useContext(LocaleContext); +/** + * Provider component for locale configuration. + */ +export function LocaleProvider({ direction, children }: LocaleProviderProps) { + const value = React.useMemo(() => ({ direction }), [direction]); + return ( + {children} + ); +} + +/** + * Returns the locale context value. Must be used inside a `PaperProvider` (or `LocaleProvider`). + * Falls back to the system direction from `I18nManager` when used outside a provider. + */ +export function useLocale(): LocaleContextValue { + return React.useContext(LocaleContext); +} + +export const getDefaultDirection = (): Direction => + I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr'; diff --git a/src/index.tsx b/src/index.tsx index 095b3eb202..761df47a23 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,7 +8,7 @@ export { adaptNavigationTheme, } from './core/theming'; -export { useLocale } from './core/locale'; +export { useLocale, LocaleProvider } from './core/locale'; export * from './styles/themes'; From d1934c84f2a60da06c64e59a392610448697fa80 Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Mon, 11 May 2026 14:37:37 +0300 Subject: [PATCH 4/8] fix: null default for locale and wrap test renders --- .../__tests__/ActivityIndicator.test.tsx | 3 +-- src/components/__tests__/AnimatedFAB.test.tsx | 3 ++- .../__tests__/Appbar/Appbar.test.tsx | 3 ++- src/components/__tests__/Avatar.test.tsx | 3 ++- src/components/__tests__/Badge.test.tsx | 3 +-- src/components/__tests__/Banner.test.tsx | 3 ++- .../__tests__/BottomNavigation.test.tsx | 3 ++- src/components/__tests__/Button.test.tsx | 3 ++- src/components/__tests__/Card/Card.test.tsx | 3 ++- .../__tests__/Checkbox/Checkbox.test.tsx | 3 +-- .../__tests__/Checkbox/CheckboxItem.test.tsx | 3 ++- src/components/__tests__/Chip.test.tsx | 3 ++- src/components/__tests__/DataTable.test.tsx | 3 +-- src/components/__tests__/Dialog.test.tsx | 3 ++- .../Drawer/DrawerCollapsedItem.test.tsx | 3 +-- .../__tests__/Drawer/DrawerSection.test.tsx | 3 +-- src/components/__tests__/DrawerItem.test.tsx | 3 +-- src/components/__tests__/FAB.test.tsx | 3 ++- src/components/__tests__/FABGroup.test.tsx | 3 ++- src/components/__tests__/HelperText.test.tsx | 3 +-- src/components/__tests__/Icon.test.tsx | 3 +-- src/components/__tests__/IconButton.test.tsx | 3 ++- .../__tests__/ListAccordion.test.tsx | 3 +-- src/components/__tests__/ListImage.test.tsx | 5 +--- src/components/__tests__/ListItem.test.tsx | 3 ++- src/components/__tests__/ListSection.test.tsx | 3 +-- src/components/__tests__/Menu.test.tsx | 3 ++- src/components/__tests__/MenuItem.test.tsx | 3 +-- src/components/__tests__/Modal.test.tsx | 3 ++- src/components/__tests__/Portal.test.tsx | 3 ++- src/components/__tests__/ProgressBar.test.tsx | 5 ++-- .../RadioButton/RadioButton.test.tsx | 3 +-- .../RadioButton/RadioButtonGroup.test.tsx | 3 +-- .../RadioButton/RadioButtonItem.test.tsx | 3 ++- src/components/__tests__/Searchbar.test.tsx | 3 ++- .../__tests__/SegmentedButton.test.tsx | 3 +-- src/components/__tests__/Snackbar.test.tsx | 3 ++- src/components/__tests__/Surface.test.tsx | 3 +-- src/components/__tests__/Switch.test.tsx | 2 +- src/components/__tests__/TextInput.test.tsx | 3 ++- .../__tests__/ToggleButton.test.tsx | 3 ++- src/components/__tests__/Tooltip.test.tsx | 3 ++- .../__tests__/TouchableRipple.test.tsx | 3 ++- .../__tests__/Typography/Text.test.tsx | 3 +-- src/core/locale.tsx | 13 ++++++---- src/react-navigation/__tests__/index.test.tsx | 3 ++- src/test-utils.tsx | 24 +++++++++++++++++++ tsconfig.json | 3 ++- 48 files changed, 106 insertions(+), 72 deletions(-) create mode 100644 src/test-utils.tsx diff --git a/src/components/__tests__/ActivityIndicator.test.tsx b/src/components/__tests__/ActivityIndicator.test.tsx index f787fe9967..1b276f9234 100644 --- a/src/components/__tests__/ActivityIndicator.test.tsx +++ b/src/components/__tests__/ActivityIndicator.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import ActivityIndicator from '../ActivityIndicator'; it('renders indicator', () => { diff --git a/src/components/__tests__/AnimatedFAB.test.tsx b/src/components/__tests__/AnimatedFAB.test.tsx index a31f6d5fb4..534f51128f 100644 --- a/src/components/__tests__/AnimatedFAB.test.tsx +++ b/src/components/__tests__/AnimatedFAB.test.tsx @@ -3,10 +3,11 @@ import * as React from 'react'; import { Animated, StyleSheet } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import { act } from 'react-test-renderer'; import { MD3Colors } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import AnimatedFAB from '../FAB/AnimatedFAB'; const styles = StyleSheet.create({ diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index e1c55fa8c8..6a0bba8c72 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { Animated } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import mockSafeAreaContext from 'react-native-safe-area-context/jest/mock'; import { getTheme } from '../../../core/theming'; import { tokens } from '../../../styles/themes/v3/tokens'; +import { render } from '../../../test-utils'; import Appbar from '../../Appbar'; import { getAppbarBackgroundColor, diff --git a/src/components/__tests__/Avatar.test.tsx b/src/components/__tests__/Avatar.test.tsx index 96d69582b6..25e6b871e5 100644 --- a/src/components/__tests__/Avatar.test.tsx +++ b/src/components/__tests__/Avatar.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import { red500 } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import * as Avatar from '../Avatar/Avatar'; const styles = StyleSheet.create({ diff --git a/src/components/__tests__/Badge.test.tsx b/src/components/__tests__/Badge.test.tsx index 32730659a1..925b47b408 100644 --- a/src/components/__tests__/Badge.test.tsx +++ b/src/components/__tests__/Badge.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import { red500 } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import Badge from '../Badge'; it('renders badge', () => { diff --git a/src/components/__tests__/Banner.test.tsx b/src/components/__tests__/Banner.test.tsx index 048bfa7275..8066555354 100644 --- a/src/components/__tests__/Banner.test.tsx +++ b/src/components/__tests__/Banner.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Animated, Image } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; +import { render } from '../../test-utils'; import Banner from '../Banner'; it('renders hidden banner, without action buttons and without image', () => { diff --git a/src/components/__tests__/BottomNavigation.test.tsx b/src/components/__tests__/BottomNavigation.test.tsx index f5bdeced00..a72b46d8f3 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { Animated, Easing, Platform, StyleSheet } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; import { MD3Colors } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import BottomNavigation from '../BottomNavigation/BottomNavigation'; import BottomNavigationRouteScreen from '../BottomNavigation/BottomNavigationRouteScreen'; import { diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx index b47be8c53e..819fd1d711 100644 --- a/src/components/__tests__/Button.test.tsx +++ b/src/components/__tests__/Button.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { Animated, StyleSheet } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; import { pink500, white } from '../../styles/themes/v2/colors'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import Button from '../Button/Button'; import { getButtonColors } from '../Button/utils'; diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx index a7dc736c62..04f9d1cd10 100644 --- a/src/components/__tests__/Card/Card.test.tsx +++ b/src/components/__tests__/Card/Card.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { Animated, StyleSheet, Text } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import { getTheme } from '../../../core/theming'; import { MD3Colors } from '../../../styles/themes/v3/tokens'; +import { render } from '../../../test-utils'; import Button from '../../Button/Button'; import Card from '../../Card/Card'; import { getCardColors, getCardCoverStyle } from '../../Card/utils'; diff --git a/src/components/__tests__/Checkbox/Checkbox.test.tsx b/src/components/__tests__/Checkbox/Checkbox.test.tsx index efa819007e..1a4d618387 100644 --- a/src/components/__tests__/Checkbox/Checkbox.test.tsx +++ b/src/components/__tests__/Checkbox/Checkbox.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../../test-utils'; import Checkbox from '../../Checkbox'; it('renders checked Checkbox with onPress', () => { diff --git a/src/components/__tests__/Checkbox/CheckboxItem.test.tsx b/src/components/__tests__/Checkbox/CheckboxItem.test.tsx index cfb6007b5c..28546d16e5 100644 --- a/src/components/__tests__/Checkbox/CheckboxItem.test.tsx +++ b/src/components/__tests__/Checkbox/CheckboxItem.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Platform } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; +import { render } from '../../../test-utils'; import Checkbox from '../../Checkbox'; it('renders unchecked', () => { diff --git a/src/components/__tests__/Chip.test.tsx b/src/components/__tests__/Chip.test.tsx index 76044715b2..359e52541a 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { Animated } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import color from 'color'; import { getTheme } from '../../core/theming'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import Chip from '../Chip/Chip'; import { getChipColors } from '../Chip/helpers'; diff --git a/src/components/__tests__/DataTable.test.tsx b/src/components/__tests__/DataTable.test.tsx index ae212d281f..01c70031ec 100644 --- a/src/components/__tests__/DataTable.test.tsx +++ b/src/components/__tests__/DataTable.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import Checkbox from '../Checkbox'; import DataTable from '../DataTable/DataTable'; diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index 8f7e327a72..742f44b941 100644 --- a/src/components/__tests__/Dialog.test.tsx +++ b/src/components/__tests__/Dialog.test.tsx @@ -7,9 +7,10 @@ import { BackHandlerStatic as RNBackHandlerStatic, } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import Dialog from '../../components/Dialog/Dialog'; +import { render } from '../../test-utils'; import Button from '../Button/Button'; interface BackHandlerStatic extends RNBackHandlerStatic { diff --git a/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx b/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx index bc3b32885a..18908acc8a 100644 --- a/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx +++ b/src/components/__tests__/Drawer/DrawerCollapsedItem.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../../test-utils'; import DrawerCollapsedItem from '../../Drawer/DrawerCollapsedItem'; describe('DrawerCollapsedItem', () => { diff --git a/src/components/__tests__/Drawer/DrawerSection.test.tsx b/src/components/__tests__/Drawer/DrawerSection.test.tsx index e74cf362c6..feb8c2aad7 100644 --- a/src/components/__tests__/Drawer/DrawerSection.test.tsx +++ b/src/components/__tests__/Drawer/DrawerSection.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { View } from 'react-native'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../../test-utils'; import DrawerSection from '../../Drawer/DrawerSection'; describe('DrawerSection', () => { diff --git a/src/components/__tests__/DrawerItem.test.tsx b/src/components/__tests__/DrawerItem.test.tsx index ec4a36efcb..a12478d51c 100644 --- a/src/components/__tests__/DrawerItem.test.tsx +++ b/src/components/__tests__/DrawerItem.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import DrawerItem from '../Drawer/DrawerItem'; it('renders basic DrawerItem', () => { diff --git a/src/components/__tests__/FAB.test.tsx b/src/components/__tests__/FAB.test.tsx index 9fc9c12e98..42c0efadf7 100644 --- a/src/components/__tests__/FAB.test.tsx +++ b/src/components/__tests__/FAB.test.tsx @@ -1,10 +1,11 @@ import * as React from 'react'; import { Animated, StyleSheet } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import { act } from 'react-test-renderer'; import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import FAB from '../FAB'; import { getFABColors } from '../FAB/utils'; diff --git a/src/components/__tests__/FABGroup.test.tsx b/src/components/__tests__/FABGroup.test.tsx index 5f25e46c6f..f90e534910 100644 --- a/src/components/__tests__/FABGroup.test.tsx +++ b/src/components/__tests__/FABGroup.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { Animated } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import FAB from '../FAB'; import { getFABGroupColors } from '../FAB/utils'; diff --git a/src/components/__tests__/HelperText.test.tsx b/src/components/__tests__/HelperText.test.tsx index ce16526776..22ffc025b9 100644 --- a/src/components/__tests__/HelperText.test.tsx +++ b/src/components/__tests__/HelperText.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import HelperText from '../HelperText/HelperText'; describe('HelperText', () => { diff --git a/src/components/__tests__/Icon.test.tsx b/src/components/__tests__/Icon.test.tsx index 29944da5ac..3c1d55af19 100644 --- a/src/components/__tests__/Icon.test.tsx +++ b/src/components/__tests__/Icon.test.tsx @@ -1,8 +1,7 @@ import React from 'react'; import { Image } from 'react-native'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import Icon from '../Icon'; const ICON_SIZE = 24; diff --git a/src/components/__tests__/IconButton.test.tsx b/src/components/__tests__/IconButton.test.tsx index fdd3316850..9f32c50c99 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -1,11 +1,12 @@ import * as React from 'react'; import { Animated, StyleSheet } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; import { pink500 } from '../../styles/themes/v2/colors'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import IconButton from '../IconButton/IconButton'; import { getIconButtonColor } from '../IconButton/utils'; diff --git a/src/components/__tests__/ListAccordion.test.tsx b/src/components/__tests__/ListAccordion.test.tsx index 65a6a9d4de..de3994d01d 100644 --- a/src/components/__tests__/ListAccordion.test.tsx +++ b/src/components/__tests__/ListAccordion.test.tsx @@ -1,10 +1,9 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; import { red500 } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import ListAccordion from '../List/ListAccordion'; import ListAccordionGroup from '../List/ListAccordionGroup'; import ListIcon from '../List/ListIcon'; diff --git a/src/components/__tests__/ListImage.test.tsx b/src/components/__tests__/ListImage.test.tsx index 5248019277..2277eb324a 100644 --- a/src/components/__tests__/ListImage.test.tsx +++ b/src/components/__tests__/ListImage.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import ListImage from '../List/ListImage'; const styles = StyleSheet.create({ @@ -52,7 +51,6 @@ it('renders ListImage with `image` variant', () => { /> ); - expect(tree.container.props['variant']).toBe('image'); expect(tree.getByTestId(testID)).toHaveStyle(styles.image); }); @@ -64,6 +62,5 @@ it('renders ListImage with `video` variant', () => { /> ); - expect(tree.container.props['variant']).toBe('video'); expect(tree.getByTestId(testID)).toHaveStyle(styles.video); }); diff --git a/src/components/__tests__/ListItem.test.tsx b/src/components/__tests__/ListItem.test.tsx index 036697f9ec..fbd72183d9 100644 --- a/src/components/__tests__/ListItem.test.tsx +++ b/src/components/__tests__/ListItem.test.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import { Platform, StyleSheet } from 'react-native'; import { Text, View } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import { red500 } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import Chip from '../Chip/Chip'; import IconButton from '../IconButton/IconButton'; import ListIcon from '../List/ListIcon'; diff --git a/src/components/__tests__/ListSection.test.tsx b/src/components/__tests__/ListSection.test.tsx index 8cc1578b8f..fb33c6afad 100644 --- a/src/components/__tests__/ListSection.test.tsx +++ b/src/components/__tests__/ListSection.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; -import { render } from '@testing-library/react-native'; - import { red500 } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import ListIcon from '../List/ListIcon'; import ListItem from '../List/ListItem'; import ListSection from '../List/ListSection'; diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index 21ed4d0802..6ef5f73702 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { Animated, Dimensions, StyleSheet, View } from 'react-native'; -import { act, render, screen, waitFor } from '@testing-library/react-native'; +import { act, screen, waitFor } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import { MD3Elevation } from '../../types'; import Button from '../Button/Button'; import Menu, { ELEVATION_LEVELS_MAP } from '../Menu/Menu'; diff --git a/src/components/__tests__/MenuItem.test.tsx b/src/components/__tests__/MenuItem.test.tsx index dead127da1..aff2d500dd 100644 --- a/src/components/__tests__/MenuItem.test.tsx +++ b/src/components/__tests__/MenuItem.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import Menu from '../Menu/Menu'; import { getMenuItemColor } from '../Menu/utils'; diff --git a/src/components/__tests__/Modal.test.tsx b/src/components/__tests__/Modal.test.tsx index d040cc62de..b750d578c1 100644 --- a/src/components/__tests__/Modal.test.tsx +++ b/src/components/__tests__/Modal.test.tsx @@ -6,10 +6,11 @@ import { Text, } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import { MD3LightTheme } from '../../styles/themes'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import Modal from '../Modal'; const { scrimAlpha } = tokens.md.ref; diff --git a/src/components/__tests__/Portal.test.tsx b/src/components/__tests__/Portal.test.tsx index ed87dce678..c92f74f8ed 100644 --- a/src/components/__tests__/Portal.test.tsx +++ b/src/components/__tests__/Portal.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Text } from 'react-native'; -import { render, waitFor } from '@testing-library/react-native'; +import { waitFor } from '@testing-library/react-native'; +import { render } from '../../test-utils'; import Portal from '../Portal/Portal'; jest.useRealTimers(); diff --git a/src/components/__tests__/ProgressBar.test.tsx b/src/components/__tests__/ProgressBar.test.tsx index 9aac20ac91..0a3445b601 100644 --- a/src/components/__tests__/ProgressBar.test.tsx +++ b/src/components/__tests__/ProgressBar.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Animated, Platform, StyleSheet } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; +import { render } from '../../test-utils'; import ProgressBar, { Props } from '../ProgressBar'; const layoutEvent = { @@ -46,7 +47,7 @@ it('renders progress bar with animated value', async () => { tree.update(); - expect(tree.container.props['animatedValue']).toBe(0.4); + expect(tree.getByRole(a11yRole)).toBeTruthy(); }); it('renders progress bar with specific progress', async () => { diff --git a/src/components/__tests__/RadioButton/RadioButton.test.tsx b/src/components/__tests__/RadioButton/RadioButton.test.tsx index ecba6bf851..89e24261d1 100644 --- a/src/components/__tests__/RadioButton/RadioButton.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButton.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../../test-utils'; import RadioButton from '../../RadioButton'; import { RadioButtonContext } from '../../RadioButton/RadioButtonGroup'; diff --git a/src/components/__tests__/RadioButton/RadioButtonGroup.test.tsx b/src/components/__tests__/RadioButton/RadioButtonGroup.test.tsx index 2b431102cb..ce1ae61c70 100644 --- a/src/components/__tests__/RadioButton/RadioButtonGroup.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButtonGroup.test.tsx @@ -1,7 +1,6 @@ import React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../../test-utils'; import RadioButton from '../../RadioButton'; describe('RadioButtonGroup', () => { diff --git a/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx b/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx index 84e7b52971..6afd21a48a 100644 --- a/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx +++ b/src/components/__tests__/RadioButton/RadioButtonItem.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Platform } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; +import { render } from '../../../test-utils'; import RadioButton from '../../RadioButton'; it('renders unchecked', () => { diff --git a/src/components/__tests__/Searchbar.test.tsx b/src/components/__tests__/Searchbar.test.tsx index eb44e85481..255225f7ae 100644 --- a/src/components/__tests__/Searchbar.test.tsx +++ b/src/components/__tests__/Searchbar.test.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; import { Animated } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; +import { render } from '../../test-utils'; import * as Avatar from '../Avatar/Avatar'; import Searchbar from '../Searchbar'; diff --git a/src/components/__tests__/SegmentedButton.test.tsx b/src/components/__tests__/SegmentedButton.test.tsx index 10385d5172..e462197ded 100644 --- a/src/components/__tests__/SegmentedButton.test.tsx +++ b/src/components/__tests__/SegmentedButton.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import SegmentedButtons from '../SegmentedButtons/SegmentedButtons'; import { getDisabledSegmentedButtonStyle, diff --git a/src/components/__tests__/Snackbar.test.tsx b/src/components/__tests__/Snackbar.test.tsx index e8f71f3319..c163f9f4c8 100644 --- a/src/components/__tests__/Snackbar.test.tsx +++ b/src/components/__tests__/Snackbar.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { Animated, StyleSheet, Text, View } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import { red200, white } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import Snackbar from '../Snackbar'; const styles = StyleSheet.create({ diff --git a/src/components/__tests__/Surface.test.tsx b/src/components/__tests__/Surface.test.tsx index 045a63a3e6..c58f173945 100644 --- a/src/components/__tests__/Surface.test.tsx +++ b/src/components/__tests__/Surface.test.tsx @@ -2,9 +2,8 @@ import * as React from 'react'; import { StyleSheet } from 'react-native'; import { Platform } from 'react-native'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import Surface from '../Surface'; describe('Surface', () => { diff --git a/src/components/__tests__/Switch.test.tsx b/src/components/__tests__/Switch.test.tsx index ee7a82a15d..169c550d1d 100644 --- a/src/components/__tests__/Switch.test.tsx +++ b/src/components/__tests__/Switch.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { Platform } from 'react-native'; -import { render } from '@testing-library/react-native'; import color from 'color'; import { getTheme } from '../../core/theming'; @@ -14,6 +13,7 @@ import { pink500, grey700, } from '../../styles/themes/v2/colors'; +import { render } from '../../test-utils'; import Switch from '../Switch/Switch'; import { getSwitchColor } from '../Switch/utils'; diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index 2e4c1f0703..77e108df0f 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -2,12 +2,13 @@ import * as React from 'react'; import { Platform, StyleSheet, Text, View } from 'react-native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import PaperProvider from '../../core/PaperProvider'; import { DefaultTheme, getTheme, ThemeProvider } from '../../core/theming'; import { red500 } from '../../styles/themes/v2/colors'; import { tokens } from '../../styles/themes/v3/tokens'; +import { render } from '../../test-utils'; import { getFlatInputColors, getOutlinedInputColors, diff --git a/src/components/__tests__/ToggleButton.test.tsx b/src/components/__tests__/ToggleButton.test.tsx index a830558681..b9af5d3b3c 100644 --- a/src/components/__tests__/ToggleButton.test.tsx +++ b/src/components/__tests__/ToggleButton.test.tsx @@ -1,9 +1,10 @@ import * as React from 'react'; import { Animated } from 'react-native'; -import { act, render } from '@testing-library/react-native'; +import { act } from '@testing-library/react-native'; import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import ToggleButton from '../ToggleButton'; import { getToggleButtonColor } from '../ToggleButton/utils'; diff --git a/src/components/__tests__/Tooltip.test.tsx b/src/components/__tests__/Tooltip.test.tsx index 075b534ba6..6c59eaa60b 100644 --- a/src/components/__tests__/Tooltip.test.tsx +++ b/src/components/__tests__/Tooltip.test.tsx @@ -1,10 +1,11 @@ import React, { RefObject } from 'react'; import { Dimensions, Text, View, Platform } from 'react-native'; -import { act, fireEvent, render } from '@testing-library/react-native'; +import { act, fireEvent } from '@testing-library/react-native'; import type { ReactTestInstance } from 'react-test-renderer'; import PaperProvider from '../../core/PaperProvider'; +import { render } from '../../test-utils'; import Tooltip from '../Tooltip/Tooltip'; const mockedRemoveEventListener = jest.fn(); diff --git a/src/components/__tests__/TouchableRipple.test.tsx b/src/components/__tests__/TouchableRipple.test.tsx index c578605b3a..89bebfa7fa 100644 --- a/src/components/__tests__/TouchableRipple.test.tsx +++ b/src/components/__tests__/TouchableRipple.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { Platform, Text } from 'react-native'; -import { render, fireEvent } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; +import { render } from '../../test-utils'; import TouchableRipple from '../TouchableRipple/TouchableRipple.native'; describe('TouchableRipple', () => { diff --git a/src/components/__tests__/Typography/Text.test.tsx b/src/components/__tests__/Typography/Text.test.tsx index 2560920322..6769fd0514 100644 --- a/src/components/__tests__/Typography/Text.test.tsx +++ b/src/components/__tests__/Typography/Text.test.tsx @@ -1,11 +1,10 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import PaperProvider from '../../../core/PaperProvider'; import configureFonts from '../../../styles/fonts'; import { MD3LightTheme } from '../../../styles/themes'; import { tokens } from '../../../styles/themes/v3/tokens'; +import { render } from '../../../test-utils'; import Text, { customText } from '../../Typography/Text'; const content = 'Something rendered as a child content'; diff --git a/src/core/locale.tsx b/src/core/locale.tsx index d22e17b3e3..11433f736f 100644 --- a/src/core/locale.tsx +++ b/src/core/locale.tsx @@ -11,9 +11,9 @@ export type LocaleContextValue = { direction: Direction; }; -export const LocaleContext = React.createContext({ - direction: I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr', -}); +export const LocaleContext = React.createContext( + null +); export type LocaleProviderProps = { direction: Direction; @@ -32,10 +32,13 @@ export function LocaleProvider({ direction, children }: LocaleProviderProps) { /** * Returns the locale context value. Must be used inside a `PaperProvider` (or `LocaleProvider`). - * Falls back to the system direction from `I18nManager` when used outside a provider. */ export function useLocale(): LocaleContextValue { - return React.useContext(LocaleContext); + const context = React.useContext(LocaleContext); + if (context === null) { + throw new Error('useLocale must be used within a LocaleProvider'); + } + return context; } export const getDefaultDirection = (): Direction => diff --git a/src/react-navigation/__tests__/index.test.tsx b/src/react-navigation/__tests__/index.test.tsx index a04495639d..ead428dcf3 100644 --- a/src/react-navigation/__tests__/index.test.tsx +++ b/src/react-navigation/__tests__/index.test.tsx @@ -2,9 +2,10 @@ import * as React from 'react'; import { Button, Text, View } from 'react-native'; import { NavigationContainer, ParamListBase } from '@react-navigation/native'; -import { fireEvent, render } from '@testing-library/react-native'; +import { fireEvent } from '@testing-library/react-native'; import PaperProvider from '../../core/PaperProvider'; +import { render } from '../../test-utils'; import { createMaterialBottomTabNavigator, MaterialBottomTabScreenProps, diff --git a/src/test-utils.tsx b/src/test-utils.tsx new file mode 100644 index 0000000000..9e1049e7ff --- /dev/null +++ b/src/test-utils.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import { render, type RenderOptions } from '@testing-library/react-native'; + +import MaterialCommunityIcon from './components/MaterialCommunityIcon'; +import { getDefaultDirection, LocaleProvider } from './core/locale'; +import { Provider as SettingsProvider } from './core/settings'; +import { defaultThemes, ThemeProvider } from './core/theming'; + +const Wrapper = ({ children }: { children: React.ReactNode }) => ( + + + {children} + + +); + +const customRender = (ui: React.ReactElement, options?: RenderOptions) => + render(ui, { wrapper: Wrapper, ...options }); + +export * from '@testing-library/react-native'; +export { customRender as render }; diff --git a/tsconfig.json b/tsconfig.json index c0b7c9cd69..d5e5ddd3e2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,8 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "esnext" + "target": "esnext", + "types": ["jest"] }, "exclude": [ "lib/**/*" From eddaaa663f766e49316ac978e4f27be37780e0ec Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Mon, 11 May 2026 14:58:48 +0300 Subject: [PATCH 5/8] fix: exclude test-utils from build and restore node types in tsconfig --- tsconfig.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index d5e5ddd3e2..77dc846303 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,9 +31,10 @@ "skipLibCheck": true, "strict": true, "target": "esnext", - "types": ["jest"] + "types": ["jest", "node"] }, "exclude": [ - "lib/**/*" + "lib/**/*", + "src/test-utils.tsx" ] } From fc1290ef8c6b1fd107d151c79a4a574747ddd8d3 Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Mon, 11 May 2026 15:05:10 +0300 Subject: [PATCH 6/8] fix: exclude test-utils from typescript build config --- tsconfig.build.json | 2 +- tsconfig.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 06de95f291..e7485e3e40 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig", - "exclude": ["example", "docs", "**/__tests__/*"] + "exclude": ["example", "docs", "**/__tests__/*", "src/test-utils.tsx"] } diff --git a/tsconfig.json b/tsconfig.json index 77dc846303..56d776d859 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -34,7 +34,6 @@ "types": ["jest", "node"] }, "exclude": [ - "lib/**/*", - "src/test-utils.tsx" + "lib/**/*" ] } From 3cb9b1cd1579521abfbc90368262e42061e4395a Mon Sep 17 00:00:00 2001 From: Hristo Totov <47380018+hristototov@users.noreply.github.com> Date: Tue, 12 May 2026 15:00:06 +0300 Subject: [PATCH 7/8] Update docs/docs/guides/10-rtl.md Co-authored-by: Satyajit Sahoo --- docs/docs/guides/10-rtl.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/docs/guides/10-rtl.md b/docs/docs/guides/10-rtl.md index bfef7137c9..f9f66e6fbd 100644 --- a/docs/docs/guides/10-rtl.md +++ b/docs/docs/guides/10-rtl.md @@ -8,9 +8,17 @@ React Native Paper supports right-to-left (RTL) layouts for languages such as Ar ## 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`. +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. -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. +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. + +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: + + + + + +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. :::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. From c3620d6a6c415cac87652383aadf45e2e287a567 Mon Sep 17 00:00:00 2001 From: Hristo Totov Date: Tue, 12 May 2026 15:07:58 +0300 Subject: [PATCH 8/8] fix: wrap html snippet in code fence in RTL guide --- docs/docs/guides/10-rtl.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/docs/guides/10-rtl.md b/docs/docs/guides/10-rtl.md index f9f66e6fbd..796ec3595c 100644 --- a/docs/docs/guides/10-rtl.md +++ b/docs/docs/guides/10-rtl.md @@ -14,9 +14,11 @@ See [I18nManager](http://reactnative.dev/docs/i18nmanager) docs and [Enabling RT 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: +```html +``` 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.