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

@bartosz grajdek/react native modal refactor kuba v2 #140

6 changes: 3 additions & 3 deletions src/components/Modal/BaseModal.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {PortalHost} from '@gorhom/portal';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
// import ReactNativeModal from 'react-native-modal';
import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import FocusTrapForModal from '@components/FocusTrap/FocusTrapForModal';
import useKeyboardState from '@hooks/useKeyboardState';
Expand Down Expand Up @@ -54,6 +53,8 @@ function BaseModal(
restoreFocusType,
shouldUseModalPaddingStyle = true,
initialFocus = false,
swipeThreshold,
swipeDirection,
}: BaseModalProps,
ref: React.ForwardedRef<View>,
) {
Expand All @@ -78,7 +79,6 @@ function BaseModal(
}
ComposerFocusManager.resetReadyToFocus(uniqueModalId);
}, [shouldEnableNewFocusManagement, uniqueModalId]);

/**
* Hides modal
* @param callHideCallback - Should we call the onModalHide callback
Expand Down Expand Up @@ -155,7 +155,6 @@ function BaseModal(
const {
modalStyle,
modalContainerStyle,
swipeDirection,
animationIn: modalStyleAnimationIn,
animationOut: modalStyleAnimationOut,
shouldAddTopSafeAreaMargin,
Expand Down Expand Up @@ -238,6 +237,7 @@ function BaseModal(
onDismiss={handleDismissModal}
onSwipeComplete={() => onClose?.()}
swipeDirection={swipeDirection}
swipeThreshold={swipeThreshold}
isVisible={isVisible}
backdropColor={theme.overlay}
backdropOpacity={!shouldUseCustomBackdrop && hideBackdrop ? 0 : variables.overlayOpacity}
Expand Down
23 changes: 18 additions & 5 deletions src/components/Modal/ReactNativeModal/Container.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import React from 'react';
import Animated, {SlideInDown, SlideOutDown} from 'react-native-reanimated';
import Animated, {runOnJS, SlideInDown, SlideOutDown, useAnimatedStyle} from 'react-native-reanimated';
import type ModalProps from './types';
import type {ContainerProps} from './types';

function Container({style, ...props}: ModalProps) {
function Container({style, ...props}: ModalProps & ContainerProps) {
const animatedStyles = useAnimatedStyle(() => {
if (!props.panPosition) {
return {};
}
return {
transform: [{translateX: props.panPosition.translateX.value}, {translateY: props.panPosition.translateY.value}],
};
});
return (
<Animated.View
style={[style, {flex: 1, height: '100%', flexDirection: 'row'}]}
entering={SlideInDown.duration(300)}
exiting={SlideOutDown.duration(300)}
entering={SlideInDown.duration(300).withCallback(() => {
runOnJS(props.onOpenCallBack)();
})}
exiting={SlideOutDown.duration(300).withCallback(() => {
runOnJS(props.onCloseCallBack)();
})}
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
>
<Animated.View style={{width: '100%', flex: 1, alignSelf: 'flex-end'}}>{props.children}</Animated.View>
<Animated.View style={[{width: '100%', flex: 1, alignSelf: 'flex-end'}, animatedStyles]}>{props.children}</Animated.View>
</Animated.View>
);
}
Expand Down
92 changes: 40 additions & 52 deletions src/components/Modal/ReactNativeModal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {EmitterSubscription, PanResponderInstance, StyleProp, ViewStyle} from 'react-native';
import {Animated, BackHandler, DeviceEventEmitter, Dimensions, KeyboardAvoidingView, Modal, PanResponder, Platform, View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
import Backdrop from './Backdrop';
import Container from './Container';
import styles from './modal.style';
Expand All @@ -10,9 +11,11 @@
import type {AnimationEvent, Direction} from './types';
import {defaultProps} from './utils';

type TransitionType = 'open' | 'close';

Check failure on line 14 in src/components/Modal/ReactNativeModal/Modal.tsx

View workflow job for this annotation

GitHub Actions / ESLint check

'TransitionType' is defined but never used

Check failure on line 14 in src/components/Modal/ReactNativeModal/Modal.tsx

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

'TransitionType' is defined but never used
sumo-slonik marked this conversation as resolved.
Show resolved Hide resolved

function ReactNativeModal(incomingProps: ModalProps) {
const mergedProps = {...defaultProps, ...incomingProps};

const {
animationIn,
animationOut,
Expand All @@ -34,11 +37,8 @@
style,
avoidKeyboard,
...props
}: ModalProps = {
...defaultProps,
...incomingProps,
};
const [showContent, setShowContent] = useState(isVisible);
} = mergedProps;

const [isVisibleState, setIsVisibleState] = useState(isVisible);
const [isContainerOpen, setIsContainerOpen] = useState(false);
const [isTransitioning, setIsTransitioning] = useState(false);
Expand All @@ -47,8 +47,8 @@
const [pan, setPan] = useState<Animated.ValueXY>(new Animated.ValueXY());
const [panResponder, setPanResponder] = useState<PanResponderInstance | null>(null);
const [inSwipeClosingState, setInSwipeClosingState] = useState(false);
const isSwipeable = !!props.swipeDirection;

const isSwappable = !!props.swipeDirection;
BartoszGrajdek marked this conversation as resolved.
Show resolved Hide resolved
const shouldHideChildren = props.hideModalContentWhileAnimating && isContainerOpen && isTransitioning;
const currentSwipingDirectionRef = useRef<Direction | null>(null);

const setCurrentSwipingDirection = (direction: Direction | null) => {
Expand All @@ -65,14 +65,28 @@

const getDeviceHeight = () => props.deviceHeight ?? deviceHeight;
const getDeviceWidth = () => props.deviceWidth ?? deviceWidth;
const Yoffset = useSharedValue<number>(0);
const Xoffset = useSharedValue<number>(0);

const buildPanResponder = useCallback(() => {
setPanResponder(
PanResponder.create({
onMoveShouldSetPanResponder: onMoveShouldSetPanResponder(props, setAnimEvt, setCurrentSwipingDirection, pan),
onStartShouldSetPanResponder: (a, b) => onStartShouldSetPanResponder(props, setCurrentSwipingDirection)(a as EnhancedGestureEvent, b),
onPanResponderMove: onPanResponderMove(props, currentSwipingDirectionRef, setCurrentSwipingDirection, setAnimEvt, animEvt, pan, deviceHeight, deviceWidth),
onPanResponderRelease: onPanResponderRelease(props, currentSwipingDirectionRef, setInSwipeClosingState, pan),
onPanResponderMove: onPanResponderMove(
props,
currentSwipingDirectionRef,
setCurrentSwipingDirection,
setAnimEvt,
animEvt,
pan,
deviceHeight,
deviceWidth,
Xoffset,
Yoffset,
props.swipeDirection,
),
onPanResponderRelease: onPanResponderRelease(props, currentSwipingDirectionRef, setInSwipeClosingState, pan, Xoffset, Yoffset),
}),
);
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
Expand All @@ -99,41 +113,24 @@
return false;
};

const handleTransition = (type: TransitionType, onFinish: () => void) => {
const handleTransition = () => {
const shouldAnimate = isVisible !== isContainerOpen;

if (shouldAnimate && !isTransitioning) {
setIsTransitioning(true);

setTimeout(() => {
setIsContainerOpen(isVisible);
setIsTransitioning(false);

onFinish();
}, 300);
// TODO: type === 'open' ? animationInTiming : animationOutTiming,
}
};

const open = () => {
if (isTransitioning) {
return;
}
if (isSwipeable) {
pan.setValue?.({x: 0, y: 0});
}

if (props.onModalWillShow) {
props.onModalWillShow();
}
setIsVisibleState(true);
handleTransition('open', () => {
if (!isVisible) {
setIsContainerOpen(false);
} else {
onModalShow();
}
});
handleTransition();
};

const close = () => {
Expand All @@ -150,24 +147,17 @@
}

setIsVisibleState(false);
handleTransition('close', () => {
if (isVisible) {
setIsContainerOpen(true);
} else {
setShowContent(false);
props.onModalHide?.();
}
});
handleTransition();
};

useEffect(() => {
if (!isSwipeable) {
if (!isSwappable) {
return;
}

setPan(new Animated.ValueXY());
buildPanResponder();
}, [isSwipeable, buildPanResponder]);
}, [isSwappable, buildPanResponder]);

useEffect(() => {
didUpdateDimensionsEmitter.current = DeviceEventEmitter.addListener('didUpdateDimensions', handleDimensionsUpdate);
Expand Down Expand Up @@ -200,33 +190,31 @@

const computedStyle: Array<StyleProp<ViewStyle>> = [{margin: getDeviceWidth() * 0.05}, styles.content, style];

let panPosition: StyleProp<ViewStyle> = {};
if (isSwipeable && panResponder) {
if (useNativeDriver) {
panPosition = {
// transform: pan.getTranslateTransform(),
};
} else {
// panPosition = pan.getLayout();
}
}

const containerView = (
<Container
animationIn={animationIn}
animationOut={animationOut}
animationInTiming={animationInTiming}
animationOutTiming={animationOutTiming}
isVisible={isVisibleState}
style={[panPosition, computedStyle]}
style={[computedStyle]}
panPosition={isSwappable ? {translateX: Xoffset, translateY: Yoffset} : undefined}
pointerEvents="box-none"
useNativeDriver={useNativeDriver}
onOpenCallBack={() => {
setIsContainerOpen(true);
setIsTransitioning(false);
}}
onCloseCallBack={() => {
setIsContainerOpen(false);
setIsTransitioning(false);
}}
// eslint-disable-next-line react/jsx-props-no-spreading
{...(isSwipeable && panResponder ? panResponder.panHandlers : {})}
{...(isSwappable && panResponder ? panResponder.panHandlers : {})}
// eslint-disable-next-line react/jsx-props-no-spreading
{...otherProps}
>
{props.hideModalContentWhileAnimating && !showContent ? <View /> : children}
{shouldHideChildren ? <View /> : children}
</Container>
);

Expand Down
48 changes: 41 additions & 7 deletions src/components/Modal/ReactNativeModal/panResponders.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,35 @@
import type {MutableRefObject} from 'react';
import type {PanResponderGestureState} from 'react-native';
import {Animated} from 'react-native';
import type {Animated, PanResponderGestureState} from 'react-native';
import type {SharedValue} from 'react-native-reanimated';
import {withSpring} from 'react-native-reanimated';
import {calcDistancePercentage, createAnimationEventForSwipe, getAccDistancePerDirection, getSwipingDirection, isSwipeDirectionAllowed, shouldPropagateSwipe} from './panHandlers';
import type {AnimationEvent, Direction, GestureResponderEvent} from './types';
import type ModalProps from './types';

function handleSwipe(dx: number, dy: number, Xoffset: SharedValue<number>, Yoffset: SharedValue<number>, swipeDirection?: Direction | Direction[]) {
if (!swipeDirection) {
return;
}
const directions = Array.isArray(swipeDirection) ? swipeDirection : [swipeDirection];

if (directions.includes('right') && dx > 0) {
// eslint-disable-next-line no-param-reassign
Xoffset.value = dx;
}
if (directions.includes('left') && dx < 0) {
// eslint-disable-next-line no-param-reassign
Xoffset.value = dx;
}
if (directions.includes('up') && dy < 0) {
// eslint-disable-next-line no-param-reassign
Yoffset.value = dy;
}
if (directions.includes('down') && dy > 0) {
// eslint-disable-next-line no-param-reassign
Yoffset.value = dy;
}
}

type RemainingModalProps = Omit<
ModalProps,
| 'animationIn'
Expand Down Expand Up @@ -82,8 +107,12 @@ const onPanResponderMove = (
pan: Animated.ValueXY,
deviceHeight: number,
deviceWidth: number,
xOffset: SharedValue<number>,
yOffset: SharedValue<number>,
swipeDirection?: Direction | Direction[],
) => {
return (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => {
handleSwipe(gestureState.dx, gestureState.dy, xOffset, yOffset, swipeDirection);
const currentSwipingDirection = currentSwipingDirectionRef.current;
if (!currentSwipingDirection) {
if (gestureState.dx === 0 && gestureState.dy === 0) {
Expand Down Expand Up @@ -128,6 +157,8 @@ const onPanResponderRelease = (
currentSwipingDirectionRef: MutableRefObject<Direction | null>,
setInSwipeClosingState: (val: boolean) => void,
pan: Animated.ValueXY,
xOffset: SharedValue<number>,
yOffset: SharedValue<number>,
) => {
return (evt: GestureResponderEvent, gestureState: PanResponderGestureState) => {
const currentSwipingDirection = currentSwipingDirectionRef.current;
Expand All @@ -141,6 +172,10 @@ const onPanResponderRelease = (
},
gestureState,
);
// eslint-disable-next-line no-param-reassign
xOffset.value = 0;
// eslint-disable-next-line no-param-reassign
yOffset.value = 0;
return;
}
}
Expand All @@ -149,11 +184,10 @@ const onPanResponderRelease = (
props.onSwipeCancel(gestureState);
}

Animated.spring(pan, {
toValue: {x: 0, y: 0},
bounciness: 0,
useNativeDriver: false,
}).start();
// eslint-disable-next-line no-param-reassign
xOffset.value = withSpring(0);
// eslint-disable-next-line no-param-reassign
yOffset.value = withSpring(0);

if (props.scrollTo && props.scrollOffset !== undefined && props.scrollOffsetMax !== undefined) {
if (props.scrollOffset > props.scrollOffsetMax) {
Expand Down
Loading
Loading