diff --git a/docs/docs/guides/05-react-native-web.md b/docs/docs/guides/05-react-native-web.md index 14b2f14a7a..154b479a11 100644 --- a/docs/docs/guides/05-react-native-web.md +++ b/docs/docs/guides/05-react-native-web.md @@ -1,8 +1,8 @@ --- -title: Using on the Web +title: Usage on the Web --- -# Using on the Web +# Usage on the Web ## Pre-requisites @@ -270,6 +270,19 @@ You can also load these fonts using [`css-loader`](https://github.com/webpack-co The default theme in React Native Paper uses the Roboto font. You can add them to your project following [the instructions on its Google Fonts page](https://fonts.google.com/specimen/Roboto?selection.family=Roboto:100,300,400,500). +## RTL support for Web +Since `react-native-web` does not support `I18nManager`, in order to support RTL layouts you have to define text direction manually: + +```jsx +export default function Main() { + return ( + + + + ); +} +``` + ## We're done! You can run `webpack-dev-server` to run the webpack server and open your project in the browser. You can add the following script in your `package.json` under the `"scripts"` section to make it easier: diff --git a/docs/src/components/GetStartedButtons.tsx b/docs/src/components/GetStartedButtons.tsx index 4debe5b5ac..964dc3166a 100644 --- a/docs/src/components/GetStartedButtons.tsx +++ b/docs/src/components/GetStartedButtons.tsx @@ -21,7 +21,7 @@ const styles = StyleSheet.create({ paddingBottom: 16, }, button: { - marginRight: 16, + marginEnd: 16, }, }); @@ -70,7 +70,7 @@ const Shimmer = () => { (0); - const preferences = React.useContext(PreferencesContext); + const preferences = usePreferences(); const _setDrawerItem = (index: number) => setDrawerItemIndex(index); const { isV3, colors } = useExampleTheme(); const isIOS = Platform.OS === 'ios'; - if (!preferences) throw new Error('PreferencesContext not provided'); - const { toggleShouldUseDeviceColors, toggleTheme, - toggleRtl: toggleRTL, toggleThemeVersion, toggleCollapsed, toggleCustomFont, toggleRippleEffect, + toggleRTL, customFontLoaded, rippleEffectEnabled, collapsed, - rtl: isRTL, + isRTL, theme: { dark: isDarkTheme }, shouldUseDeviceColors, } = preferences; - const _handleToggleRTL = () => { - toggleRTL(); - I18nManager.forceRTL(!isRTL); - if (isWeb) { - Updates.reloadAsync(); - } - }; - const coloredLabelTheme = { colors: isV3 ? { @@ -190,16 +180,14 @@ function DrawerItems() { - {!isWeb && ( - - - RTL - - - + + + RTL + + - - )} + + diff --git a/example/src/ExampleList.tsx b/example/src/ExampleList.tsx index 3645b741e4..4bce0dce78 100644 --- a/example/src/ExampleList.tsx +++ b/example/src/ExampleList.tsx @@ -157,8 +157,8 @@ export default function ExampleList({ navigation }: Props) { contentContainerStyle={{ backgroundColor: colors.background, paddingBottom: safeArea.bottom, - paddingLeft: safeArea.left, - paddingRight: safeArea.right, + paddingStart: safeArea.left, + paddingEnd: safeArea.right, }} style={{ backgroundColor: colors.background, diff --git a/example/src/Examples/AnimatedFABExample/AnimatedFABExample.tsx b/example/src/Examples/AnimatedFABExample/AnimatedFABExample.tsx index b2f475be0e..b05d68800b 100644 --- a/example/src/Examples/AnimatedFABExample/AnimatedFABExample.tsx +++ b/example/src/Examples/AnimatedFABExample/AnimatedFABExample.tsx @@ -11,6 +11,7 @@ import { } from 'react-native-paper'; import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; +import { useLocale } from '../../../../src/core/Localization'; import CustomFAB from './CustomFAB'; import CustomFABControls, { Controls, @@ -33,6 +34,7 @@ type Item = { const AnimatedFABExample = () => { const { colors, isV3 } = useExampleTheme(); + const { localeProps } = useLocale(); const isIOS = Platform.OS === 'ios'; @@ -155,6 +157,7 @@ const AnimatedFABExample = () => { ]} contentContainerStyle={styles.container} onScroll={onScroll} + {...localeProps} /> { const { colors, isV3 } = useExampleTheme(); const [selectedMode, setSelectedMode] = React.useState('elevated' as Mode); const [isSelected, setIsSelected] = React.useState(false); - const preferences = React.useContext(PreferencesContext); + const preferences = usePreferences(); const modes = isV3 ? ['elevated', 'outlined', 'contained'] @@ -188,7 +189,7 @@ const CardExample = () => { { - preferences?.toggleTheme(); + preferences.toggleTheme(); }} mode={selectedMode} > diff --git a/example/src/Examples/ChipExample.tsx b/example/src/Examples/ChipExample.tsx index f161197fa7..6ee7362f8a 100644 --- a/example/src/Examples/ChipExample.tsx +++ b/example/src/Examples/ChipExample.tsx @@ -354,8 +354,8 @@ const styles = StyleSheet.create({ }, tiny: { marginVertical: 2, - marginRight: 2, - marginLeft: 2, + marginEnd: 2, + marginStart: 2, minHeight: 19, lineHeight: 19, }, diff --git a/example/src/Examples/Dialogs/DialogWithLoadingIndicator.tsx b/example/src/Examples/Dialogs/DialogWithLoadingIndicator.tsx index ac9754fb77..7c1f6fc1d4 100644 --- a/example/src/Examples/Dialogs/DialogWithLoadingIndicator.tsx +++ b/example/src/Examples/Dialogs/DialogWithLoadingIndicator.tsx @@ -25,7 +25,7 @@ const DialogWithLoadingIndicator = ({ Loading..... @@ -40,8 +40,8 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, - marginRight: { - marginRight: 16, + marginEnd: { + marginEnd: 16, }, }); diff --git a/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx b/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx index 966422369e..8893cece09 100644 --- a/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx +++ b/example/src/Examples/Dialogs/DialogWithRadioBtns.tsx @@ -106,6 +106,6 @@ const styles = StyleSheet.create({ paddingVertical: 8, }, text: { - paddingLeft: 8, + paddingStart: 8, }, }); diff --git a/example/src/Examples/MenuExample.tsx b/example/src/Examples/MenuExample.tsx index 6559f3d8f0..3f3cd142e5 100644 --- a/example/src/Examples/MenuExample.tsx +++ b/example/src/Examples/MenuExample.tsx @@ -185,7 +185,10 @@ const styles = StyleSheet.create({ md3Divider: { marginVertical: 8, }, - bottomMenu: { width: '40%' }, + bottomMenu: { + width: '50%', + paddingStart: 20, + }, contentContainer: { justifyContent: 'space-between', flex: 1, diff --git a/example/src/Examples/SnackbarExample.tsx b/example/src/Examples/SnackbarExample.tsx index 4a6511a9b9..d220043a05 100644 --- a/example/src/Examples/SnackbarExample.tsx +++ b/example/src/Examples/SnackbarExample.tsx @@ -3,7 +3,8 @@ import { StyleSheet, View } from 'react-native'; import { Snackbar, Button, List, Text, Switch } from 'react-native-paper'; -import { PreferencesContext, useExampleTheme } from '..'; +import { useExampleTheme } from '..'; +import { usePreferences } from '../PreferencesContext'; import ScreenWrapper from '../ScreenWrapper'; const SHORT_MESSAGE = 'Single-line snackbar'; @@ -11,7 +12,7 @@ const LONG_MESSAGE = 'Snackbar with longer message which does not fit in one line'; const SnackbarExample = () => { - const preferences = React.useContext(PreferencesContext); + const preferences = usePreferences(); const theme = useExampleTheme(); const [options, setOptions] = React.useState({ @@ -33,7 +34,7 @@ const SnackbarExample = () => { const action = { label: showLongerAction ? 'Toggle Theme' : 'Action', onPress: () => { - preferences?.toggleTheme(); + preferences.toggleTheme(); }, }; diff --git a/example/src/Examples/SurfaceExample.tsx b/example/src/Examples/SurfaceExample.tsx index 8fdc698141..a2e9f9646f 100644 --- a/example/src/Examples/SurfaceExample.tsx +++ b/example/src/Examples/SurfaceExample.tsx @@ -53,10 +53,10 @@ const SurfaceExample = () => { - Left + Start - Right + End diff --git a/example/src/Examples/TeamDetails.tsx b/example/src/Examples/TeamDetails.tsx index 540507ffe8..61ed4856bc 100644 --- a/example/src/Examples/TeamDetails.tsx +++ b/example/src/Examples/TeamDetails.tsx @@ -23,6 +23,7 @@ import { } from 'react-native-paper'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useLocale } from '../../../src/core/Localization'; import { colorThemes, teamResultsData } from '../../utils'; import ScreenWrapper from '../ScreenWrapper'; @@ -180,6 +181,7 @@ const Results = () => { const ThemeBasedOnSourceColor = ({ navigation, route }: Props) => { const insets = useSafeAreaInsets(); const [index, setIndex] = React.useState(0); + const { direction } = useLocale(); const { params } = route; const { sourceColor, headerTitle, darkMode } = params; @@ -217,7 +219,7 @@ const ThemeBasedOnSourceColor = ({ navigation, route }: Props) => { const colorScheme = darkMode ? 'dark' : systemColorScheme; return ( - + navigation.goBack()} /> @@ -264,7 +266,7 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, score: { - marginRight: 16, + marginEnd: 16, }, fab: { position: 'absolute', @@ -282,10 +284,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', }, chipsContent: { - paddingLeft: 8, + paddingStart: 8, paddingVertical: 8, }, chip: { - marginRight: 8, + marginEnd: 8, }, }); diff --git a/example/src/Examples/TextInputExample.tsx b/example/src/Examples/TextInputExample.tsx index 530c22f3ba..f547a49e8a 100644 --- a/example/src/Examples/TextInputExample.tsx +++ b/example/src/Examples/TextInputExample.tsx @@ -701,7 +701,7 @@ const styles = StyleSheet.create({ margin: 8, }, inputContentStyle: { - paddingLeft: 50, + paddingStart: 50, fontWeight: 'bold', fontStyle: 'italic', }, diff --git a/example/src/PreferencesContext.ts b/example/src/PreferencesContext.ts new file mode 100644 index 0000000000..c85c163d9e --- /dev/null +++ b/example/src/PreferencesContext.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; + +import type { MD2Theme, MD3Theme } from 'react-native-paper'; + +const PreferencesContext = React.createContext<{ + toggleShouldUseDeviceColors?: () => void; + toggleTheme: () => void; + toggleRTL: () => void; + toggleThemeVersion: () => void; + toggleCollapsed: () => void; + toggleCustomFont: () => void; + toggleRippleEffect: () => void; + customFontLoaded: boolean; + rippleEffectEnabled: boolean; + collapsed: boolean; + isRTL: boolean; + theme: MD2Theme | MD3Theme; + shouldUseDeviceColors?: boolean; +} | null>(null); + +export const usePreferences = () => { + const context = React.useContext(PreferencesContext); + + if (!context) { + throw new Error('Context required'); + } + + return context; +}; + +export default PreferencesContext; diff --git a/example/src/ScreenWrapper.tsx b/example/src/ScreenWrapper.tsx index bb96250899..3291c81113 100644 --- a/example/src/ScreenWrapper.tsx +++ b/example/src/ScreenWrapper.tsx @@ -35,8 +35,8 @@ export default function ScreenWrapper({ { backgroundColor: theme.colors.background, paddingBottom: insets.bottom, - paddingLeft: insets.left, - paddingRight: insets.left, + paddingStart: insets.left, + paddingEnd: insets.left, }, ]; diff --git a/example/src/index.native.tsx b/example/src/index.native.tsx index a7966a6cc8..177f48ddd2 100644 --- a/example/src/index.native.tsx +++ b/example/src/index.native.tsx @@ -29,28 +29,13 @@ import { import { SafeAreaInsetsContext } from 'react-native-safe-area-context'; import DrawerItems from './DrawerItems'; +import PreferencesContext from './PreferencesContext'; import App from './RootNavigator'; import { deviceColorsSupported } from '../utils'; const PERSISTENCE_KEY = 'NAVIGATION_STATE'; const PREFERENCES_KEY = 'APP_PREFERENCES'; -export const PreferencesContext = React.createContext<{ - toggleShouldUseDeviceColors?: () => void; - toggleTheme: () => void; - toggleRtl: () => void; - toggleThemeVersion: () => void; - toggleCollapsed: () => void; - toggleCustomFont: () => void; - toggleRippleEffect: () => void; - customFontLoaded: boolean; - rippleEffectEnabled: boolean; - collapsed: boolean; - rtl: boolean; - theme: MD2Theme | MD3Theme; - shouldUseDeviceColors?: boolean; -} | null>(null); - export const useExampleTheme = () => useTheme(); const Drawer = createDrawerNavigator<{ Home: undefined }>(); @@ -71,7 +56,7 @@ export default function PaperExample() { React.useState(true); const [isDarkMode, setIsDarkMode] = React.useState(false); const [themeVersion, setThemeVersion] = React.useState<2 | 3>(3); - const [rtl, setRtl] = React.useState( + const [isRTL, setIsRTL] = React.useState( I18nManager.getConstants().isRTL ); const [collapsed, setCollapsed] = React.useState(false); @@ -122,7 +107,7 @@ export default function PaperExample() { setIsDarkMode(preferences.theme === 'dark'); if (typeof preferences.rtl === 'boolean') { - setRtl(preferences.rtl); + setIsRTL(preferences.rtl); } } } catch (e) { @@ -140,28 +125,28 @@ export default function PaperExample() { PREFERENCES_KEY, JSON.stringify({ theme: isDarkMode ? 'dark' : 'light', - rtl, + rtl: isRTL, }) ); } catch (e) { // ignore error } - if (I18nManager.getConstants().isRTL !== rtl) { - I18nManager.forceRTL(rtl); + if (I18nManager.getConstants().isRTL !== isRTL) { + I18nManager.forceRTL(isRTL); Updates.reloadAsync(); } }; savePrefs(); - }, [rtl, isDarkMode]); + }, [isRTL, isDarkMode]); const preferences = React.useMemo( () => ({ toggleShouldUseDeviceColors: () => setShouldUseDeviceColors((oldValue) => !oldValue), toggleTheme: () => setIsDarkMode((oldValue) => !oldValue), - toggleRtl: () => setRtl((rtl) => !rtl), + toggleRTL: () => setIsRTL((rtl) => !rtl), toggleCollapsed: () => setCollapsed(!collapsed), toggleCustomFont: () => setCustomFont(!customFontLoaded), toggleRippleEffect: () => setRippleEffectEnabled(!rippleEffectEnabled), @@ -174,12 +159,12 @@ export default function PaperExample() { customFontLoaded, rippleEffectEnabled, collapsed, - rtl, + isRTL, theme, shouldUseDeviceColors, }), [ - rtl, + isRTL, theme, collapsed, customFontLoaded, diff --git a/example/src/index.tsx b/example/src/index.tsx index 4638ac2715..f8db09c265 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { I18nManager } from 'react-native'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createDrawerNavigator } from '@react-navigation/drawer'; @@ -25,23 +26,12 @@ import { import { SafeAreaInsetsContext } from 'react-native-safe-area-context'; import DrawerItems from './DrawerItems'; +import PreferencesContext from './PreferencesContext'; import App from './RootNavigator'; const PERSISTENCE_KEY = 'NAVIGATION_STATE'; const PREFERENCES_KEY = 'APP_PREFERENCES'; -export const PreferencesContext = React.createContext<{ - toggleTheme: () => void; - toggleThemeVersion: () => void; - toggleCollapsed: () => void; - toggleCustomFont: () => void; - toggleRippleEffect: () => void; - customFontLoaded: boolean; - rippleEffectEnabled: boolean; - collapsed: boolean; - theme: MD2Theme | MD3Theme; -} | null>(null); - export const useExampleTheme = () => useTheme(); const Drawer = createDrawerNavigator<{ Home: undefined }>(); @@ -63,6 +53,7 @@ export default function PaperExample() { const [collapsed, setCollapsed] = React.useState(false); const [customFontLoaded, setCustomFont] = React.useState(false); const [rippleEffectEnabled, setRippleEffectEnabled] = React.useState(true); + const [isRTL, setIsRTL] = React.useState(false); const theme = React.useMemo(() => { if (themeVersion === 2) { @@ -99,6 +90,10 @@ export default function PaperExample() { if (preferences) { setIsDarkMode(preferences.theme === 'dark'); + + if (typeof preferences.rtl === 'boolean') { + setIsRTL(preferences.rtl); + } } } catch (e) { // ignore error @@ -115,15 +110,19 @@ export default function PaperExample() { PREFERENCES_KEY, JSON.stringify({ theme: isDarkMode ? 'dark' : 'light', + rtl: isRTL, }) ); } catch (e) { // ignore error } + + document.documentElement.setAttribute('dir', isRTL ? 'rtl' : 'ltr'); + I18nManager.forceRTL(isRTL); }; savePrefs(); - }, [isDarkMode]); + }, [isDarkMode, isRTL]); const preferences = React.useMemo( () => ({ @@ -131,6 +130,7 @@ export default function PaperExample() { toggleCollapsed: () => setCollapsed(!collapsed), toggleCustomFont: () => setCustomFont(!customFontLoaded), toggleRippleEffect: () => setRippleEffectEnabled(!rippleEffectEnabled), + toggleRTL: () => setIsRTL((rtl) => !rtl), toggleThemeVersion: () => { setCustomFont(false); setCollapsed(false); @@ -140,9 +140,10 @@ export default function PaperExample() { customFontLoaded, rippleEffectEnabled, collapsed, + isRTL, theme, }), - [theme, collapsed, customFontLoaded, rippleEffectEnabled] + [customFontLoaded, rippleEffectEnabled, collapsed, isRTL, theme] ); if (!isReady && !fontsLoaded) { @@ -186,6 +187,7 @@ export default function PaperExample() { @@ -206,6 +208,7 @@ export default function PaperExample() { drawerStyle: collapsed && { width: collapsedDrawerWidth, }, + // drawerPosition: isRTL ? 'right' : 'left', }} drawerContent={() => } > diff --git a/src/components/Appbar/Appbar.tsx b/src/components/Appbar/Appbar.tsx index d6a69e7926..b8ff8277ac 100644 --- a/src/components/Appbar/Appbar.tsx +++ b/src/components/Appbar/Appbar.tsx @@ -245,8 +245,8 @@ const Appbar = ({ const insets = { paddingBottom: safeAreaInsets?.bottom, paddingTop: safeAreaInsets?.top, - paddingLeft: safeAreaInsets?.left, - paddingRight: safeAreaInsets?.right, + paddingStart: safeAreaInsets?.left, + paddingEnd: safeAreaInsets?.right, }; return ( diff --git a/src/components/Appbar/AppbarBackIcon.tsx b/src/components/Appbar/AppbarBackIcon.tsx index 14aa21ec03..9e6fb45710 100644 --- a/src/components/Appbar/AppbarBackIcon.tsx +++ b/src/components/Appbar/AppbarBackIcon.tsx @@ -1,9 +1,11 @@ import * as React from 'react'; -import { Platform, I18nManager, View, Image, StyleSheet } from 'react-native'; +import { Platform, View, Image, StyleSheet } from 'react-native'; +import { useLocale } from '../../core/Localization'; import MaterialCommunityIcon from '../MaterialCommunityIcon'; const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => { + const { direction } = useLocale(); const iosIconSize = size - 3; return Platform.OS === 'ios' ? ( @@ -13,7 +15,7 @@ const AppbarBackIcon = ({ size, color }: { size: number; color: string }) => { { width: size, height: size, - transform: [{ scaleX: I18nManager.getConstants().isRTL ? -1 : 1 }], + transform: [{ scaleX: direction === 'rtl' ? -1 : 1 }], }, ]} > @@ -31,7 +33,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/Appbar/AppbarContent.tsx b/src/components/Appbar/AppbarContent.tsx index 356bf2b60e..eed8ffaf0f 100644 --- a/src/components/Appbar/AppbarContent.tsx +++ b/src/components/Appbar/AppbarContent.tsx @@ -14,6 +14,7 @@ import { import color from 'color'; import { modeTextVariant } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import { white } from '../../styles/themes/v2/colors'; import type { $RemoveChildren, MD3TypescaleKey, ThemeProp } from '../../types'; @@ -111,6 +112,7 @@ const AppbarContent = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const { isV3, colors } = theme; const titleTextColor = titleColor @@ -136,6 +138,7 @@ const AppbarContent = ({ style={[styles.container, isV3 && modeContainerStyles[mode], style]} testID={testID} {...rest} + {...localeProps} > {typeof title === 'string' ? ( { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const { current: opacity } = React.useRef( new Animated.Value(visible ? 1 : 0) ); @@ -119,6 +121,7 @@ const Badge = ({ styles.container, restStyle, ]} + {...localeProps} {...rest} > {children} diff --git a/src/components/BottomNavigation/BottomNavigationBar.tsx b/src/components/BottomNavigation/BottomNavigationBar.tsx index ef7ec26847..fe57a71295 100644 --- a/src/components/BottomNavigation/BottomNavigationBar.tsx +++ b/src/components/BottomNavigation/BottomNavigationBar.tsx @@ -1007,7 +1007,7 @@ const styles = StyleSheet.create({ }, badgeContainer: { position: 'absolute', - left: 0, + start: 0, }, v3TouchableContainer: { paddingTop: 12, diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx index 3ed3ee791a..6d3c31c7f6 100644 --- a/src/components/Button/Button.tsx +++ b/src/components/Button/Button.tsx @@ -13,6 +13,7 @@ import { import color from 'color'; import { ButtonMode, getButtonColors } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { $Omit, ThemeProp } from '../../types'; import hasTouchHandler from '../../utils/hasTouchHandler'; @@ -172,6 +173,7 @@ const Button = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const isMode = React.useCallback( (modeToCompare: ButtonMode) => { return mode === modeToCompare; @@ -295,6 +297,7 @@ const Button = ({ ] as ViewStyle } {...(isV3 && { elevation: elevation })} + {...localeProps} > { const theme = useInternalTheme(themeOverrides); + const { direction, localeProps } = useLocale(); const { isV3 } = theme; const { current: elevation } = React.useRef( @@ -261,14 +263,14 @@ const Chip = ({ const elevationStyle = isV3 || Platform.OS === 'android' ? elevation : 0; const multiplier = isV3 ? (compact ? 1.5 : 2) : 1; const labelSpacings = { - marginRight: onClose ? 0 : 8 * multiplier, - marginLeft: + marginEnd: onClose ? 0 : 8 * multiplier, + marginStart: avatar || icon || (selected && showSelectedCheck) ? 4 * multiplier : 8 * multiplier, }; const contentSpacings = { - paddingRight: isV3 ? (onClose ? 34 : 0) : onClose ? 32 : 4, + paddingEnd: isV3 ? (onClose ? 34 : 0) : onClose ? 32 : 4, }; const labelTextStyle = { color: textColor, @@ -294,6 +296,8 @@ const Chip = ({ {...rest} testID={`${testID}-container`} theme={theme} + // @ts-ignore + dir="rtl" > )} @@ -403,7 +408,7 @@ const Chip = ({ name={isV3 ? 'close' : 'close-circle'} size={iconSize} color={iconColor} - direction="ltr" + direction={direction} /> )} @@ -429,25 +434,25 @@ const styles = StyleSheet.create({ content: { flexDirection: 'row', alignItems: 'center', - paddingLeft: 4, + paddingStart: 4, position: 'relative', }, md3Content: { - paddingLeft: 0, + paddingStart: 0, }, icon: { padding: 4, alignSelf: 'center', }, md3Icon: { - paddingLeft: 8, - paddingRight: 0, + paddingStart: 8, + paddingEnd: 0, }, closeIcon: { - marginRight: 4, + marginEnd: 4, }, md3CloseIcon: { - marginRight: 8, + marginEnd: 8, padding: 0, }, labelText: { @@ -466,25 +471,25 @@ const styles = StyleSheet.create({ borderRadius: 12, }, avatarWrapper: { - marginRight: 4, + marginEnd: 4, }, md3AvatarWrapper: { - marginLeft: 4, - marginRight: 0, + marginStart: 4, + marginEnd: 0, }, md3SelectedIcon: { - paddingLeft: 4, + paddingStart: 4, }, // eslint-disable-next-line react-native/no-color-literals avatarSelected: { position: 'absolute', top: 4, - left: 4, + start: 4, backgroundColor: 'rgba(0, 0, 0, .29)', }, closeButtonStyle: { position: 'absolute', - right: 0, + end: 0, height: '100%', justifyContent: 'center', alignItems: 'center', diff --git a/src/components/DataTable/DataTablePagination.tsx b/src/components/DataTable/DataTablePagination.tsx index dd72dbe54f..f09195209f 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/Localization'; 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.isV3 ? theme.colors.onSurface : theme.colors.text; @@ -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} @@ -377,7 +378,7 @@ const styles = StyleSheet.create({ justifyContent: 'flex-end', flexDirection: 'row', alignItems: 'center', - paddingLeft: 16, + paddingStart: 16, flexWrap: 'wrap', }, optionsContainer: { @@ -387,11 +388,11 @@ const styles = StyleSheet.create({ }, label: { fontSize: 12, - marginRight: 16, + marginEnd: 16, }, button: { textAlign: 'center', - marginRight: 16, + marginEnd: 16, }, iconsContainer: { flexDirection: 'row', diff --git a/src/components/DataTable/DataTableTitle.tsx b/src/components/DataTable/DataTableTitle.tsx index 4b325bf82e..579c807b0c 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, StyleProp, StyleSheet, TextStyle, @@ -13,6 +12,7 @@ import { import color from 'color'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import MaterialCommunityIcon from '../MaterialCommunityIcon'; @@ -90,6 +90,7 @@ const DataTableTitle = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const { current: spinAnim } = React.useRef( new Animated.Value(sortDirection === 'ascending' ? 0 : 1) ); @@ -117,7 +118,7 @@ const DataTableTitle = ({ name="arrow-up" size={16} color={textColor} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> ) : null; @@ -135,7 +136,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 @@ -186,7 +187,7 @@ const styles = StyleSheet.create({ }, sorted: { - marginLeft: 8, + marginStart: 8, }, icon: { diff --git a/src/components/Dialog/DialogActions.tsx b/src/components/Dialog/DialogActions.tsx index b8f7b0b16a..08e988fa51 100644 --- a/src/components/Dialog/DialogActions.tsx +++ b/src/components/Dialog/DialogActions.tsx @@ -61,7 +61,7 @@ const DialogActions = (props: Props) => { uppercase: !isV3, style: [ isV3 && { - marginRight: i + 1 === actionsLength ? 0 : 8, + marginEnd: i + 1 === actionsLength ? 0 : 8, }, child.props.style, ], diff --git a/src/components/Divider.tsx b/src/components/Divider.tsx index be30d94c7c..3dc3a1512c 100644 --- a/src/components/Divider.tsx +++ b/src/components/Divider.tsx @@ -85,14 +85,14 @@ const Divider = ({ const styles = StyleSheet.create({ leftInset: { - marginLeft: 72, + marginStart: 72, }, v3LeftInset: { - marginLeft: 16, + marginStart: 16, }, horizontalInset: { - marginLeft: 16, - marginRight: 16, + marginStart: 16, + marginEnd: 16, }, bold: { height: 1, diff --git a/src/components/Drawer/DrawerCollapsedItem.tsx b/src/components/Drawer/DrawerCollapsedItem.tsx index 86ba1e2a58..354c161a8f 100644 --- a/src/components/Drawer/DrawerCollapsedItem.tsx +++ b/src/components/Drawer/DrawerCollapsedItem.tsx @@ -261,7 +261,7 @@ const styles = StyleSheet.create({ }, badgeContainer: { position: 'absolute', - left: 20, + start: 20, bottom: 20, zIndex: 2, }, diff --git a/src/components/Drawer/DrawerItem.tsx b/src/components/Drawer/DrawerItem.tsx index d01be2e3de..fce69fa56e 100644 --- a/src/components/Drawer/DrawerItem.tsx +++ b/src/components/Drawer/DrawerItem.tsx @@ -136,7 +136,7 @@ const DrawerItem = ({ styles.label, { color: contentColor, - marginLeft: labelMargin, + marginStart: labelMargin, ...font, }, ]} @@ -162,8 +162,8 @@ const styles = StyleSheet.create({ v3Container: { justifyContent: 'center', height: 56, - marginLeft: 12, - marginRight: 12, + marginStart: 12, + marginEnd: 12, marginVertical: 0, }, wrapper: { @@ -172,8 +172,8 @@ const styles = StyleSheet.create({ padding: 8, }, v3Wrapper: { - marginLeft: 16, - marginRight: 24, + marginStart: 16, + marginEnd: 24, padding: 0, }, content: { @@ -182,7 +182,7 @@ const styles = StyleSheet.create({ alignItems: 'center', }, label: { - marginRight: 32, + marginEnd: 32, }, }); diff --git a/src/components/Drawer/DrawerSection.tsx b/src/components/Drawer/DrawerSection.tsx index 16d2c2f662..9ed33f36e8 100644 --- a/src/components/Drawer/DrawerSection.tsx +++ b/src/components/Drawer/DrawerSection.tsx @@ -86,7 +86,7 @@ const DrawerSection = ({ style={[ { color: titleColor, - marginLeft: titleMargin, + marginStart: titleMargin, ...font, }, ]} diff --git a/src/components/FAB/AnimatedFAB.tsx b/src/components/FAB/AnimatedFAB.tsx index 757a143c7a..ec67d67544 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, @@ -21,6 +20,7 @@ import { import color from 'color'; import { getCombinedStyles, getFABColors } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { $Omit, $RemoveChildren, ThemeProp } from '../../types'; import type { IconSource } from '../Icon'; @@ -129,7 +129,6 @@ const SCALE = 0.9; * ScrollView, * Text, * SafeAreaView, - * I18nManager, * } from 'react-native'; * import { AnimatedFAB } from 'react-native-paper'; * @@ -213,11 +212,11 @@ const AnimatedFAB = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const uppercase: boolean = uppercaseProp ?? !theme.isV3; const isIOS = Platform.OS === 'ios'; const isAnimatedFromRight = animateFrom === 'right'; const isIconStatic = iconMode === 'static'; - const { isRTL } = I18nManager; const { current: visibility } = React.useRef( new Animated.Value(visible ? 1 : 0) ); @@ -307,6 +306,7 @@ const AnimatedFAB = ({ isIconStatic, distance, animFAB, + direction, }); const font = isV3 ? theme.fonts.labelLarge : theme.fonts.medium; @@ -463,9 +463,10 @@ const AnimatedFAB = ({ ellipsizeMode={'tail'} style={[ { - [isAnimatedFromRight || isRTL ? 'right' : 'left']: isIconStatic - ? textWidth - SIZE + borderRadius / (isV3 ? 1 : 2) - : borderRadius, + [isAnimatedFromRight || direction === 'rtl' ? 'right' : 'left']: + isIconStatic + ? textWidth - SIZE + borderRadius / (isV3 ? 1 : 2) + : borderRadius, }, { minWidth: textWidth, diff --git a/src/components/FAB/FABGroup.tsx b/src/components/FAB/FABGroup.tsx index 9aeae123f6..d865527d76 100644 --- a/src/components/FAB/FABGroup.tsx +++ b/src/components/FAB/FABGroup.tsx @@ -324,8 +324,8 @@ const FABGroup = ({ const { top, bottom, right, left } = useSafeAreaInsets(); const containerPaddings = { paddingBottom: bottom, - paddingRight: right, - paddingLeft: left, + paddingEnd: right, + paddingStart: left, paddingTop: top, }; diff --git a/src/components/FAB/utils.ts b/src/components/FAB/utils.ts index 3396acda4d..8522424507 100644 --- a/src/components/FAB/utils.ts +++ b/src/components/FAB/utils.ts @@ -1,7 +1,8 @@ -import { Animated, ColorValue, I18nManager, ViewStyle } from 'react-native'; +import type { Animated, ColorValue, ViewStyle } from 'react-native'; import color from 'color'; +import type { Direction } from '../../core/Localization'; import { black, white } from '../../styles/themes/v2/colors'; import type { InternalTheme } from '../../types'; import getContrastingColor from '../../utils/getContrastingColor'; @@ -11,6 +12,7 @@ type GetCombinedStylesProps = { isIconStatic: boolean; distance: number; animFAB: Animated.Value; + direction: Direction; }; type CombinedStyles = { @@ -32,9 +34,9 @@ export const getCombinedStyles = ({ isIconStatic, distance, animFAB, + direction, }: GetCombinedStylesProps): CombinedStyles => { - const { isRTL } = I18nManager; - + const isRTL = direction === 'rtl'; const defaultPositionStyles = { left: -distance, right: undefined }; const combinedStyles: CombinedStyles = { diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 84a4b617ae..650636463c 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/Localization'; import { Consumer as SettingsConsumer } from '../core/settings'; import { useInternalTheme } from '../core/theming'; import type { ThemeProp } from '../types'; @@ -75,10 +71,11 @@ const Icon = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction: localeDirection } = useLocale(); const direction = typeof source === 'object' && source.direction && source.source ? source.direction === 'auto' - ? I18nManager.getConstants().isRTL + ? localeDirection ? 'rtl' : 'ltr' : source.direction diff --git a/src/components/List/ListAccordion.tsx b/src/components/List/ListAccordion.tsx index 0674ed5a01..bc847dbb60 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, @@ -14,8 +13,9 @@ import { } from 'react-native'; import { ListAccordionGroupContext } from './ListAccordionGroup'; -import type { Style } from './utils'; import { getAccordionColors, getLeftStyles } from './utils'; +import type { Style } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import MaterialCommunityIcon from '../MaterialCommunityIcon'; @@ -169,6 +169,7 @@ const ListAccordion = ({ pointerEvents = 'none', }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction } = useLocale(); const [expanded, setExpanded] = React.useState( expandedProp || false ); @@ -288,7 +289,7 @@ const ListAccordion = ({ name={isExpanded ? 'chevron-up' : 'chevron-down'} color={theme.isV3 ? descriptionColor : titleColor} size={24} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )} @@ -357,7 +358,7 @@ const styles = StyleSheet.create({ paddingLeft: 16, }, child: { - paddingLeft: 64, + paddingStart: 64, }, childV3: { paddingLeft: 40, diff --git a/src/components/List/ListImage.tsx b/src/components/List/ListImage.tsx index 9839be7b02..a94ea564f7 100644 --- a/src/components/List/ListImage.tsx +++ b/src/components/List/ListImage.tsx @@ -75,12 +75,12 @@ const styles = StyleSheet.create({ video: { width: 100, height: 64, - marginLeft: 0, + marginStart: 0, }, videoV3: { width: 114, height: 64, - marginLeft: 0, + marginStart: 0, }, }); diff --git a/src/components/List/ListItem.tsx b/src/components/List/ListItem.tsx index c945251225..5f4fc63865 100644 --- a/src/components/List/ListItem.tsx +++ b/src/components/List/ListItem.tsx @@ -13,6 +13,7 @@ import { import color from 'color'; import { Style, getLeftStyles, getRightStyles } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { $RemoveChildren, EllipsizeProp, ThemeProp } from '../../types'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; @@ -135,6 +136,7 @@ const ListItem = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const [alignToTop, setAlignToTop] = React.useState(false); const onDescriptionTextLayout = ( @@ -209,8 +211,9 @@ const ListItem = ({ style={[theme.isV3 ? styles.containerV3 : styles.container, style]} onPress={onPress} theme={theme} + {...localeProps} > - + {left ? left({ color: descriptionColor, @@ -245,7 +248,7 @@ const styles = StyleSheet.create({ }, containerV3: { paddingVertical: 8, - paddingRight: 24, + paddingEnd: 24, }, row: { width: '100%', @@ -264,10 +267,10 @@ const styles = StyleSheet.create({ }, item: { marginVertical: 6, - paddingLeft: 8, + paddingStart: 8, }, itemV3: { - paddingLeft: 16, + paddingStart: 16, }, content: { flexShrink: 1, diff --git a/src/components/List/ListSection.tsx b/src/components/List/ListSection.tsx index 59640c70da..0a7c325931 100644 --- a/src/components/List/ListSection.tsx +++ b/src/components/List/ListSection.tsx @@ -8,6 +8,7 @@ import { } from 'react-native'; import ListSubheader from './ListSubheader'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; @@ -62,10 +63,11 @@ const ListSection = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const viewProps = { ...rest, theme }; return ( - + {title ? ( {title} diff --git a/src/components/List/utils.ts b/src/components/List/utils.ts index 773150255d..7ac12ec2c2 100644 --- a/src/components/List/utils.ts +++ b/src/components/List/utils.ts @@ -13,8 +13,8 @@ type Description = }) => React.ReactNode); export type Style = { - marginLeft?: number; - marginRight?: number; + marginStart?: number; + marginEnd?: number; marginVertical?: number; alignSelf?: FlexAlignType; }; @@ -25,25 +25,25 @@ export const getLeftStyles = ( isV3: boolean ) => { const stylesV3 = { - marginRight: 0, - marginLeft: 16, + marginEnd: 0, + marginStart: 16, alignSelf: alignToTop ? 'flex-start' : 'center', }; if (!description) { return { - ...styles.iconMarginLeft, + ...styles.iconmarginStart, ...styles.marginVerticalNone, ...(isV3 && { ...stylesV3 }), }; } if (!isV3) { - return styles.iconMarginLeft; + return styles.iconmarginStart; } return { - ...styles.iconMarginLeft, + ...styles.iconmarginStart, ...stylesV3, }; }; @@ -54,32 +54,32 @@ export const getRightStyles = ( isV3: boolean ) => { const stylesV3 = { - marginLeft: 16, + marginStart: 16, alignSelf: alignToTop ? 'flex-start' : 'center', }; if (!description) { return { - ...styles.iconMarginRight, + ...styles.iconmarginEnd, ...styles.marginVerticalNone, ...(isV3 && { ...stylesV3 }), }; } if (!isV3) { - return styles.iconMarginRight; + return styles.iconmarginEnd; } return { - ...styles.iconMarginRight, + ...styles.iconmarginEnd, ...stylesV3, }; }; const styles = StyleSheet.create({ marginVerticalNone: { marginVertical: 0 }, - iconMarginLeft: { marginLeft: 0, marginRight: 16 }, - iconMarginRight: { marginRight: 0 }, + iconmarginStart: { marginStart: 0, marginEnd: 16 }, + iconmarginEnd: { marginEnd: 0 }, }); export const getAccordionColors = ({ diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index 0d8be6c9e1..72ad2172ad 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -150,9 +150,6 @@ const WINDOW_LAYOUT = Dimensions.get('window'); * wrapping is not necessary if you use Paper's `Modal` instead. */ class Menu extends React.Component { - // @component ./MenuItem.tsx - static Item = MenuItem; - static defaultProps = { statusBarHeight: APPROX_STATUSBAR_HEIGHT, overlayAccessibilityLabel: 'Close menu', @@ -391,8 +388,7 @@ class Menu extends React.Component { }; private keyboardDidShow = (e: RNKeyboardEvent) => { - const keyboardHeight = e.endCoordinates.height; - this.keyboardHeight = keyboardHeight; + this.keyboardHeight = e.endCoordinates.height; }; private keyboardDidHide = () => { @@ -452,7 +448,14 @@ class Menu extends React.Component { ]; // We need to translate menu while animating scale to imitate transform origin for scale animation - const positionTransforms = []; + const positionTransforms: ( + | { + translateX: Animated.AnimatedInterpolation; + } + | { + translateY: Animated.AnimatedInterpolation; + } + )[] = []; // Check if menu fits horizontally and if not align it to right. if (left <= windowLayout.width - menuLayout.width - SCREEN_INDENT) { @@ -595,7 +598,7 @@ class Menu extends React.Component { const positionStyle = { top: this.isCoordinate(anchor) ? top : top + additionalVerticalValue, - ...(I18nManager.getConstants().isRTL ? { right: left } : { left }), + ...(I18nManager.getConstants().isRTL ? { end: left } : { start: left }), }; const pointerEvents = visible ? 'box-none' : 'none'; @@ -675,4 +678,7 @@ const styles = StyleSheet.create({ }, }); -export default withInternalTheme(Menu); +export default Object.assign(withInternalTheme(Menu), { + // @component ./MenuItem.tsx + Item: MenuItem, +}); diff --git a/src/components/Menu/MenuItem.tsx b/src/components/Menu/MenuItem.tsx index 320731efca..a430e605d0 100644 --- a/src/components/Menu/MenuItem.tsx +++ b/src/components/Menu/MenuItem.tsx @@ -16,6 +16,7 @@ import { MAX_WIDTH, MIN_WIDTH, } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp } from '../../types'; import Icon, { IconSource } from '../Icon'; @@ -125,6 +126,7 @@ const MenuItem = ({ titleMaxFontSizeMultiplier = 1.5, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); const { titleColor, iconColor, rippleColor } = getMenuItemColor({ theme, disabled, @@ -189,6 +191,7 @@ const MenuItem = ({ contentStyle, ]} pointerEvents="none" + {...localeProps} > & { 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. @@ -75,6 +74,7 @@ const ProgressBar = ({ ...rest }: Props) => { const isWeb = Platform.OS === 'web'; + const { direction } = useLocale(); const theme = useInternalTheme(themeOverrides); const { current: timer } = React.useRef( new Animated.Value(0) @@ -218,17 +218,20 @@ const ProgressBar = ({ ? { inputRange: [0, 0.5, 1], outputRange: [ - (isRTL ? 1 : -1) * 0.5 * width, - (isRTL ? 1 : -1) * + (direction === 'rtl' ? 1 : -1) * 0.5 * width, + (direction === 'rtl' ? 1 : -1) * 0.5 * INDETERMINATE_MAX_WIDTH * width, - (isRTL ? -1 : 1) * 0.7 * width, + (direction === 'rtl' ? -1 : 1) * 0.7 * width, ], } : { inputRange: [0, 1], - outputRange: [(isRTL ? 1 : -1) * 0.5 * width, 0], + outputRange: [ + (direction === 'rtl' ? 1 : -1) * 0.5 * width, + 0, + ], } ), }, diff --git a/src/components/RadioButton/RadioButtonItem.tsx b/src/components/RadioButton/RadioButtonItem.tsx index 2d577f9587..5824bdd257 100644 --- a/src/components/RadioButton/RadioButtonItem.tsx +++ b/src/components/RadioButton/RadioButtonItem.tsx @@ -13,6 +13,7 @@ import RadioButtonAndroid from './RadioButtonAndroid'; import { RadioButtonContext, RadioButtonContextType } from './RadioButtonGroup'; import RadioButtonIOS from './RadioButtonIOS'; import { handlePress, isChecked } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { ThemeProp, MD3TypescaleKey } from '../../types'; import TouchableRipple from '../TouchableRipple/TouchableRipple'; @@ -135,6 +136,7 @@ const RadioButtonItem = ({ labelVariant = 'bodyLarge', }: Props) => { const theme = useInternalTheme(themeOverrides); + const { overwriteRTL } = useLocale(); const radioButtonProps = { value, disabled, @@ -158,7 +160,12 @@ const RadioButtonItem = ({ const disabledTextColor = theme.isV3 ? theme.colors.onSurfaceDisabled : theme.colors.disabled; - const textAlign = isLeading ? 'right' : 'left'; + + let textAlign = isLeading ? 'right' : 'left'; + + if (overwriteRTL) { + textAlign = isLeading ? 'left' : 'right'; + } const computedStyle = { color: disabled ? disabledTextColor : textColor, diff --git a/src/components/Searchbar.tsx b/src/components/Searchbar.tsx index 08226c2855..5dcbeaa7b7 100644 --- a/src/components/Searchbar.tsx +++ b/src/components/Searchbar.tsx @@ -3,7 +3,6 @@ import { Animated, ColorValue, GestureResponderEvent, - I18nManager, Platform, StyleProp, StyleSheet, @@ -22,12 +21,13 @@ import type { IconSource } from './Icon'; import IconButton from './IconButton/IconButton'; import MaterialCommunityIcon from './MaterialCommunityIcon'; import Surface from './Surface'; +import { useLocale } from '../core/Localization'; import { useInternalTheme } from '../core/theming'; import type { ThemeProp } from '../types'; import { forwardRef } from '../utils/forwardRef'; interface Style { - marginRight: number; + marginEnd: number; } export type Props = React.ComponentPropsWithRef & { @@ -210,6 +210,7 @@ const Searchbar = forwardRef( ref ) => { const theme = useInternalTheme(themeOverrides); + const { direction, localeProps } = useLocale(); const root = React.useRef(null); React.useImperativeHandle(ref, () => { @@ -293,6 +294,7 @@ const Searchbar = forwardRef( testID={`${testID}-container`} {...(theme.isV3 && { elevation })} theme={theme} + {...localeProps} > ( name="magnify" color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -318,7 +320,9 @@ const Searchbar = forwardRef( ( name={isV3 ? 'close' : 'close-circle-outline'} color={color} size={size} - direction={I18nManager.getConstants().isRTL ? 'rtl' : 'ltr'} + direction={direction} /> )) } @@ -418,17 +422,16 @@ const styles = StyleSheet.create({ input: { flex: 1, fontSize: 18, - paddingLeft: 8, + paddingStart: 8, alignSelf: 'stretch', - textAlign: I18nManager.getConstants().isRTL ? 'right' : 'left', minWidth: 0, }, barModeInput: { - paddingLeft: 0, + paddingStart: 0, minHeight: 56, }, viewModeInput: { - paddingLeft: 0, + paddingStart: 0, minHeight: 72, }, elevation: { @@ -441,12 +444,12 @@ const styles = StyleSheet.create({ marginHorizontal: 16, }, rightStyle: { - marginRight: 16, + marginEnd: 16, }, v3ClearIcon: { position: 'absolute', right: 0, - marginLeft: 16, + marginStart: 16, }, v3ClearIconHidden: { display: 'none', diff --git a/src/components/SegmentedButtons/SegmentedButtonItem.tsx b/src/components/SegmentedButtons/SegmentedButtonItem.tsx index 60a7f9f7c1..f94ad99715 100644 --- a/src/components/SegmentedButtons/SegmentedButtonItem.tsx +++ b/src/components/SegmentedButtons/SegmentedButtonItem.tsx @@ -18,6 +18,7 @@ import { getSegmentedButtonColors, getSegmentedButtonDensityPadding, } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { IconSource } from '../Icon'; import Icon from '../Icon'; @@ -113,6 +114,7 @@ const SegmentedButtonItem = ({ theme: themeOverrides, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { overwriteRTL } = useLocale(); const checkScale = React.useRef(new Animated.Value(0)).current; @@ -147,6 +149,7 @@ const SegmentedButtonItem = ({ const segmentBorderRadius = getSegmentedButtonBorderRadius({ theme, segment, + overwriteRTL, }); const rippleColor = customRippleColor || color(textColor).alpha(0.12).rgb().string(); @@ -156,7 +159,7 @@ const SegmentedButtonItem = ({ const iconSize = isV3 ? 18 : 16; const iconStyle = { - marginRight: label ? 5 : showCheckedIcon ? 3 : 0, + marginEnd: label ? 5 : showCheckedIcon ? 3 : 0, ...(label && { transform: [ { diff --git a/src/components/SegmentedButtons/SegmentedButtons.tsx b/src/components/SegmentedButtons/SegmentedButtons.tsx index e5ab79629f..9620b73855 100644 --- a/src/components/SegmentedButtons/SegmentedButtons.tsx +++ b/src/components/SegmentedButtons/SegmentedButtons.tsx @@ -12,6 +12,7 @@ import type { ThemeProp } from 'src/types'; import SegmentedButtonItem from './SegmentedButtonItem'; import { getDisabledSegmentedButtonStyle } from './utils'; +import { useLocale } from '../../core/Localization'; import { useInternalTheme } from '../../core/theming'; import type { IconSource } from '../Icon'; @@ -136,9 +137,10 @@ const SegmentedButtons = ({ theme: themeOverrides, }: Props) => { const theme = useInternalTheme(themeOverrides); + const { localeProps } = useLocale(); return ( - + {buttons.map((item, i) => { const disabledChildStyle = getDisabledSegmentedButtonStyle({ theme, diff --git a/src/components/SegmentedButtons/utils.ts b/src/components/SegmentedButtons/utils.ts index 87d343c8a9..a21bc176c2 100644 --- a/src/components/SegmentedButtons/utils.ts +++ b/src/components/SegmentedButtons/utils.ts @@ -58,28 +58,48 @@ export const getDisabledSegmentedButtonStyle = ({ return {}; }; +const ltrCSSBorders = { + topEndRadius: 'borderTopRightRadius', + bottomEndRadius: 'borderBottomRightRadius', + endWidth: 'borderRightWidth', + topStartRadius: 'borderTopLeftRadius', + bottomStartRadius: 'borderBottomLeftRadius', +}; + +const rtlCSSBorders = { + topEndRadius: 'borderTopLeftRadius', + bottomEndRadius: 'borderBottomLeftRadius', + endWidth: 'borderLeftWidth', + topStartRadius: 'borderTopRightRadius', + bottomStartRadius: 'borderBottomRightRadius', +}; + export const getSegmentedButtonBorderRadius = ({ segment, theme, + overwriteRTL, }: { theme: InternalTheme; segment?: 'first' | 'last'; + overwriteRTL?: boolean; }): ViewStyle => { + const cssBorders = overwriteRTL ? rtlCSSBorders : ltrCSSBorders; + if (segment === 'first') { return { - borderTopRightRadius: 0, - borderBottomRightRadius: 0, - ...(theme.isV3 && { borderRightWidth: 0 }), + [cssBorders.topEndRadius]: 0, + [cssBorders.bottomEndRadius]: 0, + ...(theme.isV3 && { [cssBorders.endWidth]: 0 }), }; } else if (segment === 'last') { return { - borderTopLeftRadius: 0, - borderBottomLeftRadius: 0, + [cssBorders.topStartRadius]: 0, + [cssBorders.bottomStartRadius]: 0, }; } else { return { borderRadius: 0, - ...(theme.isV3 && { borderRightWidth: 0 }), + ...(theme.isV3 && { [cssBorders.endWidth]: 0 }), }; } }; diff --git a/src/components/Snackbar.tsx b/src/components/Snackbar.tsx index 531d1b624e..f58360589e 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/Localization'; import { useInternalTheme } from '../core/theming'; import type { $Omit, $RemoveChildren, ThemeProp } from '../types'; @@ -156,6 +156,7 @@ const Snackbar = ({ ...rest }: Props) => { const theme = useInternalTheme(themeOverrides); + const { direction, localeProps } = useLocale(); const { bottom, right, left } = useSafeAreaInsets(); const { current: opacity } = React.useRef( @@ -243,7 +244,7 @@ const Snackbar = ({ const isIconButton = isV3 && onIconPress; - const marginLeft = action ? -12 : -16; + const marginStart = action ? -12 : -16; const wrapperPaddings = { paddingBottom: bottom, @@ -301,11 +302,12 @@ const Snackbar = ({ ]} testID={testID} {...(isV3 && { elevation })} + {...localeProps} {...rest} > {renderChildrenWithWrapper()} {(action || isIconButton) && ( - + {action ? ( ); expect(getByTestId('compact-button-icon-container')).toHaveStyle({ - marginLeft: 6, - marginRight: 0, + marginStart: 6, + marginEnd: 0, }); }); @@ -211,8 +211,8 @@ describe('button icon styles', () => { ); expect(getByTestId('compact-button-icon-container')).toHaveStyle({ - marginLeft: 8, - marginRight: 0, + marginStart: 8, + marginEnd: 0, }); }) ); @@ -224,8 +224,8 @@ describe('button icon styles', () => { ); expect(getByTestId('compact-button-icon-container')).toHaveStyle({ - marginLeft: 12, - marginRight: -8, + marginStart: 12, + marginEnd: -8, }); }); @@ -238,8 +238,8 @@ describe('button icon styles', () => { ); expect(getByTestId('compact-button-icon-container')).toHaveStyle({ - marginLeft: 16, - marginRight: -16, + marginStart: 16, + marginEnd: -16, }); }) ); diff --git a/src/components/__tests__/Dialog.test.tsx b/src/components/__tests__/Dialog.test.tsx index eef2dd9fea..ffb6094f9d 100644 --- a/src/components/__tests__/Dialog.test.tsx +++ b/src/components/__tests__/Dialog.test.tsx @@ -142,8 +142,8 @@ describe('DialogActions', () => { paddingBottom: 24, paddingHorizontal: 24, }); - expect(dialogActionButtons[0]).toHaveStyle({ marginRight: 8 }); - expect(dialogActionButtons[1]).toHaveStyle({ marginRight: 0 }); + expect(dialogActionButtons[0]).toHaveStyle({ marginEnd: 8 }); + expect(dialogActionButtons[1]).toHaveStyle({ marginEnd: 0 }); }); it('should apply custom styles', () => { diff --git a/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap b/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap index bcd35191f0..50bad5e9bf 100644 --- a/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap +++ b/src/components/__tests__/Drawer/__snapshots__/DrawerSection.test.tsx.snap @@ -21,8 +21,8 @@ exports[`DrawerSection renders properly 1`] = ` }, undefined, { - "marginLeft": 16, - "marginRight": 16, + "marginEnd": 16, + "marginStart": 16, }, { "height": 1, diff --git a/src/components/__tests__/FABGroup.test.tsx b/src/components/__tests__/FABGroup.test.tsx index 0d1019698d..edbad8adbf 100644 --- a/src/components/__tests__/FABGroup.test.tsx +++ b/src/components/__tests__/FABGroup.test.tsx @@ -147,7 +147,7 @@ describe('FABActions - labelStyle - containerStyle', () => { containerStyle: { padding: 16, backgroundColor: '#687456', - marginLeft: 16, + marginStart: 16, }, onPress() {}, icon: '', diff --git a/src/components/__tests__/ListImage.test.tsx b/src/components/__tests__/ListImage.test.tsx index 5248019277..283cc6afa5 100644 --- a/src/components/__tests__/ListImage.test.tsx +++ b/src/components/__tests__/ListImage.test.tsx @@ -13,7 +13,7 @@ const styles = StyleSheet.create({ video: { width: 114, height: 64, - marginLeft: 0, + marginStart: 0, }, container: { width: 30, diff --git a/src/components/__tests__/ListUtils.test.tsx b/src/components/__tests__/ListUtils.test.tsx index 4aeefb6ca9..98fa2b0141 100644 --- a/src/components/__tests__/ListUtils.test.tsx +++ b/src/components/__tests__/ListUtils.test.tsx @@ -6,20 +6,20 @@ import Text from '../Typography/Text'; const styles = StyleSheet.create({ leftItem: { - marginLeft: 0, - marginRight: 16, + marginStart: 0, + marginEnd: 16, }, leftItemV3: { - marginLeft: 16, - marginRight: 0, + marginStart: 16, + marginEnd: 0, alignSelf: 'center', }, rightItem: { - marginRight: 0, + marginEnd: 0, }, rightItemV3: { - marginLeft: 16, - marginRight: 0, + marginStart: 16, + marginEnd: 0, alignSelf: 'center', }, }); diff --git a/src/components/__tests__/Menu.test.tsx b/src/components/__tests__/Menu.test.tsx index b27027b0fc..88c8e62cb0 100644 --- a/src/components/__tests__/Menu.test.tsx +++ b/src/components/__tests__/Menu.test.tsx @@ -103,7 +103,6 @@ it('uses the default anchorPosition of top', async () => { const menu = screen.getByTestId('menu-view'); expect(menu).toHaveStyle({ position: 'absolute', - left: 100, top: 100, }); }); @@ -143,7 +142,6 @@ it('respects anchorPosition bottom', async () => { const menu = screen.getByTestId('menu-view'); expect(menu).toHaveStyle({ position: 'absolute', - left: 100, top: 132, }); }); diff --git a/src/components/__tests__/Searchbar.test.tsx b/src/components/__tests__/Searchbar.test.tsx index 090df9964b..ce6db22d5c 100644 --- a/src/components/__tests__/Searchbar.test.tsx +++ b/src/components/__tests__/Searchbar.test.tsx @@ -124,7 +124,7 @@ it('renders clear icon wrapper, with appropriate style for v3', () => { expect(getByTestId('search-bar-icon-wrapper')).toHaveStyle({ position: 'absolute', right: 0, - marginLeft: 16, + marginStart: 16, }); update( diff --git a/src/components/__tests__/Snackbar.test.tsx b/src/components/__tests__/Snackbar.test.tsx index b9e7eec9e1..e78e1d528c 100644 --- a/src/components/__tests__/Snackbar.test.tsx +++ b/src/components/__tests__/Snackbar.test.tsx @@ -15,7 +15,7 @@ const styles = StyleSheet.create({ backgroundColor: red200, padding: 15, }, - text: { color: white, marginLeft: 10, flexWrap: 'wrap', flexShrink: 1 }, + text: { color: white, marginStart: 10, flexWrap: 'wrap', flexShrink: 1 }, }); // Make sure any animation finishes before checking the snapshot results diff --git a/src/components/__tests__/Surface.test.tsx b/src/components/__tests__/Surface.test.tsx index 8d8e639779..9034b9869b 100644 --- a/src/components/__tests__/Surface.test.tsx +++ b/src/components/__tests__/Surface.test.tsx @@ -76,8 +76,8 @@ describe('Surface', () => { ${'width'} | ${'42%'} ${'height'} | ${'32.5%'} ${'margin'} | ${13} - ${'marginLeft'} | ${13.1} - ${'marginRight'} | ${13.2} + ${'marginStart'} | ${13.1} + ${'marginEnd'} | ${13.2} ${'marginTop'} | ${13.3} ${'marginBottom'} | ${13.4} ${'marginHorizontal'} | ${13.5} @@ -107,8 +107,8 @@ describe('Surface', () => { it.each` property | value ${'padding'} | ${12} - ${'paddingLeft'} | ${12.1} - ${'paddingRight'} | ${12.2} + ${'paddingStart'} | ${12.1} + ${'paddingEnd'} | ${12.2} ${'paddingTop'} | ${12.3} ${'paddingBottom'} | ${12.4} ${'paddingHorizontal'} | ${12.5} diff --git a/src/components/__tests__/TextInput.test.tsx b/src/components/__tests__/TextInput.test.tsx index cf9b4feb83..394a7916bc 100644 --- a/src/components/__tests__/TextInput.test.tsx +++ b/src/components/__tests__/TextInput.test.tsx @@ -1,6 +1,6 @@ /* eslint-disable react-native/no-inline-styles */ import * as React from 'react'; -import { StyleSheet, Text, Platform, I18nManager } from 'react-native'; +import { StyleSheet, Text, Platform } from 'react-native'; import { fireEvent, render } from '@testing-library/react-native'; import color from 'color'; @@ -27,7 +27,7 @@ const style = StyleSheet.create({ lineHeight: 22, }, contentStyle: { - paddingLeft: 20, + paddingStart: 20, }, }); @@ -194,7 +194,7 @@ it('correctly applies a component as the text label', () => { expect(toJSON()).toMatchSnapshot(); }); -it('correctly applies paddingLeft from contentStyleProp', () => { +it('correctly applies paddingStart from contentStyleProp', () => { const { toJSON } = render( { expect(getByTestId('text-input').props.placeholder).toBe(' '); }); -it('correctly applies padding offset to input label on Android when RTL', () => { - Platform.OS = 'android'; - I18nManager.isRTL = true; - - const { getByTestId } = render( - - } - 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', () => { - Platform.OS = 'android'; - - const { getByTestId } = render( - - } - right={ - - } - /> - ); - - expect(getByTestId('text-input-flat-label-active')).toHaveStyle({ - paddingLeft: 16, - paddingRight: 56, - }); -}); - it('calls onLayout on right-side affix adornment', () => { const onLayoutMock = jest.fn(); const nativeEventMock = { diff --git a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap index e304d8827c..7743b34861 100644 --- a/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/BottomNavigation.test.tsx.snap @@ -239,8 +239,8 @@ exports[`allows customizing Route's type via generics 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -481,8 +481,8 @@ exports[`allows customizing Route's type via generics 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -957,8 +957,8 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -1154,8 +1154,8 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -1351,8 +1351,8 @@ exports[`hides labels in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -1707,8 +1707,8 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -1904,8 +1904,8 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -2101,8 +2101,8 @@ exports[`hides labels in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -2588,8 +2588,8 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -2904,8 +2904,8 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -3220,8 +3220,8 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -3536,8 +3536,8 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -3852,8 +3852,8 @@ exports[`renders bottom navigation with getLazy 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -4332,8 +4332,8 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -4598,8 +4598,8 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -4864,8 +4864,8 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -5130,8 +5130,8 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -5396,8 +5396,8 @@ exports[`renders bottom navigation with scene animation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -5750,8 +5750,8 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -5927,8 +5927,8 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -6104,8 +6104,8 @@ exports[`renders custom icon and label in non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -6445,8 +6445,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -6609,8 +6609,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -6773,8 +6773,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -6937,8 +6937,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -7101,8 +7101,8 @@ exports[`renders custom icon and label in shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -7485,8 +7485,8 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -7801,8 +7801,8 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -8117,8 +8117,8 @@ exports[`renders custom icon and label with custom colors in non-shifting bottom style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -8597,8 +8597,8 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -8863,8 +8863,8 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -9129,8 +9129,8 @@ exports[`renders custom icon and label with custom colors in shifting bottom nav style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -9549,8 +9549,8 @@ exports[`renders non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -9865,8 +9865,8 @@ exports[`renders non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -10181,8 +10181,8 @@ exports[`renders non-shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -10661,8 +10661,8 @@ exports[`renders shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -10927,8 +10927,8 @@ exports[`renders shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -11193,8 +11193,8 @@ exports[`renders shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -11459,8 +11459,8 @@ exports[`renders shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, @@ -11725,8 +11725,8 @@ exports[`renders shifting bottom navigation 1`] = ` style={ [ { - "left": 0, "position": "absolute", + "start": 0, }, { "right": 0, diff --git a/src/components/__tests__/__snapshots__/Button.test.tsx.snap b/src/components/__tests__/__snapshots__/Button.test.tsx.snap index c023baed3b..952289dd19 100644 --- a/src/components/__tests__/__snapshots__/Button.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Button.test.tsx.snap @@ -851,16 +851,16 @@ exports[`renders button with icon 1`] = ` style={ [ { - "marginLeft": 12, - "marginRight": -4, + "marginEnd": -4, + "marginStart": 12, }, { - "marginLeft": 16, - "marginRight": -16, + "marginEnd": -16, + "marginStart": 16, }, { - "marginLeft": 12, - "marginRight": -8, + "marginEnd": -8, + "marginStart": 12, }, ] } @@ -1059,16 +1059,16 @@ exports[`renders button with icon in reverse order 1`] = ` style={ [ { - "marginLeft": -4, - "marginRight": 12, + "marginEnd": 12, + "marginStart": -4, }, { - "marginLeft": -16, - "marginRight": 16, + "marginEnd": 16, + "marginStart": -16, }, { - "marginLeft": -8, - "marginRight": 12, + "marginEnd": 12, + "marginStart": -8, }, ] } @@ -1578,16 +1578,16 @@ exports[`renders loading button 1`] = ` }, [ { - "marginLeft": 12, - "marginRight": -4, + "marginEnd": -4, + "marginStart": 12, }, { - "marginLeft": 16, - "marginRight": -16, + "marginEnd": -16, + "marginStart": 16, }, { - "marginLeft": 12, - "marginRight": -8, + "marginEnd": -8, + "marginStart": 12, }, ], ] diff --git a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap index f75512145c..ac57550e7b 100644 --- a/src/components/__tests__/__snapshots__/Chip.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/Chip.test.tsx.snap @@ -20,6 +20,7 @@ exports[`renders chip with close button 1`] = ` > @@ -244,10 +245,10 @@ exports[`renders chip with close button 1`] = ` "padding": 4, }, { - "marginRight": 4, + "marginEnd": 4, }, { - "marginRight": 8, + "marginEnd": 8, "padding": 0, }, ] @@ -315,6 +316,7 @@ exports[`renders chip with custom close button 1`] = ` > @@ -539,10 +541,10 @@ exports[`renders chip with custom close button 1`] = ` "padding": 4, }, { - "marginRight": 4, + "marginEnd": 4, }, { - "marginRight": 8, + "marginEnd": 8, "padding": 0, }, ] @@ -610,6 +612,7 @@ exports[`renders chip with icon 1`] = ` > @@ -523,7 +530,7 @@ exports[`renders list item with left and right items 1`] = ` style={ [ { - "paddingLeft": 16, + "paddingStart": 16, }, { "flexGrow": 1, @@ -607,8 +614,8 @@ exports[`renders list item with left and right items 1`] = ` }, { "alignSelf": "center", - "marginLeft": 16, - "marginRight": 0, + "marginEnd": 0, + "marginStart": 16, }, ] } @@ -690,7 +697,7 @@ exports[`renders list item with left item 1`] = ` false, [ { - "paddingRight": 24, + "paddingEnd": 24, "paddingVertical": 8, }, undefined, @@ -700,11 +707,13 @@ exports[`renders list item with left item 1`] = ` > @@ -307,7 +307,7 @@ exports[`renders snackbar with action button 1`] = ` "minHeight": 48, }, { - "marginLeft": -12, + "marginStart": -12, }, ] } @@ -318,8 +318,8 @@ exports[`renders snackbar with action button 1`] = ` { "backgroundColor": "transparent", "borderRadius": 20, - "marginLeft": 4, - "marginRight": 8, + "marginEnd": 8, + "marginStart": 4, "shadowColor": "#000", "shadowOffset": { "height": 0, diff --git a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap index d89ec4b1e0..66abecb40d 100644 --- a/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap +++ b/src/components/__tests__/__snapshots__/TextInput.test.tsx.snap @@ -818,7 +818,7 @@ exports[`correctly applies height to multiline Outline TextInput 1`] = ` `; -exports[`correctly applies paddingLeft from contentStyleProp 1`] = ` +exports[`correctly applies paddingStart from contentStyleProp 1`] = ` (defaultDirection); +const isWeb = Platform.OS === 'web'; + +const useLocale = () => { + const direction = useContext(context); + const localeProps = direction === 'rtl' ? { dir: 'rtl' } : {}; + + // Since I18nManager is mocked in react-native-web (https://github.com/necolas/react-native-web/releases/tag/0.18.0) + // we have to rely on default react-native-web positioning for RTL languages. + // Most of the time this works out of the box, but in few specific cases + // we need to overwrite right/left ourselves. + const overwriteRTL = isWeb && direction === 'rtl'; + + return { + direction, + localeProps, + overwriteRTL, + }; +}; + +export const { Provider: LocalizationProvider } = context; + +export { Direction, useLocale }; diff --git a/src/core/PaperProvider.tsx b/src/core/PaperProvider.tsx index 355818a957..32621d2c22 100644 --- a/src/core/PaperProvider.tsx +++ b/src/core/PaperProvider.tsx @@ -3,9 +3,12 @@ import { AccessibilityInfo, Appearance, ColorSchemeName, + I18nManager, NativeEventSubscription, + Platform, } from 'react-native'; +import { Direction, LocalizationProvider } from './Localization'; import SafeAreaProviderCompat from './SafeAreaProviderCompat'; import { Provider as SettingsProvider, Settings } from './settings'; import { defaultThemesByVersion, ThemeProvider } from './theming'; @@ -18,6 +21,7 @@ export type Props = { children: React.ReactNode; theme?: ThemeProp; settings?: Settings; + direction?: Direction; }; const PaperProvider = (props: Props) => { @@ -76,6 +80,25 @@ const PaperProvider = (props: Props) => { }; }, [props.theme, isOnlyVersionInTheme]); + React.useEffect(() => { + if (!props.direction || !['rtl', 'ltr'].includes(props.direction)) { + return; + } + + const isRTL = props.direction === 'rtl'; + + if (Platform.OS === 'web') { + const htmlDir = document.documentElement.getAttribute('dir'); + if (isRTL && htmlDir !== 'rtl') { + document.documentElement.setAttribute('dir', 'rtl'); + } + } else { + if (isRTL && !I18nManager.isRTL) { + I18nManager.forceRTL(isRTL); + } + } + }, [props.direction]); + const getTheme = () => { const themeVersion = props.theme?.version || 3; const scheme = colorScheme || 'light'; @@ -97,21 +120,27 @@ const PaperProvider = (props: Props) => { }; }; - const { children, settings } = props; + const { + children, + settings, + direction = I18nManager.getConstants().isRTL ? 'rtl' : 'ltr', + } = props; return ( - - - {children} - - + + + + {children} + + + ); }; diff --git a/src/react-navigation/views/MaterialBottomTabView.tsx b/src/react-navigation/views/MaterialBottomTabView.tsx index df846ae2fb..b734303678 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/Localization'; import type { MaterialBottomTabDescriptorMap, MaterialBottomTabNavigationConfig, @@ -29,6 +30,7 @@ export default function MaterialBottomTabView({ descriptors, ...rest }: Props) { + const { direction } = useLocale(); const buildLink = useLinkBuilder(); return ( @@ -76,7 +78,7 @@ export default function MaterialBottomTabView({ if (typeof options.tabBarIcon === 'string') { return ( { backgroundColor: 'red', marginTop: 1, marginBottom: 2, - marginLeft: 3, + marginStart: 3, padding: 4, borderTopLeftRadius: 5, borderTopRightRadius: 6,