Skip to content

Commit

Permalink
feat(foundation): implement gesture to slide type modal
Browse files Browse the repository at this point in the history
  • Loading branch information
bang9 committed Mar 2, 2022
1 parent 0c82d60 commit cc5af93
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 51 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useContext } from 'react';
import React, { useContext, useEffect, useState } from 'react';

import { ActionMenu } from '@sendbird/uikit-react-native-foundation';

Expand All @@ -12,10 +12,17 @@ const GroupChannelListChannelMenu: React.FC<GroupChannelListProps['ChannelMenu']
const { LABEL } = useLocalization();
const { currentUser } = useSendbirdChat();

const [visible, setVisible] = useState(false);

useEffect(() => {
if (channelMenu.selectedChannel) setVisible(true);
}, [channelMenu.selectedChannel]);

return (
<ActionMenu
visible={Boolean(channelMenu.selectedChannel)}
onHide={channelMenu.selectChannel}
visible={visible}
onHide={() => setVisible(false)}
onDismiss={channelMenu.selectChannel}
title={
channelMenu.selectedChannel &&
LABEL.GROUP_CHANNEL_LIST.CHANNEL_MENU.TITLE(currentUser?.userId ?? '', channelMenu.selectedChannel)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ const GroupChannelListTypeSelector: React.FC<GroupChannelListProps['TypeSelector
};

return (
<Modal visible={visible} onPressBackground={hide} onRequestClose={hide} statusBarTranslucent={statusBarTranslucent}>
<Modal visible={visible} onClose={hide} statusBarTranslucent={statusBarTranslucent}>
{renderHeader()}
</Modal>
);
Expand Down
42 changes: 24 additions & 18 deletions packages/uikit-react-native-foundation/src/ui/ActionMenu/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { ActivityIndicator, Pressable, View } from 'react-native';
import React from 'react';
import { Pressable, View } from 'react-native';

import createStyleSheet from '../../styles/createStyleSheet';
import useHeaderStyle from '../../styles/useHeaderStyle';
Expand All @@ -24,19 +24,17 @@ type Props = {
const ActionMenu: React.FC<Props> = ({ visible, onHide, onError, onDismiss, title, items }) => {
const { statusBarTranslucent } = useHeaderStyle();
const { colors } = useUIKitTheme();
const [pending, setPending] = useState(false);

const _onHide = () => {
if (!pending) onHide();
};
// const [pending, setPending] = useState(false);
// const _onHide = () => {
// if (!pending) onHide();
// };

return (
<Modal
onClose={onHide}
onDismiss={onDismiss}
statusBarTranslucent={statusBarTranslucent}
visible={visible}
onRequestClose={_onHide}
onPressBackground={_onHide}
backgroundStyle={{ alignItems: 'center', justifyContent: 'center' }}
>
<DialogBox>
Expand All @@ -45,35 +43,43 @@ const ActionMenu: React.FC<Props> = ({ visible, onHide, onError, onDismiss, titl
h1
color={colors.ui.dialog.default.none.text}
numberOfLines={1}
style={{ maxWidth: pending ? '86%' : '100%' }}
style={{ flex: 1 }}
// style={{ maxWidth: pending ? '86%' : '100%' }}
>
{title}
</Text>
{pending && (
{/*{pending && (
<ActivityIndicator
size={'small'}
color={colors.ui.dialog.default.none.highlight}
style={{ width: '10%', marginLeft: '4%' }}
/>
)}
)}*/}
</View>
<View style={styles.buttonContainer}>
{items.map(({ title, onPress }, index) => {
return (
<Pressable
key={title + index}
style={styles.button}
disabled={pending}
// disabled={pending}
onPress={async () => {
setPending(true);
try {
await onPress?.();
onHide();
} catch (e: unknown) {
onPress?.();
} catch (e) {
onError?.(e);
} finally {
setPending(false);
onHide();
}
// setPending(true);
// try {
// await onPress?.();
// onHide();
// } catch (e: unknown) {
// onError?.(e);
// } finally {
// setPending(false);
// }
}}
>
<Text subtitle2 color={colors.ui.dialog.default.none.text} numberOfLines={1}>
Expand Down
5 changes: 2 additions & 3 deletions packages/uikit-react-native-foundation/src/ui/Alert/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ export type AlertItem = {
type Props = {
visible: boolean;
onHide: () => void;
onDismiss?: () => void;
title: AlertItem['title'];
message: AlertItem['message'];
buttons: AlertItem['buttons'];
onDismiss?: () => void;
};
const Alert: React.FC<Props> = ({
onDismiss,
Expand All @@ -36,11 +36,10 @@ const Alert: React.FC<Props> = ({

return (
<Modal
onClose={onHide}
onDismiss={onDismiss}
statusBarTranslucent={statusBarTranslucent}
visible={visible}
onRequestClose={onHide}
onPressBackground={onHide}
backgroundStyle={{ alignItems: 'center', justifyContent: 'center' }}
>
<DialogBox style={styles.container}>
Expand Down
140 changes: 121 additions & 19 deletions packages/uikit-react-native-foundation/src/ui/Modal/index.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,166 @@
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import {
Animated,
Dimensions,
ModalProps,
PanResponder,
Platform,
Pressable,
Modal as RNModal,
StyleProp,
StyleSheet,
TouchableWithoutFeedback,
View,
ViewStyle,
} from 'react-native';

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

type Props = { backgroundStyle?: StyleProp<ViewStyle>; onPressBackground?: () => void } & ModalProps;
type Props = { type?: 'slide' | 'fade'; onClose: () => void; backgroundStyle?: StyleProp<ViewStyle> } & Omit<
ModalProps,
'animationType' | 'onRequestClose'
>;

/**
* Modal Open: Triggered by Modal.props.visible state changed to true
* - visible true -> modalVisible true -> animation start
*
* Modal Close: Triggered by Modal.props.onClose() call
* - Modal.props.onClose() -> visible false -> animation start -> modalVisible false
* */
const Modal: React.FC<Props> = ({
children,
onClose,
backgroundStyle,
onPressBackground,
onDismiss,
type = 'fade',
visible = false,
...props
}) => {
const { palette } = useUIKitTheme();
const { content, backdrop, showTransition, hideTransition } = useModalAnimation(type);
const panResponder = useModalPanResponder(type, content.translateY, showTransition, onClose);

const [modalVisible, setModalVisible] = useState(false);
const showAction = () => setModalVisible(true);
const hideAction = () => hideTransition(() => setModalVisible(false));

useEffect(() => {
if (visible) showAction();
else hideAction();
}, [visible]);

useOnDismiss(visible, onDismiss);
useOnDismiss(modalVisible, onDismiss);

return (
<RNModal
transparent
visible={visible}
hardwareAccelerated
visible={modalVisible}
onRequestClose={onClose}
onShow={() => showTransition()}
onDismiss={onDismiss}
supportedOrientations={['portrait', 'portrait-upside-down', 'landscape', 'landscape-left', 'landscape-right']}
animationType={'fade'}
animationType={'none'}
{...props}
>
<TouchableWithoutFeedback onPress={onPressBackground}>
<View style={[StyleSheet.absoluteFill, { backgroundColor: palette.onBackgroundLight03 }]} />
<TouchableWithoutFeedback onPress={onClose}>
<Animated.View
style={[StyleSheet.absoluteFill, { opacity: backdrop.opacity, backgroundColor: palette.onBackgroundLight03 }]}
/>
</TouchableWithoutFeedback>
<View pointerEvents={'box-none'} style={[styles.background, backgroundStyle]}>
{children}
</View>
<Animated.View
style={[
styles.background,
backgroundStyle,
{ opacity: content.opacity, transform: [{ translateY: content.translateY }] },
]}
pointerEvents={'box-none'}
{...panResponder.panHandlers}
>
{/* NOTE: https://github.com/facebook/react-native/issues/14295 */}
<Pressable>{children}</Pressable>
</Animated.View>
</RNModal>
);
};

const useModalPanResponder = (
type: 'slide' | 'fade',
translateY: Animated.Value,
show: () => void,
hide: () => void,
) => {
if (type === 'fade') return { panHandlers: {} };
return React.useRef(
PanResponder.create({
onMoveShouldSetPanResponderCapture: (_, { dy }) => dy > 8,
// @ts-ignore
onPanResponderGrant: () => translateY.setOffset(translateY.__getValue()),
onPanResponderMove: Animated.event([null, { dy: translateY }], { useNativeDriver: false }),
onPanResponderRelease: (_, { dy, vy }) => {
const isHideGesture = dy > 125 || (dy > 0 && vy > 0.1);
if (isHideGesture) hide();
else show();
},
}),
).current;
};

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 content = {
opacity: baseAnimationVal.interpolate({
inputRange: [0, 1],
outputRange: [type === 'slide' ? 1 : 0, 1],
}),
translateY: baseTranslateVal,
};
const backdrop = {
opacity: baseAnimationVal.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 }),
]).start;
};
return {
content,
backdrop,
showTransition: createTransition(1),
hideTransition: createTransition(0),
};
};

// NOTE: onDismiss is supports iOS only
const useOnDismiss = (visible: boolean, onDismiss?: () => void) => {
const prevVisibleState = useRef(false);
const prevVisible = usePrevProp(visible);
useEffect(() => {
if (Platform.OS === 'ios') return;
if (prevVisibleState.current && !visible) onDismiss?.();
prevVisibleState.current = visible;
}, [visible]);
if (prevVisible && !visible) onDismiss?.();
}, [prevVisible, visible]);
};

const usePrevProp = <T,>(prop: T) => {
const prev = useRef(prop);
const curr = useRef(prop);
useEffect(() => {
prev.current = curr.current;
curr.current = prop;
});
return prev.current;
};

const styles = createStyleSheet({
background: {
flex: 1,
},
background: { flex: 1 },
});

export default Modal;
10 changes: 3 additions & 7 deletions sample/stories/Dialog.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ const WrappedActionMenu: React.FC = () => {
items: [
{ title: 'Loooooooooooong ActionMenu button title', onPress: () => {} },
{
title: 'Close after 3 seconds',
onPress: () => {
return new Promise((resolve) => {
setTimeout(() => resolve(0), 3000);
});
},
title: 'Close',
onPress: () => {},
},
],
})
Expand All @@ -54,7 +50,7 @@ const WrappedActionMenu: React.FC = () => {
title={'Open multiple times (Queued)'}
onPress={() =>
openMenu({
title: 'Action Menu',
title: 'Action Menu Title title title title title title',
items: [
{
title: 'Open menu 2 times',
Expand Down

0 comments on commit cc5af93

Please sign in to comment.