Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/added foundation components (2) #15

Merged
merged 7 commits into from
Mar 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
feat(foundation): added BottomSheet component
  • Loading branch information
bang9 committed Mar 3, 2022
commit c208710eea9f8a7c165fa7af820a1972965cb75e
1 change: 1 addition & 0 deletions packages/uikit-react-native-foundation/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export { default as ActionMenu } from './ui/ActionMenu';
export { default as Alert } from './ui/Alert';
export { default as Avatar } from './ui/Avatar';
export { default as Badge } from './ui/Badge';
export { default as BottomSheet } from './ui/BottomSheet';
export { default as Button } from './ui/Button';
export { DialogProvider, useActionMenu, useAlert, usePrompt } from './ui/Dialog';
export { default as Header } from './ui/Header';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,7 @@ type Props = {
onHide: () => void;
onError?: (error: unknown) => void;
onDismiss?: () => void;

title?: ActionMenuItem['title'];
menuItems: ActionMenuItem['menuItems'];
};
} & ActionMenuItem;
const ActionMenu: React.FC<Props> = ({ visible, onHide, onError, onDismiss, title, menuItems }) => {
const { statusBarTranslucent } = useHeaderStyle();
const { colors } = useUIKitTheme();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React from 'react';
import { Pressable, useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';

import useHeaderStyle from '../../styles/useHeaderStyle';
import DialogSheet from '../Dialog/DialogSheet';
import type Icon from '../Icon';
import Modal from '../Modal';

export type BottomSheetItem = {
sheetItems: {
icon?: keyof typeof Icon.Assets;
iconColor?: string;
title: string;
titleColor?: string;
onPress: () => void;
}[];
};
type Props = {
visible: boolean;
onHide: () => void;
onError?: (error: unknown) => void;
onDismiss?: () => void;
} & BottomSheetItem;
const BottomSheet: React.FC<Props> = ({ onDismiss, onHide, visible, sheetItems }) => {
const { statusBarTranslucent } = useHeaderStyle();
const { width } = useWindowDimensions();
const { bottom, left, right } = useSafeAreaInsets();
return (
<Modal
type={'slide'}
onClose={onHide}
onDismiss={onDismiss}
statusBarTranslucent={statusBarTranslucent}
visible={visible}
backgroundStyle={{ alignItems: 'center', justifyContent: 'flex-end' }}
>
<DialogSheet style={{ width, paddingBottom: bottom }}>
{sheetItems.map(({ onPress, ...props }, idx) => (
<Pressable
key={props.title + idx}
style={{ paddingLeft: left, paddingRight: right }}
onPress={() => {
try {
onPress();
} finally {
onHide();
}
}}
>
<DialogSheet.Item {...props} />
</Pressable>
))}
</DialogSheet>
</Modal>
);
};

export default BottomSheet;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';

import createStyleSheet from '../../styles/createStyleSheet';
import useUIKitTheme from '../../theme/useUIKitTheme';
import Icon from '../Icon';
import Text from '../Text';

type Props = {
style?: StyleProp<ViewStyle>;
};
const DialogSheet: React.FC<Props> & { Item: typeof SheetItem } = ({ style, children }) => {
const { colors } = useUIKitTheme();
return (
<View style={[styles.container, { backgroundColor: colors.ui.dialog.default.none.background }, style]}>
{children}
</View>
);
};

export type SheetItemProps = {
icon?: keyof typeof Icon.Assets;
iconColor?: string;
title: string;
titleColor?: string;
};
const SheetItem: React.FC<SheetItemProps> = ({ icon, title, iconColor, titleColor }) => {
const { colors } = useUIKitTheme();
return (
<View style={[styles.sheetItemContainer, { backgroundColor: colors.ui.dialog.default.none.background }]}>
{icon && (
<Icon
icon={icon}
color={iconColor ?? colors.ui.dialog.default.none.highlight}
containerStyle={styles.sheetItemIcon}
/>
)}
<Text
subtitle1
numberOfLines={1}
color={titleColor ?? colors.ui.dialog.default.none.text}
style={styles.sheetItemText}
>
{title}
</Text>
</View>
);
};

const styles = createStyleSheet({
container: {
overflow: 'hidden',
flexDirection: 'column',
width: '100%',
borderTopLeftRadius: 8,
borderTopRightRadius: 8,
},
sheetItemContainer: {
flexDirection: 'row',
alignItems: 'center',
height: 48,
},
sheetItemIcon: {
marginLeft: 16,
},
sheetItemText: {
marginHorizontal: 24,
},
});

DialogSheet.Item = SheetItem;
export default DialogSheet;
87 changes: 55 additions & 32 deletions packages/uikit-react-native-foundation/src/ui/Dialog/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import type { ActionMenuItem } from '../ActionMenu';
import ActionMenu from '../ActionMenu';
import type { AlertItem } from '../Alert';
import Alert from '../Alert';
import type { BottomSheetItem } from '../BottomSheet';
import BottomSheet from '../BottomSheet';
import type { PromptItem } from '../Prompt';
import Prompt from '../Prompt';

Expand All @@ -21,6 +23,10 @@ type DialogJob =
| {
type: 'Prompt';
props: PromptItem;
}
| {
type: 'BottomSheet';
props: BottomSheetItem;
};

type DialogPropsBy<T extends DialogJob['type'], U extends DialogJob = DialogJob> = U extends { type: T }
Expand All @@ -31,11 +37,13 @@ type DialogContextType = {
openMenu: (props: DialogPropsBy<'ActionMenu'>) => void;
alert: (props: DialogPropsBy<'Alert'>) => void;
prompt: (props: DialogPropsBy<'Prompt'>) => void;
openSheet: (props: DialogPropsBy<'BottomSheet'>) => void;
};

const ActionMenuContext = React.createContext<Pick<DialogContextType, 'openMenu'> | null>(null);
const AlertContext = React.createContext<Pick<DialogContextType, 'alert'> | null>(null);
const PromptContext = React.createContext<Pick<DialogContextType, 'prompt'> | null>(null);
const BottomSheetContext = React.createContext<Pick<DialogContextType, 'openSheet'> | null>(null);

export const DialogProvider: React.FC = ({ children }) => {
const render = useForceUpdate();
Expand All @@ -53,7 +61,6 @@ export const DialogProvider: React.FC = ({ children }) => {
visibleState.current = false;
render();
}, []);

const consumeQueue = useCallback(() => {
const job = dialogQueue.current.shift();
if (job) {
Expand All @@ -78,42 +85,53 @@ export const DialogProvider: React.FC = ({ children }) => {
const openMenu = useCallback(createJob('ActionMenu'), []);
const alert = useCallback(createJob('Alert'), []);
const prompt = useCallback(createJob('Prompt'), []);
const openSheet = useCallback(createJob('BottomSheet'), []);

return (
<AlertContext.Provider value={{ alert }}>
<ActionMenuContext.Provider value={{ openMenu }}>
<PromptContext.Provider value={{ prompt }}>
{children}
{workingDialogJob.current?.type === 'ActionMenu' && (
<ActionMenu
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
menuItems={workingDialogJob.current.props.menuItems}
/>
)}
{workingDialogJob.current?.type === 'Alert' && (
<Alert
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
message={workingDialogJob.current.props.message}
buttons={workingDialogJob.current.props.buttons}
/>
)}
{workingDialogJob.current?.type === 'Prompt' && (
<Prompt
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
onSubmit={workingDialogJob.current.props.onSubmit}
submitLabel={workingDialogJob.current.props.submitLabel}
cancelLabel={workingDialogJob.current.props.cancelLabel}
/>
)}
<BottomSheetContext.Provider value={{ openSheet }}>
{children}
{workingDialogJob.current?.type === 'ActionMenu' && (
<ActionMenu
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
menuItems={workingDialogJob.current.props.menuItems}
/>
)}
{workingDialogJob.current?.type === 'Alert' && (
<Alert
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
message={workingDialogJob.current.props.message}
buttons={workingDialogJob.current.props.buttons}
/>
)}
{workingDialogJob.current?.type === 'Prompt' && (
<Prompt
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
title={workingDialogJob.current.props.title}
onSubmit={workingDialogJob.current.props.onSubmit}
submitLabel={workingDialogJob.current.props.submitLabel}
cancelLabel={workingDialogJob.current.props.cancelLabel}
/>
)}
{workingDialogJob.current?.type === 'BottomSheet' && (
<BottomSheet
onDismiss={consumeQueue}
visible={visibleState.current}
onHide={updateToHide}
sheetItems={workingDialogJob.current.props.sheetItems}
/>
)}
</BottomSheetContext.Provider>
</PromptContext.Provider>
</ActionMenuContext.Provider>
</AlertContext.Provider>
Expand All @@ -135,3 +153,8 @@ export const usePrompt = () => {
if (!context) throw new Error('PromptContext is not provided');
return context;
};
export const useBottomSheet = () => {
const context = useContext(BottomSheetContext);
if (!context) throw new Error('BottomSheetContext is not provided');
return context;
};
32 changes: 19 additions & 13 deletions packages/uikit-react-native-foundation/src/ui/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const Modal: React.FC<Props> = ({
</TouchableWithoutFeedback>
<KeyboardAvoidingView
// NOTE: This is trick for Android.
// When orientation is changed on Android, the offset is not updated normally.
// When orientation is changed on Android, the offset that to avoid soft-keyboard is not updated normally.
key={`${width}-${height}`}
enabled={enableKeyboardAvoid}
style={styles.background}
Expand All @@ -100,14 +100,21 @@ const Modal: React.FC<Props> = ({
pointerEvents={'box-none'}
{...panResponder.panHandlers}
>
{/* NOTE: https://github.com/facebook/react-native/issues/14295 */}
<Pressable>{children}</Pressable>
<Pressable
// NOTE: https://github.com/facebook/react-native/issues/14295
// Due to 'Pressable', the width of the children must be explicitly specified as a number.
>
{children}
</Pressable>
</Animated.View>
</KeyboardAvoidingView>
</RNModal>
);
};

const isHideGesture = (distanceY: number, velocityY: number) => {
return distanceY > 125 || (distanceY > 0 && velocityY > 0.1);
};
const useModalPanResponder = (
type: 'slide' | 'fade',
translateY: Animated.Value,
Expand All @@ -120,10 +127,9 @@ const useModalPanResponder = (
onMoveShouldSetPanResponderCapture: (_, { dy }) => dy > 8,
// @ts-ignore
onPanResponderGrant: () => translateY.setOffset(translateY.__getValue()),
onPanResponderMove: Animated.event([null, { dy: translateY }], { useNativeDriver: false }),
onPanResponderMove: (_, { dy }) => dy >= 0 && translateY.setValue(dy), // Animated.event([null, { dy: translateY }], { useNativeDriver: false }),
onPanResponderRelease: (_, { dy, vy }) => {
const isHideGesture = dy > 125 || (dy > 0 && vy > 0.1);
if (isHideGesture) hide();
if (isHideGesture(dy, vy)) hide();
else show();
},
}),
Expand All @@ -132,27 +138,27 @@ const useModalPanResponder = (

const useModalAnimation = (type: 'slide' | 'fade') => {
const initialY = type === 'slide' ? Dimensions.get('window').height : 0;
const baseAnimationVal = useRef(new Animated.Value(0)).current;
const baseTranslateVal = useRef(new Animated.Value(initialY)).current;
const baseAnimBackground = useRef(new Animated.Value(0)).current;
const baseAnimContent = useRef(new Animated.Value(initialY)).current;

const content = {
opacity: baseAnimationVal.interpolate({
opacity: baseAnimBackground.interpolate({
inputRange: [0, 1],
outputRange: [type === 'slide' ? 1 : 0, 1],
}),
translateY: baseTranslateVal,
translateY: baseAnimContent,
};
const backdrop = {
opacity: baseAnimationVal.interpolate({
opacity: baseAnimBackground.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
};
const createTransition = (toValue: 0 | 1) => {
const config = { duration: 250, useNativeDriver: false };
return Animated.parallel([
Animated.timing(baseAnimationVal, { toValue, ...config }),
Animated.timing(baseTranslateVal, { toValue: toValue === 0 ? initialY : 0, ...config }),
Animated.timing(baseAnimBackground, { toValue, ...config }),
Animated.timing(baseAnimContent, { toValue: toValue === 0 ? initialY : 0, ...config }),
]).start;
};
return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const Prompt: React.FC<Props> = ({
variant={'text'}
style={styles.button}
contentColor={colors.ui.dialog.default.none.highlight}
onPress={async () => {
onPress={() => {
try {
onPress?.();
} finally {
Expand Down
2 changes: 1 addition & 1 deletion sample/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import SendBird from 'sendbird';
import { SendbirdUIKitContainer } from '@sendbird/uikit-react-native';
import { createFilePickerServiceNative, useConnection } from '@sendbird/uikit-react-native-core';
import { DarkUIKitTheme, LightUIKitTheme } from '@sendbird/uikit-react-native-foundation';
import type { SendbirdChatSDK } from '@sendbird/uikit-utils';

import { APP_ID, USER_ID } from './env';
import useAppearance from './hooks/useAppearance';
import * as themeScreens from './screens/theme';
import * as uikitScreens from './screens/uikit-app';
import type { SendbirdChatSDK } from "@sendbird/uikit-utils";

Platform.OS === 'android' && StatusBar.setTranslucent(false);
const Stack = createNativeStackNavigator();
Expand Down
Loading