I am using React Native Swipeable component from React Native Gesture Handler.
https://docs.swmansion.com/react-native-gesture-handler/docs/api/components/swipeable/
The original behaviour: When user swipe element right, it pushes content out of the view to display left menu.
Current implementation
What I try to achieve: When user swipe element right, content width to be animated to display left menu, so the whole content is still visible to the user.
UX Figma Design
My swipeable Document.tsx
component
import MkmIcon from './MkmIcon';
import StatusLabel from './StatusLabel';
import { ColorSchemeName, Pressable, StyleSheet, Text, TextStyle, useColorScheme, View, ViewStyle } from 'react-native';
import { Colours } from '../../assets/styles';
import { Swipeable } from 'react-native-gesture-handler';
import { WINDOW_FONT_SCALE } from '../../services/DimensionsHelper';
import { setSwipeDemoSeen } from '../../redux/settingsSlice';
import { useDispatch } from 'react-redux';
import { useLayoutEffect, useRef } from 'react';
type Props = {
number: number,
title: string,
status: string,
description?: string,
date: string,
value: number,
onPress?: (number: number) => void,
onSwipeRight?: (number: number) => void,
onSwipeableClose?: (number: number) => void,
showSwipeDemo?: boolean,
};
export default function Document({ number, title, status, description, date, value, onPress, onSwipeRight, onSwipeableClose, showSwipeDemo }: Props): JSX.Element {
const theme: Style = getStyle(useColorScheme());
const swipeableRef = useRef(null);
const dispatch = useDispatch();
const onPressHandler = (): void => {
if (onPress) {
onPress(number);
}
};
const onSwipeRightHandler = (): void => {
if (onSwipeRight) {
onSwipeRight(number);
}
};
const onSwipeableCloseHandler = (): void => {
if (onSwipeableClose) {
onSwipeableClose(number);
}
};
const renderLeftCheckBox = (progress): JSX.Element => {
return (
<View style={ theme.checkContainer }>
<MkmIcon name='CheckThick' colour={ Colours.white.s100 } style={ theme.check } />
</View>
);
};
const Container = ({ children }): JSX.Element => {
return (
onSwipeRight && onSwipeableClose
? <Swipeable
ref={ swipeableRef }
containerStyle={ theme.swipeableContainer }
renderLeftActions={ renderLeftCheckBox }
onSwipeableOpen={ onSwipeRightHandler }
onSwipeableClose={ onSwipeableCloseHandler }
overshootLeft={ false }
>{ children }</Swipeable>
: <View style={ theme.container }>{ children }</View>
);
};
useLayoutEffect(() => {
if (showSwipeDemo) {
const swipeable: Swipeable = swipeableRef.current;
setTimeout(() => {
swipeable.openLeft();
}, 1000);
setTimeout(() => {
swipeable.close();
}, 2500);
setTimeout(() => {
dispatch(setSwipeDemoSeen());
}, 3500)
}
}, [showSwipeDemo]);
return (
<Container>
<Pressable style={ ({ pressed }) => [theme.documentContainer, (onPress && pressed) && theme.pressed ] } onPress={ onPressHandler }>
<View style={ theme.row }>
<View style={ theme.column }>
<Text style={ theme.title }>{ title }</Text>
</View>
<View>
<StatusLabel status={ status } />
</View>
</View>
{
description &&
<View style={ theme.descriptionContainer }>
<Text style={ theme.description }>{ description }</Text>
</View>
}
<View style={[ theme.row, theme.rowBottom ]}>
<View style={ theme.column }>
<Text style={ theme.date }>{ date }</Text>
</View>
<View>
<Text style={ theme.amount }>£{ value }</Text>
</View>
</View>
</Pressable>
</Container>
);
}
type Style = {
container: ViewStyle,
swipeableContainer: ViewStyle,
documentContainer: ViewStyle,
pressed: ViewStyle,
row: ViewStyle,
column: ViewStyle,
title: TextStyle,
descriptionContainer: ViewStyle,
description: TextStyle,
rowBottom: ViewStyle,
date: TextStyle,
amount: TextStyle,
checkContainer: ViewStyle,
check: ViewStyle,
};
const getStyle = (colourScheme: ColorSchemeName): Style => {
return StyleSheet.create({
container: {
marginBottom: 16,
},
swipeableContainer: {
padding: -1,
marginBottom: 16,
borderRadius: 4,
backgroundColor: Colours.success.s100,
},
documentContainer: {
padding: 16,
borderWidth: 1,
borderBottomWidth: 4,
borderRadius: 4,
borderColor: Colours.grey.s30,
backgroundColor: Colours.white.s100,
},
pressed: {
opacity: 0.6,
},
row: {
flex: 1,
flexDirection: 'row',
alignItems: 'center',
alignContent: 'space-between',
},
column: {
flex: 1,
},
title: {
fontFamily: 'Montserrat_500Medium',
fontSize: 14 / WINDOW_FONT_SCALE,
lineHeight: 21 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
descriptionContainer: {
paddingTop: 8,
},
description: {
fontFamily: 'Montserrat_600SemiBold',
fontSize: 16 / WINDOW_FONT_SCALE,
lineHeight: 24 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
rowBottom: {
paddingTop: 12,
},
date: {
fontFamily: 'Montserrat_500Medium',
fontSize: 14 / WINDOW_FONT_SCALE,
lineHeight: 21 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s60,
},
amount: {
fontFamily: 'Montserrat_700Bold',
fontSize: 16 / WINDOW_FONT_SCALE,
lineHeight: 24 / WINDOW_FONT_SCALE,
color: Colours.charcoal.s100,
},
checkContainer: {
paddingHorizontal: 12,
alignItems: 'center',
justifyContent: 'center',
},
check: {
width: 30,
height: 30,
padding: 7,
borderWidth: 1,
borderColor: Colours.white.s100,
borderRadius: 15,
},
});
};