diff --git a/docs/docs/guides/10-rtl.md b/docs/docs/guides/10-rtl.md new file mode 100644 index 0000000000..796ec3595c --- /dev/null +++ b/docs/docs/guides/10-rtl.md @@ -0,0 +1,82 @@ +--- +title: RTL Support +--- + +# RTL Support + +React Native Paper supports right-to-left (RTL) layouts for languages such as Arabic and Hebrew. + +## How it works + +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. + +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: + +```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. + +:::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/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 6a3fc52b3d..056bb432b9 100644 --- a/src/components/DataTable/DataTablePagination.tsx +++ b/src/components/DataTable/DataTablePagination.tsx @@ -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'; @@ -92,6 +87,7 @@ const PaginationControls = ({ theme: themeOverrides, }: PaginationControlsProps) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const textColor = theme.colors.onSurface; @@ -104,7 +100,7 @@ const PaginationControls = ({ name="page-first" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -120,7 +116,7 @@ const PaginationControls = ({ name="chevron-left" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -135,7 +131,7 @@ const PaginationControls = ({ name="chevron-right" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} iconColor={textColor} @@ -151,7 +147,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 3f911123b4..b31ec8b899 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, @@ -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'; @@ -91,6 +91,7 @@ const DataTableTitle = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { current: spinAnim } = React.useRef( new Animated.Value(sortDirection === 'ascending' ? 0 : 1) ); @@ -118,7 +119,7 @@ const DataTableTitle = ({ name="arrow-up" size={16} color={textColor} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> ) : null; @@ -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 diff --git a/src/components/FAB/AnimatedFAB.tsx b/src/components/FAB/AnimatedFAB.tsx index af3222a4a2..84b14a6a92 100644 --- a/src/components/FAB/AnimatedFAB.tsx +++ b/src/components/FAB/AnimatedFAB.tsx @@ -9,7 +9,6 @@ import { Animated, Easing, GestureResponderEvent, - I18nManager, Platform, ScrollView, StyleProp, @@ -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'; @@ -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(null); const { current: visibility } = React.useRef( new Animated.Value(visible ? 1 : 0) @@ -342,6 +343,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 93485aecb3..1d0dbb68e7 100644 --- a/src/components/FAB/utils.ts +++ b/src/components/FAB/utils.ts @@ -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; }; @@ -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 = { diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index e792e2670e..eee770f6b7 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 3eaee97d61..d79634d984 100644 --- a/src/components/List/ListAccordion.tsx +++ b/src/components/List/ListAccordion.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import { GestureResponderEvent, - I18nManager, NativeSyntheticEvent, StyleProp, StyleSheet, @@ -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'; @@ -198,6 +198,7 @@ const ListAccordion = ({ hitSlop, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const [expanded, setExpanded] = React.useState( expandedProp || false ); @@ -316,7 +317,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 ebbd94e057..1c56b7b574 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 { Elevation, Theme, ThemeProp } from '../../types'; import { addEventListener } from '../../utils/addEventListener'; @@ -200,6 +200,7 @@ const Menu = ({ keyboardShouldPersistTaps, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { colors: md3Colors } = theme as Theme; const insets = useSafeAreaInsets(); const [rendered, setRendered] = React.useState(visible); @@ -630,7 +631,7 @@ const Menu = ({ top: isCoordinate(anchor) ? topTransformation : topTransformation + additionalVerticalValue, - ...(I18nManager.getConstants().isRTL + ...(direction === 'rtl' ? { right: leftTransformation } : { left: leftTransformation }), }; 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/components/ProgressBar.tsx b/src/components/ProgressBar.tsx index eefed6e50d..bf1941490f 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 69fbfb572d..ba169fd9ba 100644 --- a/src/components/Searchbar.tsx +++ b/src/components/Searchbar.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Animated, GestureResponderEvent, - I18nManager, Platform, StyleProp, StyleSheet, @@ -19,6 +18,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 { cornerNone } from '../theme/tokens/sys/shape'; import type { Theme, ThemeProp } from '../types'; @@ -194,6 +194,7 @@ const Searchbar = forwardRef( ref ) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { colors, fonts } = theme as Theme; const root = React.useRef(null); @@ -229,6 +230,7 @@ const Searchbar = forwardRef( }; const isBarMode = mode === 'bar'; + const inputTextAlign = direction === 'rtl' ? 'right' : 'left'; const shouldRenderTraileringIcon = isBarMode && traileringIcon && @@ -265,7 +267,7 @@ const Searchbar = forwardRef( name="magnify" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -280,6 +282,7 @@ const Searchbar = forwardRef( color: textColor, ...font, ...Platform.select({ web: { outline: 'none' } }), + textAlign: inputTextAlign, }, isBarMode ? styles.barModeInput : styles.viewModeInput, inputStyle, @@ -326,7 +329,7 @@ const Searchbar = forwardRef( name="close" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -376,7 +379,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 c1f66bf402..f6eb590e84 100644 --- a/src/components/Snackbar.tsx +++ b/src/components/Snackbar.tsx @@ -2,7 +2,6 @@ import * as React from 'react'; import { Animated, Easing, - I18nManager, StyleProp, StyleSheet, View, @@ -18,6 +17,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, Theme, ThemeProp } from '../types'; @@ -158,6 +158,7 @@ const Snackbar = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { bottom, right, left } = useSafeAreaInsets(); const { current: opacity } = React.useRef( @@ -349,9 +350,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 221d000a71..f7235ea5d2 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 } = theme; const roundness = theme.shapes.corner.extraSmall; const font = theme.fonts.bodyLarge; @@ -170,11 +172,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 @@ -278,13 +277,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, @@ -420,11 +415,7 @@ const TextInputFlat = ({ color: inputTextColor, opacity: disabledOpacity, 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 aa563cf55f..be4fae7487 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 } = theme; const roundness = theme.shapes.corner.extraSmall; @@ -133,7 +135,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); @@ -150,8 +152,7 @@ const TextInputOutlined = ({ if (isAdornmentLeftIcon) { labelTranslationXOffset = - (I18nManager.getConstants().isRTL ? -1 : 1) * ADORNMENT_SIZE + - ADORNMENT_OFFSET; + (isRTL ? -1 : 1) * ADORNMENT_SIZE + ADORNMENT_OFFSET; } const minInputHeight = @@ -404,11 +405,7 @@ const TextInputOutlined = ({ color: inputTextColor, opacity: disabledOpacity, 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__/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 4017f019fc..318f59c3b9 100644 --- a/src/components/__tests__/AnimatedFAB.test.tsx +++ b/src/components/__tests__/AnimatedFAB.test.tsx @@ -3,9 +3,10 @@ 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 { render } from '../../test-utils'; import { Palette } from '../../theme/tokens'; import AnimatedFAB from '../FAB/AnimatedFAB'; diff --git a/src/components/__tests__/Appbar/Appbar.test.tsx b/src/components/__tests__/Appbar/Appbar.test.tsx index 0419e18e57..69a6ad13e5 100644 --- a/src/components/__tests__/Appbar/Appbar.test.tsx +++ b/src/components/__tests__/Appbar/Appbar.test.tsx @@ -1,10 +1,11 @@ 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 { render } from '../../../test-utils'; import { tokens } from '../../../theme/tokens'; import Appbar from '../../Appbar'; import { diff --git a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap index 82c623d863..e3efd16f62 100644 --- a/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap +++ b/src/components/__tests__/Appbar/__snapshots__/Appbar.test.tsx.snap @@ -236,7 +236,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)", @@ -245,6 +244,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__/Avatar.test.tsx b/src/components/__tests__/Avatar.test.tsx index d21100c0db..19842d435a 100644 --- a/src/components/__tests__/Avatar.test.tsx +++ b/src/components/__tests__/Avatar.test.tsx @@ -1,8 +1,9 @@ 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 { render } from '../../test-utils'; import { red500 } from '../../theme/colors'; import * as Avatar from '../Avatar/Avatar'; diff --git a/src/components/__tests__/Badge.test.tsx b/src/components/__tests__/Badge.test.tsx index 2beabe9eae..3f7e6c8fcc 100644 --- a/src/components/__tests__/Badge.test.tsx +++ b/src/components/__tests__/Badge.test.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - +import { render } from '../../test-utils'; import { red500 } from '../../theme/colors'; import Badge from '../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 496987e254..0ebc9458ed 100644 --- a/src/components/__tests__/BottomNavigation.test.tsx +++ b/src/components/__tests__/BottomNavigation.test.tsx @@ -1,9 +1,10 @@ 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 { render } from '../../test-utils'; import { Palette } from '../../theme/tokens'; import BottomNavigation from '../BottomNavigation/BottomNavigation'; import BottomNavigationRouteScreen from '../BottomNavigation/BottomNavigationRouteScreen'; diff --git a/src/components/__tests__/Button.test.tsx b/src/components/__tests__/Button.test.tsx index a203e1d237..71107089e7 100644 --- a/src/components/__tests__/Button.test.tsx +++ b/src/components/__tests__/Button.test.tsx @@ -1,9 +1,10 @@ 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 { render } from '../../test-utils'; import { pink500, white } from '../../theme/colors'; import { tokens } from '../../theme/tokens'; import Button from '../Button/Button'; diff --git a/src/components/__tests__/Card/Card.test.tsx b/src/components/__tests__/Card/Card.test.tsx index ac4eabfa11..b11e006445 100644 --- a/src/components/__tests__/Card/Card.test.tsx +++ b/src/components/__tests__/Card/Card.test.tsx @@ -1,9 +1,10 @@ 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 { render } from '../../../test-utils'; import { Palette } from '../../../theme/tokens'; import Button from '../../Button/Button'; import Card from '../../Card/Card'; 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 4ed5cad4ea..cf0f89ddba 100644 --- a/src/components/__tests__/Chip.test.tsx +++ b/src/components/__tests__/Chip.test.tsx @@ -1,10 +1,11 @@ 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 { render } from '../../test-utils'; import { tokens } from '../../theme/tokens'; 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 91b54e22be..e99c694885 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 229b8b462e..9702fe499c 100644 --- a/src/components/__tests__/IconButton.test.tsx +++ b/src/components/__tests__/IconButton.test.tsx @@ -1,9 +1,10 @@ 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 { render } from '../../test-utils'; import { pink500 } from '../../theme/colors'; import { tokens } from '../../theme/tokens'; import IconButton from '../IconButton/IconButton'; diff --git a/src/components/__tests__/ListAccordion.test.tsx b/src/components/__tests__/ListAccordion.test.tsx index 93e3c62bad..96b057e5b1 100644 --- a/src/components/__tests__/ListAccordion.test.tsx +++ b/src/components/__tests__/ListAccordion.test.tsx @@ -1,9 +1,8 @@ import * as React from 'react'; import { StyleSheet, View } from 'react-native'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import { red500 } from '../../theme/colors'; import ListAccordion from '../List/ListAccordion'; import ListAccordionGroup from '../List/ListAccordionGroup'; 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 c2b7f0d88d..e1c2f20357 100644 --- a/src/components/__tests__/ListItem.test.tsx +++ b/src/components/__tests__/ListItem.test.tsx @@ -2,8 +2,9 @@ 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 { render } from '../../test-utils'; import { red500 } from '../../theme/colors'; import Chip from '../Chip/Chip'; import IconButton from '../IconButton/IconButton'; diff --git a/src/components/__tests__/ListSection.test.tsx b/src/components/__tests__/ListSection.test.tsx index 91efbfc008..27a48f748a 100644 --- a/src/components/__tests__/ListSection.test.tsx +++ b/src/components/__tests__/ListSection.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 { red500 } from '../../theme/colors'; import ListIcon from '../List/ListIcon'; import ListItem from '../List/ListItem'; diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index 753124e478..5737f4d313 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 { Elevation } 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 98d433723d..8b525a5844 100644 --- a/src/components/__tests__/MenuItem.test.tsx +++ b/src/components/__tests__/MenuItem.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import { tokens } from '../../theme/tokens'; 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 9fd9eef832..f4b37bc564 100644 --- a/src/components/__tests__/Modal.test.tsx +++ b/src/components/__tests__/Modal.test.tsx @@ -6,8 +6,9 @@ import { Text, } 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 { LightTheme } from '../../theme/schemes'; import { tokens } from '../../theme/tokens'; import Modal from '../Modal'; 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 06a7390626..94ab9828b1 100644 --- a/src/components/__tests__/SegmentedButton.test.tsx +++ b/src/components/__tests__/SegmentedButton.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import { getTheme } from '../../core/theming'; +import { render } from '../../test-utils'; import { tokens } from '../../theme/tokens'; import SegmentedButtons from '../SegmentedButtons/SegmentedButtons'; import { diff --git a/src/components/__tests__/Snackbar.test.tsx b/src/components/__tests__/Snackbar.test.tsx index a5938d516b..334a9bca0a 100644 --- a/src/components/__tests__/Snackbar.test.tsx +++ b/src/components/__tests__/Snackbar.test.tsx @@ -1,8 +1,9 @@ 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 { render } from '../../test-utils'; import { red200, white } from '../../theme/colors'; import Snackbar from '../Snackbar'; 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 2022439d76..27637c085b 100644 --- a/src/components/__tests__/Switch.test.tsx +++ b/src/components/__tests__/Switch.test.tsx @@ -1,10 +1,10 @@ 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'; +import { render } from '../../test-utils'; import { white, black, diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index a9f06c7a8e..3b4c860e1f 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -1,10 +1,12 @@ /* 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 { fireEvent } from '@testing-library/react-native'; +import PaperProvider from '../../core/PaperProvider'; import { DefaultTheme, getTheme, ThemeProvider } from '../../core/theming'; +import { render } from '../../test-utils'; import { red500 } from '../../theme/colors'; import { tokens } from '../../theme/tokens'; import { @@ -259,28 +261,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__/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 67f99ae94b..3fc4b37361 100644 --- a/src/components/__tests__/Typography/Text.test.tsx +++ b/src/components/__tests__/Typography/Text.test.tsx @@ -1,8 +1,7 @@ import * as React from 'react'; -import { render } from '@testing-library/react-native'; - import PaperProvider from '../../../core/PaperProvider'; +import { render } from '../../../test-utils'; import configureFonts from '../../../theme/fonts'; import { LightTheme } from '../../../theme/schemes'; import { tokens } from '../../../theme/tokens'; diff --git a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap index 1954a404ef..eb09a78f1a 100644 --- a/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Searchbar.test.tsx.snap @@ -195,7 +195,6 @@ exports[`activity indicator snapshot test 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -204,6 +203,7 @@ exports[`activity indicator snapshot test 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, @@ -613,7 +613,6 @@ exports[`renders with placeholder 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -622,6 +621,7 @@ exports[`renders with placeholder 1`] = ` "fontWeight": "400", "letterSpacing": 0.15, "lineHeight": 0, + "textAlign": "left", }, { "minHeight": 56, @@ -990,7 +990,6 @@ exports[`renders with text 1`] = ` "fontSize": 18, "minWidth": 0, "paddingLeft": 8, - "textAlign": "left", }, { "color": "rgba(73, 69, 79, 1)", @@ -999,6 +998,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..cdb2d5d8a9 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,6 +19,7 @@ export type Props = { children: React.ReactNode; theme?: ThemeProp; settings?: Settings; + direction?: Direction; }; const PaperProvider = (props: Props) => { @@ -88,6 +90,8 @@ const PaperProvider = (props: Props) => { const { children, settings } = props; + const direction = props.direction ?? getDefaultDirection(); + const settingsValue = React.useMemo( () => ({ icon: MaterialCommunityIcon, @@ -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..11433f736f --- /dev/null +++ b/src/core/locale.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { I18nManager } from 'react-native'; + +/** + * 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'; + +export type LocaleContextValue = { + direction: Direction; +}; + +export const LocaleContext = React.createContext( + null +); + +export type LocaleProviderProps = { + direction: Direction; + children: React.ReactNode; +}; + +/** + * 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`). + */ +export function useLocale(): LocaleContextValue { + const context = React.useContext(LocaleContext); + if (context === null) { + throw new Error('useLocale must be used within a LocaleProvider'); + } + return context; +} + +export const getDefaultDirection = (): Direction => + I18nManager.getConstants?.().isRTL ? 'rtl' : 'ltr'; diff --git a/src/index.tsx b/src/index.tsx index 298658cf50..ce2d6f98dc 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -8,10 +8,13 @@ export { adaptNavigationTheme, } from './core/theming'; +export { useLocale, LocaleProvider } from './core/locale'; + export * from './theme/schemes'; export { default as Provider } from './core/PaperProvider'; export { default as PaperProvider } from './core/PaperProvider'; + export { default as shadow } from './theme/shadow'; export { default as configureFonts } from './theme/fonts'; 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/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 ( ( + + + {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.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 c0b7c9cd69..56d776d859 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,7 +30,8 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "esnext" + "target": "esnext", + "types": ["jest", "node"] }, "exclude": [ "lib/**/*"