Skip to content

Commit

Permalink
Merge pull request #203 from sendbird/feat/rtl
Browse files Browse the repository at this point in the history
[CLNP-5218] feat: RTL support
  • Loading branch information
bang9 authored Nov 21, 2024
2 parents 44cabe5 + 6148d13 commit badf23f
Show file tree
Hide file tree
Showing 115 changed files with 1,227 additions and 689 deletions.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ import { isFunction } from '@sendbird/uikit-utils';
import useUIKitTheme from '../../theme/useUIKitTheme';
import type { UIKitColors, UIKitPalette } from '../../types';

type DeprecatedBoxProps = {
/** @deprecated Please use `paddingStart` instead **/
paddingLeft?: ViewStyle['paddingLeft'];
/** @deprecated Please use `paddingEnd` instead **/
paddingRight?: ViewStyle['paddingRight'];
/** @deprecated Please use `marginStart` instead **/
marginLeft?: ViewStyle['marginLeft'];
/** @deprecated Please use `marginEnd` instead **/
marginRight?: ViewStyle['marginRight'];
};

type BaseBoxProps = Pick<
ViewStyle,
| 'flex'
Expand All @@ -22,15 +33,15 @@ type BaseBoxProps = Pick<
| 'margin'
| 'marginHorizontal'
| 'marginVertical'
| 'marginLeft'
| 'marginRight'
| 'marginStart'
| 'marginEnd'
| 'marginTop'
| 'marginBottom'
| 'padding'
| 'paddingHorizontal'
| 'paddingVertical'
| 'paddingLeft'
| 'paddingRight'
| 'paddingStart'
| 'paddingEnd'
| 'paddingTop'
| 'paddingBottom'
| 'overflow'
Expand All @@ -40,7 +51,7 @@ type BaseBoxProps = Pick<
backgroundColor?: string | ((theme: { colors: UIKitColors; palette: UIKitPalette }) => string);
};

