Skip to content

Commit badf23f

Browse files
authored
Merge pull request #203 from sendbird/feat/rtl
[CLNP-5218] feat: RTL support
2 parents 44cabe5 + 6148d13 commit badf23f

File tree

115 files changed

+1227
-689
lines changed

Some content is hidden

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

115 files changed

+1227
-689
lines changed
88 Bytes
Loading
303 Bytes
Loading
441 Bytes
Loading

packages/uikit-react-native-foundation/src/components/Box/index.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ import { isFunction } from '@sendbird/uikit-utils';
66
import useUIKitTheme from '../../theme/useUIKitTheme';
77
import type { UIKitColors, UIKitPalette } from '../../types';
88

9+
type DeprecatedBoxProps = {
10+
/** @deprecated Please use `paddingStart` instead **/
11+
paddingLeft?: ViewStyle['paddingLeft'];
12+
/** @deprecated Please use `paddingEnd` instead **/
13+
paddingRight?: ViewStyle['paddingRight'];
14+
/** @deprecated Please use `marginStart` instead **/
15+
marginLeft?: ViewStyle['marginLeft'];
16+
/** @deprecated Please use `marginEnd` instead **/
17+
marginRight?: ViewStyle['marginRight'];
18+
};
19+
920
type BaseBoxProps = Pick<
1021
ViewStyle,
1122
| 'flex'
@@ -22,15 +33,15 @@ type BaseBoxProps = Pick<
2233
| 'margin'
2334
| 'marginHorizontal'
2435
| 'marginVertical'
25-
| 'marginLeft'
26-
| 'marginRight'
36+
| 'marginStart'
37+
| 'marginEnd'
2738
| 'marginTop'
2839
| 'marginBottom'
2940
| 'padding'
3041
| 'paddingHorizontal'
3142
| 'paddingVertical'
32-
| 'paddingLeft'
33-
| 'paddingRight'
43+
| 'paddingStart'
44+
| 'paddingEnd'
3445
| 'paddingTop'
3546
| 'paddingBottom'
3647
| 'overflow'
@@ -40,7 +51,7 @@ type BaseBoxProps = Pick<
4051
backgroundColor?: string | ((theme: { colors: UIKitColors; palette: UIKitPalette }) => string);
4152
};
4253

