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 ed4a200b09..7c3cb2761a 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 ad49fa13be..944d46a2c9 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 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 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 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 ce6088ecb1..9213cc70b4 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 type { MD3Theme, ThemeProp } from '../types'; import { forwardRef } from '../utils/forwardRef'; @@ -193,6 +193,7 @@ const Searchbar = forwardRef( ref ) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { colors, fonts } = theme as MD3Theme; const root = React.useRef(null); @@ -228,6 +229,7 @@ const Searchbar = forwardRef( }; const isBarMode = mode === 'bar'; + const inputTextAlign = direction === 'rtl' ? 'right' : 'left'; const shouldRenderTraileringIcon = isBarMode && traileringIcon && @@ -262,7 +264,7 @@ const Searchbar = forwardRef( name="magnify" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -277,6 +279,7 @@ const Searchbar = forwardRef( color: textColor, ...font, ...Platform.select({ web: { outline: 'none' } }), + textAlign: inputTextAlign, }, isBarMode ? styles.barModeInput : styles.viewModeInput, inputStyle, @@ -323,7 +326,7 @@ const Searchbar = forwardRef( name="close" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -373,7 +376,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 9ee01c7cb2..852dfec20c 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, MD3Theme, 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 e1f4c2a6c3..01a400ae2d 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; @@ -169,11 +171,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 @@ -277,13 +276,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, @@ -419,11 +414,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 63efb7f14b..1ebc55c278 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; @@ -132,7 +134,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); @@ -149,8 +151,7 @@ const TextInputOutlined = ({ if (isAdornmentLeftIcon) { labelTranslationXOffset = - (I18nManager.getConstants().isRTL ? -1 : 1) * ADORNMENT_SIZE + - ADORNMENT_OFFSET; + (isRTL ? -1 : 1) * ADORNMENT_SIZE + ADORNMENT_OFFSET; } const minInputHeight = @@ -403,11 +404,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__/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__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index 2fbebb326f..2e4c1f0703 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -1,9 +1,10 @@ /* 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 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'; @@ -259,28 +260,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 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..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 (