type BoxProps = BaseBoxProps & ViewProps;
type BoxProps = BaseBoxProps & DeprecatedBoxProps & ViewProps;
const Box = ({ style, children, ...props }: BoxProps) => {
const boxStyle = useBoxStyle(props);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { Image, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';
import { I18nManager, Image, ImageStyle, StyleProp, StyleSheet, View, ViewStyle } from 'react-native';

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

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

const mirroredIcons: Partial<Record<IconNames, boolean>> = {
create: true,
send: true,
reply: true,
'reply-filled': true,
thread: true,
chat: true,
'chat-filled': true,
message: true,
broadcast: true,
'file-audio': true,
'arrow-left': true,
leave: true,
'chevron-right': true,
};

type Props = {
icon: IconNames;
color?: string;
size?: number;
style?: StyleProp<ImageStyle>;
containerStyle?: StyleProp<ViewStyle>;
direction?: 'ltr' | 'rtl';
};

const Icon = ({ icon, color, size = 24, containerStyle, style }: Props) => {
const Icon = ({
icon,
color,
size = 24,
containerStyle,
style,
direction = I18nManager.isRTL ? 'rtl' : 'ltr',
}: Props) => {
const sizeStyle = sizeStyles[size as SizeFactor] ?? { width: size, height: size };
const { colors } = useUIKitTheme();

const shouldMirror = direction === 'rtl' && mirroredIcons[icon];

return (
<View style={[containerStyle, containerStyles.container]}>
<Image
resizeMode={'contain'}
source={IconAssets[icon]}
style={[{ tintColor: color ?? colors.primary }, sizeStyle, style]}
style={[
{ tintColor: color ?? colors.primary },
sizeStyle,
shouldMirror && { transform: [{ scaleX: -1 }] },
style,
]}
/>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { ReactNode, useEffect, useRef } from 'react';
import { Animated, Easing, StyleSheet, ViewStyle } from 'react-native';
import { Animated, Easing, I18nManager, StyleSheet, ViewStyle } from 'react-native';

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

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

const progressBarPosition = (() => {
if (I18nManager.isRTL && I18nManager.doLeftAndRightSwapInRTL) {
return { right: 0 };
}

return { left: 0 };
})();

return (
<Box
height={36}
Expand All @@ -52,17 +60,20 @@ const ProgressBar = ({ current = 100, total = 100, trackColor, barColor, overlay
style={style}
>
<Animated.View
style={{
position: 'absolute',
width: progress.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
extrapolate: 'clamp',
}),
height: '100%',
opacity: 0.38,
backgroundColor: uiColors.bar,
}}
style={[
progressBarPosition,
{
position: 'absolute',
width: progress.interpolate({
inputRange: [0, 1],
outputRange: ['0%', '100%'],
extrapolate: 'clamp',
}),
height: '100%',
opacity: 0.38,
backgroundColor: uiColors.bar,
},
]}
/>
<Box style={StyleSheet.absoluteFill}>{overlay}</Box>
</Box>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from 'react';
import { Animated, Platform, Pressable } from 'react-native';
import { Animated, I18nManager, Platform, Pressable } from 'react-native';

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

const start = I18nManager.isRTL ? styles.thumbOn.start : styles.thumbOff.start;
const end = I18nManager.isRTL ? styles.thumbOff.start : styles.thumbOn.start;

useEffect(() => {
const animation = Animated.timing(position, {
toValue: value ? styles.thumbOn.left : styles.thumbOff.left,
toValue: value ? end : start,
duration: 150,
useNativeDriver: false,
});
Expand All @@ -36,11 +39,12 @@ const Switch = ({

const createInterpolate = <T extends string>(offValue: T, onValue: T) => {
return position.interpolate({
inputRange: [styles.thumbOff.left, styles.thumbOn.left],
outputRange: [offValue, onValue],
inputRange: [styles.thumbOff.start, styles.thumbOn.start],
outputRange: I18nManager.isRTL ? [onValue, offValue] : [offValue, onValue],
extrapolate: 'clamp',
});
};

const _trackColor = createInterpolate(inactiveTrackColor ?? colors.onBackground04, trackColor ?? palette.primary200);
const _thumbColor = createInterpolate(
inactiveThumbColor ?? palette.background300,
Expand Down Expand Up @@ -86,10 +90,10 @@ const styles = createStyleSheet({
}),
},
thumbOn: {
left: OFFSET.H / 2,
start: OFFSET.H / 2,
},
thumbOff: {
left: -OFFSET.H / 2,
start: -OFFSET.H / 2,
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,60 @@
import React from 'react';
import { Text as RNText, TextProps as RNTextProps } from 'react-native';
import { I18nManager, Text as RNText, TextProps as RNTextProps, StyleSheet, TextStyle } from 'react-native';

import { isStartsWithRTL } from '@sendbird/uikit-utils';

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

export interface RTLTextAlignSupportProps {
/**
* 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.
* In the case of the `Text` component, the alignment value is calculated based on `I18nManager.doLeftAndRightSwapInRTL`.
* For the `TextInput` component, the alignment value is calculated as a physical alignment, unaffected by `I18nManager.doLeftAndRightSwapInRTL`.
*/
supportRTLAlign?: boolean;
/**
* 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.
*/
originalText?: string;
}

type TypographyProps = Partial<Record<TypoName, boolean>>;
export type TextProps = RNTextProps & TypographyProps & { color?: ((colors: UIKitTheme['colors']) => string) | string };
const Text = ({ children, color, style, ...props }: TextProps) => {
export type TextProps = RNTextProps &
TypographyProps & { color?: ((colors: UIKitTheme['colors']) => string) | string } & RTLTextAlignSupportProps;

const Text = ({ children, color, style, supportRTLAlign = true, originalText, ...props }: TextProps) => {
const { colors } = useUIKitTheme();
const typoStyle = useTypographyFilter(props);

const textStyle = StyleSheet.flatten([
{ color: typeof color === 'string' ? color : color?.(colors) ?? colors.text },
typoStyle,
style,
]) as TextStyle;

const textAlign = (() => {
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
return textStyle.textAlign;
}

if (I18nManager.isRTL && supportRTLAlign) {
if (
(originalText && isStartsWithRTL(originalText)) ||
(typeof children === 'string' && isStartsWithRTL(children))
) {
return I18nManager.doLeftAndRightSwapInRTL ? 'left' : 'right';
} else {
return I18nManager.doLeftAndRightSwapInRTL ? 'right' : 'left';
}
}

if (textStyle.textAlign) return textStyle.textAlign;
return undefined;
})();

return (
<RNText
style={[{ color: typeof color === 'string' ? color : color?.(colors) ?? colors.text }, typoStyle, style]}
{...props}
>
<RNText style={[textStyle, { textAlign }]} {...props}>
{children}
</RNText>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import React from 'react';
import { TextInput as RNTextInput, TextInputProps } from 'react-native';
import { I18nManager, TextInput as RNTextInput, StyleSheet, TextInputProps, TextStyle } from 'react-native';

import { isStartsWithRTL } from '@sendbird/uikit-utils';

import createStyleSheet from '../../styles/createStyleSheet';
import useUIKitTheme from '../../theme/useUIKitTheme';
import type { UIKitTheme } from '../../types';
import { RTLTextAlignSupportProps } from '../Text';

type Props = { variant?: keyof UIKitTheme['colors']['ui']['input'] } & TextInputProps;
type Props = {
variant?: keyof UIKitTheme['colors']['ui']['input'];
} & TextInputProps &
RTLTextAlignSupportProps;
const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
{ children, style, variant = 'default', editable = true, ...props },
{ children, style, variant = 'default', editable = true, originalText, supportRTLAlign = true, ...props },
ref,
) {
const { typography, colors } = useUIKitTheme();
Expand All @@ -20,19 +26,40 @@ const TextInput = React.forwardRef<RNTextInput, Props>(function TextInput(
lineHeight: typography.body3.fontSize ? typography.body3.fontSize * 1.2 : undefined,
};

const textStyle = StyleSheet.flatten([
fontStyle,
styles.input,
{ color: inputStyle.text, backgroundColor: inputStyle.background },
underlineStyle,
style,
]) as TextStyle;

const textAlign = (() => {
if (textStyle.textAlign && textStyle.textAlign !== 'left' && textStyle.textAlign !== 'right') {
return textStyle.textAlign;
}

if (I18nManager.isRTL && supportRTLAlign) {
const text = originalText || props.value || props.placeholder;
// Note: TextInput is not affected by doLeftAndRightSwapInRTL
if (text && isStartsWithRTL(text)) {
return 'right';
} else {
return 'left';
}
}

if (textStyle.textAlign) return textStyle.textAlign;
return undefined;
})();

return (
<RNTextInput
ref={ref}
editable={editable}
selectionColor={inputStyle.highlight}
placeholderTextColor={inputStyle.placeholder}
style={[
fontStyle,
styles.input,
{ color: inputStyle.text, backgroundColor: inputStyle.background },
underlineStyle,
style,
]}
style={[textStyle, { textAlign }]}
{...props}
>
{children}
Expand All @@ -45,8 +72,8 @@ const styles = createStyleSheet({
includeFontPadding: false,
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
paddingRight: 16,
paddingStart: 16,
paddingEnd: 16,
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,23 @@ const preProcessor: Partial<StylePreprocessor> = {
'paddingBottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'paddingLeft': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'paddingRight': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'paddingStart': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'paddingEnd': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'margin': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginVertical': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginHorizontal': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginTop': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginBottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginLeft': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginRight': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'left': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'right': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginStart': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'marginEnd': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'top': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'bottom': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'left': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'right': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'start': SCALE_FACTOR_WITH_DIMENSION_VALUE,
'end': SCALE_FACTOR_WITH_DIMENSION_VALUE,
};

const preProcessorKeys = Object.keys(preProcessor);
Expand Down
Loading

0 comments on commit badf23f

Please sign in to comment.