I am using React Native Swipeable for "swipe to select" functionality in the mobile app I am working on. I have a list of invoices. Each invoice is swipeable. When user swipe right, it displays selected checkbox in the left menu and invoice number should be added to selectedInvoices
state of the invoices list component, without having to take any further action. To achieve this I used onSwipeableOpen
and all works as expected, apart one thing. When user swipe right, invoice number is being added to the array of selected invoices, but swipeable component is getting closed straight away, so user cannot see left menu that indicates it is selected. Any idea how to prevent swipeable auto closing after executing onSwipeableOpen
?
Link to the original package documentation: https://docs.swmansion.com/react-native-gesture-handler/docs/api/components/swipeable/
Current Behaviour
Swipeable Document
closes automatically after onSwipeableOpen
execution when invoice number is being added to selectedInvoices
state (array) in InvoicesList
component.
Expected Behaviour
Swipeable Document
stays open, to display left menu to indicate invoice has been selected.
InvoicesList.tsx component
import Modal from 'react-native-modal';
import Moment from 'moment';
import { ColorSchemeName, StyleSheet, Text, TextStyle, useColorScheme, View, ViewStyle } from 'react-native';
import { Colours } from '../../assets/styles';
import { Document } from '../UI';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { InvoiceType } from '../../types';
import { RootState } from '../../redux/store';
import { SCREEN_HEIGHT, SCREEN_WIDTH, WINDOW_FONT_SCALE } from '../../services/DimensionsHelper';
import { useSelector } from 'react-redux';
import { useState } from 'react';
type Props = {
invoices: Array<InvoiceType>,
showSwipeDemo: boolean,
swipeDemoContentStyle: ViewStyle,
containerStyle?: ViewStyle,
};
export default function InvoicesList({ invoices, showSwipeDemo, swipeDemoContentStyle, containerStyle }: Props): JSX.Element {
const theme: Style = getStyle(useColorScheme());
const swipeDemoSeen: boolean = useSelector((state: RootState) => state.settings.swipeDemoSeen);
const firstInvoice: InvoiceType = invoices[0];
const [selectedInvoices, setSelectedInvoices] = useState<Array<number>>([]);
const selectInvoice = (invoiceNumber: number): void => {
setSelectedInvoices((currentSelectedInvoices: Array<number>) => [...currentSelectedInvoices, invoiceNumber]);
};
const unselectInvoice = (invoiceNumber: number): void => {
setSelectedInvoices((currentSelectedInvoices: Array<number>) => currentSelectedInvoices.filter((invoice: number) => invoice !== invoiceNumber));
};
return (
<>
<GestureHandlerRootView>
<View style={[ theme.container, containerStyle ]}>
{
invoices.map((invoice: InvoiceType, index: number) =>
<Document
key={ index }
number={ invoice.glitem }
title={ 'Invoice #' + invoice.documentNumber.toString() }
status={ invoice.status }
description={ invoice.customerReference }
date={ 'Invoiced ' + Moment(invoice.documentDate).format('D MMM YYYY ') }
value={ invoice.value }
onSwipeRight={ selectInvoice }
onSwipeableClose={ unselectInvoice }
showSwipeDemo={ false }
/>
)
}
</View>
</GestureHandlerRootView>
<Modal
customBackdrop={ <View style={ theme.modalBackdrop } /> }
backdropOpacity={ 0.7 }
statusBarTranslucent={ true }
isVisible={ showSwipeDemo && !swipeDemoSeen }
animationIn='fadeIn'
animationOut='fadeOut'
useNativeDriver={ true }
useNativeDriverForBackdrop={ true }
hideModalContentWhileAnimating={ true }
>
<View style={[ theme.modalContent, swipeDemoContentStyle ]}>
<GestureHandlerRootView>
<Document
number={ firstInvoice.glitem }
title={ 'Invoice #' + firstInvoice.documentNumber.toString() }
status={ firstInvoice.status }
description={ firstInvoice.customerReference }
date={ 'Invoiced ' + Moment(firstInvoice.documentDate).format('D MMM YYYY ') }
value={ firstInvoice.value }
onSwipeRight={ () => {} }
onSwipeableClose={ () => {} }
showSwipeDemo={ showSwipeDemo && !swipeDemoSeen }
/>
</GestureHandlerRootView>
<View style={ theme.instructionContainer }><Text style={ theme.instruction }>Swipe right to select</Text></View>
</View>
</Modal>
</>
);
}
type Style = {
container: ViewStyle,
modalBackdrop: ViewStyle,
modalContent: ViewStyle,
instructionContainer: ViewStyle,
instruction: TextStyle,
};
const getStyle = (colourScheme: ColorSchemeName): Style => {
return StyleSheet.create({
container: {
paddingHorizontal: 16,
paddingBottom: 32,
},
modalBackdrop: {
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
backgroundColor: Colours.black.s100,
},
modalContent: {
position: 'absolute',
width: '101%',
marginLeft: -2,
},
instructionContainer: {
alignItems: 'center',
},
instruction: {
fontFamily: 'Montserrat_700Bold',
fontSize: 18 / WINDOW_FONT_SCALE,
lineHeight: 22 / WINDOW_FONT_SCALE,
color: Colours.white.s100,
textTransform: 'uppercase',
},
});
};
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,
},
});
};