43-
type BoxProps = BaseBoxProps & ViewProps;
54+
type BoxProps = BaseBoxProps & DeprecatedBoxProps & ViewProps;
4455
const Box = ({ style, children, ...props }: BoxProps) => {
4556
const boxStyle = useBoxStyle(props);
4657

packages/uikit-react-native-foundation/src/components/Icon/index.tsx

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react';
2-
import { Image, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
2+
import { I18nManager, Image, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
33

44
import { FileType, convertFileTypeToMessageType, getFileIconFromMessageType } from '@sendbird/uikit-utils';
55

@@ -10,23 +10,55 @@ import useUIKitTheme from '../../theme/useUIKitTheme';
1010
type IconNames = keyof typeof IconAssets;
1111
type SizeFactor = keyof typeof sizeStyles;
1212

13+
const mirroredIcons: Partial<Record<IconNames, boolean>> = {
14+
create: true,
15+
send: true,
16+
reply: true,
17+
'reply-filled': true,
18+
thread: true,
19+
chat: true,
20+
'chat-filled': true,
21+
message: true,
22+
broadcast: true,
23+
'file-audio': true,
24+
'arrow-left': true,
25+
leave: true,
26+
'chevron-right': true,
27+
};
28+
1329
type Props = {
1430
icon: IconNames;
1531
color?: string;
1632
size?: number;
1733
style?: StyleProp<ImageStyle>;
1834
containerStyle?: StyleProp<ViewStyle>;
35+
direction?: 'ltr' | 'rtl';
1936
};
2037

21-
const Icon = ({ icon, color, size = 24, containerStyle, style }: Props) => {
38+
const Icon = ({
39+
icon,
40+
color,
41+
size = 24,
42+
containerStyle,
43+
style,
44+
direction = I18nManager.isRTL ? 'rtl' : 'ltr',
45+
}: Props) => {
2246
const sizeStyle = sizeStyles[size as SizeFactor] ?? { width: size, height: size };
2347
const { colors } = useUIKitTheme();
48+
49+
const shouldMirror = direction === 'rtl' && mirroredIcons[icon];
50+
2451
return (
2552
<View style={[containerStyle, containerStyles.container]}>
2653
<Image
2754
resizeMode={'contain'}
2855
source={IconAssets[icon]}
29-
style={[{ tintColor: color ?? colors.primary }, sizeStyle, style]}
56+
style={[
57+
{ tintColor: color ?? colors.primary },
58+
sizeStyle,
59+
shouldMirror && { transform: [{ scaleX: -1 }] },
60+
style,
61+
]}
3062
/>
3163
</View>
3264
);

packages/uikit-react-native-foundation/src/components/ProgressBar/index.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { ReactNode, useEffect, useRef } from 'react';
2-
import { Animated, Easing, StyleSheet, ViewStyle } from 'react-native';
2+
import { Animated, Easing, I18nManager, StyleSheet, ViewStyle } from 'react-native';
33

44
import { NOOP } from '@sendbird/uikit-utils';
55

@@ -42,6 +42,14 @@ const ProgressBar = ({ current = 100, total = 100, trackColor, barColor, overlay
4242
return NOOP;
4343
}, [percent]);
4444

45+
const progressBarPosition = (() => {
46+
if (I18nManager.isRTL && I18nManager.doLeftAndRightSwapInRTL) {
47+
return { right: 0 };
48+
}
49+
50+
return { left: 0 };
51+
})();
52+
4553
return (
4654
<Box
4755
height={36}
@@ -52,17 +60,20 @@ const ProgressBar = ({ current = 100, total = 100, trackColor, barColor, overlay
5260
style={style}
5361
>
5462
<Animated.View
55-
style={{
56-
position: 'absolute',
57-
width: progress.interpolate({
58-
inputRange: [0, 1],
59-
outputRange: ['0%', '100%'],
60-
extrapolate: 'clamp',
61-
}),
62-
height: '100%',
63-
opacity: 0.38,
64-
backgroundColor: uiColors.bar,
65-
}}
63+
style={[
64+
progressBarPosition,
65+
{
66+
position: 'absolute',
67+
width: progress.interpolate({
68+
inputRange: [0, 1],
69+
outputRange: ['0%', '100%'],
70+
extrapolate: 'clamp',
71+
}),
72+
height: '100%',
73+
opacity: 0.38,
74+
backgroundColor: uiColors.bar,
75+
},
76+
]}
6677
/>
6778
<Box style={StyleSheet.absoluteFill}>{overlay}</Box>
6879
</Box>

packages/uikit-react-native-foundation/src/components/Switch/index.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useEffect, useRef } from 'react';
2-
import { Animated, Platform, Pressable } from 'react-native';
2+
import { Animated, I18nManager, Platform, Pressable } from 'react-native';
33

44
import createStyleSheet from '../../styles/createStyleSheet';
55
import useUIKitTheme from '../../theme/useUIKitTheme';
@@ -24,9 +24,12 @@ const Switch = ({
2424
const { select, palette, colors } = useUIKitTheme();
2525
const position = useRef(new Animated.Value(0)).current;
2626

27+
const start = I18nManager.isRTL ? styles.thumbOn.start : styles.thumbOff.start;
28+
const end = I18nManager.isRTL ? styles.thumbOff.start : styles.thumbOn.start;
29+
2730
useEffect(() => {
2831
const animation = Animated.timing(position, {
29-
toValue: value ? styles.thumbOn.left : styles.thumbOff.left,
32+
toValue: value ? end : start,
3033
duration: 150,
3134
useNativeDriver: false,
3235
});
@@ -36,11 +39,12 @@ const Switch = ({
3639

3740
const createInterpolate = <T extends string>(offValue: T, onValue: T) => {
3841
return position.interpolate({
39-
inputRange: [styles.thumbOff.left, styles.thumbOn.left],
40-
outputRange: [offValue, onValue],
42+
inputRange: [styles.thumbOff.start, styles.thumbOn.start],
43+
outputRange: I18nManager.isRTL ? [onValue, offValue] : [offValue, onValue],
4144
extrapolate: 'clamp',
4245
});
4346
};
47+
4448
const _trackColor = createInterpolate(inactiveTrackColor ?? colors.onBackground04, trackColor ?? palette.primary200);
4549
const _thumbColor = createInterpolate(
4650
inactiveThumbColor ?? palette.background300,
@@ -86,10 +90,10 @@ const styles = createStyleSheet({
8690
}),
8791
},
8892
thumbOn: {
89-
left: OFFSET.H / 2,
93+
start: OFFSET.H / 2,
9094
},
9195
thumbOff: {
92-
left: -OFFSET.H / 2,
96+
start: -OFFSET.H / 2,
9397
},
9498
});
9599

packages/uikit-react-native-foundation/src/components/Text/index.tsx

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,60 @@
11
import React from 'react';
2-
import { Text as RNText, TextProps as RNTextProps } from 'react-native';
2+
import { I18nManager, Text as RNText, TextProps as RNTextProps, StyleSheet, TextStyle } from 'react-native';
3+
4+
import { isStartsWithRTL } from '@sendbird/uikit-utils';
35

46
import useUIKitTheme from '../../theme/useUIKitTheme';
57
import type { TypoName, UIKitTheme } from '../../types';
68

9+
export interface RTLTextAlignSupportProps {
10+
/**
11+
* If `I18nManager.isRTL` is `true` and this value is enabled, the text will be aligned according to RTL if it starts in an RTL language.
12+
* In the case of the `Text` component, the alignment value is calculated based on `I18nManager.doLeftAndRightSwapInRTL`.
13+
* For the `TextInput` component, the alignment value is calculated as a physical alignment, unaffected by `I18nManager.doLeftAndRightSwapInRTL`.
14+
*/
15+
supportRTLAlign?: boolean;
16+
/**
17+
* If you want to enable `supportRTLAlign` but are using nested `Text` components that are not simple text under the `Text` component, pass the original text here.
18+
*/
19+
originalText?: string;
20+
}
21+
722
type TypographyProps = Partial<Record<TypoName, boolean>>;
8-
export type TextProps = RNTextProps & TypographyProps & { color?: ((colors: UIKitTheme['colors']) => string) | string };
9-
const Text = ({ children, color, style, ...props }: TextProps) => {
23+
export type TextProps = RNTextProps &
24+
TypographyProps & { color?: ((colors: UIKitTheme['colors']) => string) | string } & RTLTextAlignSupportProps;
25+
26+
const Text = ({ children, color, style, supportRTLAlign = true, originalText, ...props }: TextProps) => {
1027
const { colors } = useUIKitTheme();
1128
const typoStyle = useTypographyFilter(props);
29+
30+
const textStyle = StyleSheet.flatten([
31+
{ color: typeof color === 'string' ? color : color?.(colors) ?? colors.text },
32+
typoStyle,
33+
style,
34+
]) as TextStyle;
35+
36+
const textAlign = (() => {
37+
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
38+
return textStyle.textAlign;
39+
}
40+
41+
if (I18nManager.isRTL && supportRTLAlign) {
42+
if (
43+
(originalText && isStartsWithRTL(originalText)) ||
44+
(typeof children === 'string' && isStartsWithRTL(children))
45+
) {
46+
return I18nManager.doLeftAndRightSwapInRTL ? 'left' : 'right';
47+
} else {
48+
return I18nManager.doLeftAndRightSwapInRTL ? 'right' : 'left';
49+
}
50+
}
51+
52+
if (textStyle.textAlign) return textStyle.textAlign;
53+
return undefined;
54+
})();
55+
1256
return (
13-
<RNText
14-
style={[{ color: typeof color === 'string' ? color : color?.(colors) ?? colors.text }, typoStyle, style]}
15-
{...props}
16-
>
57+
<RNText style={[textStyle, { textAlign }]} {...props}>
1758
{children}
1859
</RNText>
1960
);

packages/uikit-react-native-foundation/src/components/TextInput/index.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import React from 'react';
2-
import { TextInput as RNTextInput, TextInputProps } from 'react-native';
2+
import { I18nManager, TextInput as RNTextInput, StyleSheet, TextInputProps, TextStyle } from 'react-native';
3+
4+
import { isStartsWithRTL } from '@sendbird/uikit-utils';
35

46
import createStyleSheet from '../../styles/createStyleSheet';
57
import useUIKitTheme from '../../theme/useUIKitTheme';
68
import type { UIKitTheme } from '../../types';
9+
import { RTLTextAlignSupportProps } from '../Text';
710

8-
type Props = { variant?: keyof UIKitTheme['colors']['ui']['input'] } & TextInputProps;
11+
type Props = {
12+
variant?: keyof UIKitTheme['colors']['ui']['input'];
13+
} & TextInputProps &
14+
RTLTextAlignSupportProps;
915
const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
10-
{ children, style, variant = 'default', editable = true, ...props },
16+
{ children, style, variant = 'default', editable = true, originalText, supportRTLAlign = true, ...props },
1117
ref,
1218
) {
1319
const { typography, colors } = useUIKitTheme();
@@ -20,19 +26,40 @@ const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
2026
lineHeight: typography.body3.fontSize ? typography.body3.fontSize * 1.2 : undefined,
2127
};
2228

29+
const textStyle = StyleSheet.flatten([
30+
fontStyle,
31+
styles.input,
32+
{ color: inputStyle.text, backgroundColor: inputStyle.background },
33+
underlineStyle,
34+
style,
35+
]) as TextStyle;
36+
37+
const textAlign = (() => {
38+
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
39+
return textStyle.textAlign;
40+
}
41+
42+
if (I18nManager.isRTL && supportRTLAlign) {
43+
const text = originalText || props.value || props.placeholder;
44+
// Note: TextInput is not affected by doLeftAndRightSwapInRTL
45+
if (text && isStartsWithRTL(text)) {
46+
return 'right';
47+
} else {
48+
return 'left';
49+
}
50+
}
51+
52+
if (textStyle.textAlign) return textStyle.textAlign;
53+
return undefined;
54+
})();
55+
2356
return (
2457
<RNTextInput
2558
ref={ref}
2659
editable={editable}
2760
selectionColor={inputStyle.highlight}
2861
placeholderTextColor={inputStyle.placeholder}
29-
style={[
30-
fontStyle,
31-
styles.input,
32-
{ color: inputStyle.text, backgroundColor: inputStyle.background },
33-
underlineStyle,
34-
style,
35-
]}
62+
style={[textStyle, { textAlign }]}
3663
{...props}
3764
>
3865
{children}
@@ -45,8 +72,8 @@ const styles = createStyleSheet({
4572
includeFontPadding: false,
4673
paddingTop: 8,
4774
paddingBottom: 8,
48-
paddingLeft: 16,
49-
paddingRight: 16,
75+
paddingStart: 16,
76+
paddingEnd: 16,
5077
},
5178
});
5279

packages/uikit-react-native-foundation/src/styles/createStyleSheet.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,17 +35,23 @@ const preProcessor: Partial<StylePreprocessor> = {
3535
'paddingBottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
3636
'paddingLeft': SCALE_FACTOR_WITH_DIMENSION_VALUE,
3737
'paddingRight': SCALE_FACTOR_WITH_DIMENSION_VALUE,
38+
'paddingStart': SCALE_FACTOR_WITH_DIMENSION_VALUE,
39+
'paddingEnd': SCALE_FACTOR_WITH_DIMENSION_VALUE,
3840
'margin': SCALE_FACTOR_WITH_DIMENSION_VALUE,
3941
'marginVertical': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4042
'marginHorizontal': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4143
'marginTop': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4244
'marginBottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4345
'marginLeft': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4446
'marginRight': SCALE_FACTOR_WITH_DIMENSION_VALUE,
45-
'left': SCALE_FACTOR_WITH_DIMENSION_VALUE,
46-
'right': SCALE_FACTOR_WITH_DIMENSION_VALUE,
47+
'marginStart': SCALE_FACTOR_WITH_DIMENSION_VALUE,
48+
'marginEnd': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4749
'top': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4850
'bottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
51+
'left': SCALE_FACTOR_WITH_DIMENSION_VALUE,
52+
'right': SCALE_FACTOR_WITH_DIMENSION_VALUE,
53+
'start': SCALE_FACTOR_WITH_DIMENSION_VALUE,
54+
'end': SCALE_FACTOR_WITH_DIMENSION_VALUE,
4955
};
5056

5157
const preProcessorKeys = Object.keys(preProcessor);

0 commit comments

Comments
 (0)