Skip to content

Commit 65be6cf

Browse files
committed
feat: basic setup react-native-true-sheet
1 parent 047660f commit 65be6cf

File tree

8 files changed

+90
-212
lines changed

8 files changed

+90
-212
lines changed

app/containers/ActionSheet/ActionSheet.tsx

Lines changed: 34 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,98 +1,36 @@
11
import { useBackHandler } from '@react-native-community/hooks';
22
import * as Haptics from 'expo-haptics';
3-
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react';
4-
import { Keyboard, type LayoutChangeEvent, useWindowDimensions } from 'react-native';
5-
import { Easing, useDerivedValue, useSharedValue } from 'react-native-reanimated';
6-
import BottomSheet, { BottomSheetBackdrop, type BottomSheetBackdropProps } from '@discord/bottom-sheet';
3+
import React, { forwardRef, isValidElement, useEffect, useImperativeHandle, useRef, useState } from 'react';
4+
import { Keyboard, useWindowDimensions } from 'react-native';
5+
import { TrueSheet } from '@lodev09/react-native-true-sheet';
76
import { useSafeAreaInsets } from 'react-native-safe-area-context';
87

98
import { useTheme } from '../../theme';
10-
import { isIOS, isTablet } from '../../lib/methods/helpers';
9+
import { isTablet } from '../../lib/methods/helpers';
1110
import { Handle } from './Handle';
1211
import { type TActionSheetOptions } from './Provider';
1312
import BottomSheetContent from './BottomSheetContent';
1413
import styles from './styles';
1514

1615
export const ACTION_SHEET_ANIMATION_DURATION = 250;
17-
const HANDLE_HEIGHT = 28;
18-
const CANCEL_HEIGHT = 64;
19-
20-
const ANIMATION_CONFIG = {
21-
duration: ACTION_SHEET_ANIMATION_DURATION,
22-
// https://easings.net/#easeInOutCubic
23-
easing: Easing.bezier(0.645, 0.045, 0.355, 1.0)
24-
};
2516

2617
const ActionSheet = React.memo(
2718
forwardRef(({ children }: { children: React.ReactElement }, ref) => {
2819
const { colors } = useTheme();
2920
const { height: windowHeight } = useWindowDimensions();
30-
const { bottom, right, left } = useSafeAreaInsets();
31-
const { fontScale } = useWindowDimensions();
32-
const itemHeight = 48 * fontScale;
33-
const bottomSheetRef = useRef<BottomSheet>(null);
21+
const { right, left } = useSafeAreaInsets();
22+
const sheetRef = useRef<TrueSheet>(null);
3423
const [data, setData] = useState<TActionSheetOptions>({} as TActionSheetOptions);
3524
const [isVisible, setVisible] = useState(false);
36-
const animatedContentHeight = useSharedValue(0);
37-
const animatedHandleHeight = useSharedValue(0);
38-
const animatedDataSnaps = useSharedValue<TActionSheetOptions['snaps']>([]);
39-
const animatedSnapPoints = useDerivedValue(() => {
40-
if (animatedDataSnaps.value?.length) {
41-
return animatedDataSnaps.value;
42-
}
43-
const contentWithHandleHeight = animatedContentHeight.value + animatedHandleHeight.value;
44-
// Bottom sheet requires a default value to work
45-
if (contentWithHandleHeight === 0) {
46-
return ['25%'];
47-
}
48-
return [contentWithHandleHeight];
49-
}, [data]);
50-
51-
const handleContentLayout = useCallback(
52-
({
53-
nativeEvent: {
54-
layout: { height }
55-
}
56-
}: LayoutChangeEvent) => {
57-
/**
58-
* This logic is only necessary to prevent the action sheet from
59-
* occupying the entire screen when the dynamic content is too big.
60-
*/
61-
animatedContentHeight.value = Math.min(height, windowHeight * 0.8);
62-
},
63-
[animatedContentHeight, windowHeight]
64-
);
65-
66-
const maxSnap = Math.min(
67-
(itemHeight + 0.5) * (data?.options?.length || 0) +
68-
HANDLE_HEIGHT +
69-
// Custom header height
70-
(data?.headerHeight || 0) +
71-
// Insets bottom height (Notch devices)
72-
bottom +
73-
// Cancel button height
74-
(data?.hasCancel ? CANCEL_HEIGHT : 0),
75-
windowHeight * 0.8
76-
);
77-
78-
/*
79-
* if the action sheet cover more than 60% of the screen height,
80-
* we'll provide more one snap of 50%
81-
*/
82-
const snaps = maxSnap > windowHeight * 0.6 && !data.snaps ? ['50%', maxSnap] : [maxSnap];
83-
84-
const toggleVisible = () => setVisible(!isVisible);
8525

8626
const hide = () => {
87-
bottomSheetRef.current?.close();
27+
sheetRef.current?.dismiss();
8828
};
8929

9030
const show = (options: TActionSheetOptions) => {
9131
setData(options);
92-
if (options.snaps?.length) {
93-
animatedDataSnaps.value = options.snaps;
94-
}
95-
toggleVisible();
32+
setVisible(true);
33+
sheetRef.current?.present(0);
9634
};
9735

9836
useBackHandler(() => {
@@ -114,69 +52,43 @@ const ActionSheet = React.memo(
11452
hideActionSheet: hide
11553
}));
11654

117-
const renderHandle = () => (
55+
const renderHeader = () => (
11856
<>
11957
<Handle />
12058
{isValidElement(data?.customHeader) ? data.customHeader : null}
12159
</>
12260
);
12361

124-
const onClose = () => {
125-
toggleVisible();
126-
data?.onClose && data?.onClose();
127-
animatedDataSnaps.value = [];
62+
const onDidDismiss = () => {
63+
setVisible(false);
64+
data?.onClose?.();
12865
};
12966

130-
const renderBackdrop = useCallback(
131-
(props: BottomSheetBackdropProps) => (
132-
<BottomSheetBackdrop
133-
{...props}
134-
appearsOnIndex={0}
135-
// Backdrop should be visible all the time bottom sheet is open
136-
disappearsOnIndex={-1}
137-
opacity={colors.backdropOpacity}
138-
/>
139-
),
140-
[]
141-
);
142-
143-
const bottomSheet = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left };
144-
145-
// Must need this prop to avoid keyboard dismiss
146-
// when is android tablet and the input text is focused
147-
const androidTablet: any = isTablet && !isIOS ? { android_keyboardInputMode: 'adjustResize' } : {};
67+
const bottomSheetStyle = isTablet ? styles.bottomSheet : { marginRight: right, marginLeft: left };
14868

14969
return (
15070
<>
15171
{children}
152-
{isVisible && (
153-
<BottomSheet
154-
ref={bottomSheetRef}
155-
// If data.options exist, we calculate snaps to be precise, otherwise we cal
156-
snapPoints={data.options?.length ? snaps : animatedSnapPoints}
157-
handleHeight={animatedHandleHeight}
158-
// We need undefined to enable vertical swipe gesture inside the bottom sheet like in reaction picker
159-
contentHeight={data.snaps?.length || data.options?.length ? undefined : animatedContentHeight}
160-
animationConfigs={ANIMATION_CONFIG}
161-
animateOnMount={true}
162-
backdropComponent={renderBackdrop}
163-
handleComponent={renderHandle}
164-
enablePanDownToClose
165-
style={{ ...styles.container, ...bottomSheet }}
166-
backgroundStyle={{ backgroundColor: colors.surfaceLight }}
167-
onChange={index => index === -1 && onClose()}
168-
// We need this to allow horizontal swipe gesture inside the bottom sheet like in reaction picker
169-
enableContentPanningGesture={data?.enableContentPanningGesture ?? true}
170-
{...androidTablet}>
171-
<BottomSheetContent
172-
options={data?.options}
173-
hide={hide}
174-
children={data?.children}
175-
hasCancel={data?.hasCancel}
176-
onLayout={handleContentLayout}
177-
/>
178-
</BottomSheet>
179-
)}
72+
<TrueSheet
73+
ref={sheetRef}
74+
detents={['auto']}
75+
maxHeight={windowHeight * 0.8}
76+
backgroundColor={colors.surfaceLight}
77+
cornerRadius={16}
78+
dimmed
79+
grabber={false}
80+
header={renderHeader()}
81+
scrollable={!!data?.options}
82+
style={[styles.container, bottomSheetStyle]}
83+
onDidDismiss={onDidDismiss}>
84+
<BottomSheetContent
85+
options={data?.options}
86+
hide={hide}
87+
children={data?.children}
88+
hasCancel={data?.hasCancel}
89+
onLayout={() => {}}
90+
/>
91+
</TrueSheet>
18092
</>
18193
);
18294
})

app/containers/ActionSheet/BottomSheetContent.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { Text, useWindowDimensions, type ViewProps } from 'react-native';
1+
import { FlatList, Text, useWindowDimensions, View, type ViewProps } from 'react-native';
22
import React from 'react';
3-
import { BottomSheetView, BottomSheetFlatList } from '@discord/bottom-sheet';
43
import { useSafeAreaInsets } from 'react-native-safe-area-context';
54

65
import I18n from '../../i18n';
76
import { useTheme } from '../../theme';
7+
import { isAndroid } from '../../lib/methods/helpers';
88
import { type IActionSheetItem, Item } from './Item';
99
import { type TActionSheetOptionsItem } from './Provider';
1010
import styles from './styles';
@@ -41,7 +41,7 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
4141

4242
if (options) {
4343
return (
44-
<BottomSheetFlatList
44+
<FlatList
4545
testID='action-sheet'
4646
data={options}
4747
refreshing={false}
@@ -56,13 +56,14 @@ const BottomSheetContent = React.memo(({ options, hasCancel, hide, children, onL
5656
ListHeaderComponent={List.Separator}
5757
ListFooterComponent={renderFooter}
5858
onLayout={onLayout}
59+
nestedScrollEnabled={isAndroid}
5960
/>
6061
);
6162
}
6263
return (
63-
<BottomSheetView testID='action-sheet' style={styles.contentContainer} onLayout={onLayout}>
64+
<View testID='action-sheet' style={styles.contentContainer} onLayout={onLayout}>
6465
{children}
65-
</BottomSheetView>
66+
</View>
6667
);
6768
});
6869

app/containers/TextInput/FormTextInput.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
View,
1010
type ViewStyle
1111
} from 'react-native';
12-
import { BottomSheetTextInput } from '@discord/bottom-sheet';
1312
import Touchable from 'react-native-platform-touchable';
1413
import { A11y } from 'react-native-a11y-order';
1514

@@ -124,7 +123,7 @@ export const FormTextInput = ({
124123
const { colors } = useTheme();
125124
const [showPassword, setShowPassword] = useState(false);
126125
const showClearInput = onClearInput && value && value.length > 0;
127-
const Input = bottomSheet ? BottomSheetTextInput : TextInput;
126+
const Input = TextInput;
128127

129128
const inputError = getInputError(error);
130129
const accessibilityLabelText = useMemo(() => {

ios/Podfile.lock

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2606,6 +2606,30 @@ PODS:
26062606
- ReactCommon/turbomodule/bridging
26072607
- ReactCommon/turbomodule/core
26082608
- Yoga
2609+
- RNTrueSheet (3.6.11):
2610+
- DoubleConversion
2611+
- glog
2612+
- hermes-engine
2613+
- RCT-Folly (= 2024.11.18.00)
2614+
- RCTRequired
2615+
- RCTTypeSafety
2616+
- React-Core
2617+
- React-debug
2618+
- React-Fabric
2619+
- React-featureflags
2620+
- React-graphics
2621+
- React-hermes
2622+
- React-ImageManager
2623+
- React-jsi
2624+
- React-NativeModulesApple
2625+
- React-RCTFabric
2626+
- React-renderercss
2627+
- React-rendererdebug
2628+
- React-utils
2629+
- ReactCodegen
2630+
- ReactCommon/turbomodule/bridging
2631+
- ReactCommon/turbomodule/core
2632+
- Yoga
26092633
- SDWebImage (5.21.0):
26102634
- SDWebImage/Core (= 5.21.0)
26112635
- SDWebImage/Core (5.21.0)
@@ -2754,6 +2778,7 @@ DEPENDENCIES:
27542778
- RNReanimated (from `../node_modules/react-native-reanimated`)
27552779
- RNScreens (from `../node_modules/react-native-screens`)
27562780
- RNSVG (from `../node_modules/react-native-svg`)
2781+
- "RNTrueSheet (from `../node_modules/@lodev09/react-native-true-sheet`)"
27572782
- "simdjson (from `../node_modules/@nozbe/simdjson`)"
27582783
- "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)"
27592784
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@@ -3030,6 +3055,8 @@ EXTERNAL SOURCES:
30303055
:path: "../node_modules/react-native-screens"
30313056
RNSVG:
30323057
:path: "../node_modules/react-native-svg"
3058+
RNTrueSheet:
3059+
:path: "../node_modules/@lodev09/react-native-true-sheet"
30333060
simdjson:
30343061
:path: "../node_modules/@nozbe/simdjson"
30353062
WatermelonDB:
@@ -3086,7 +3113,7 @@ SPEC CHECKSUMS:
30863113
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
30873114
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
30883115
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851
3089-
RCT-Folly: e78785aa9ba2ed998ea4151e314036f6c49e6d82
3116+
RCT-Folly: 36fe2295e44b10d831836cc0d1daec5f8abcf809
30903117
RCTDeprecation: 0418ac97b9f53b2e37f473da1663ef3061e46beb
30913118
RCTRequired: b9fde7f981b11aa898f03a70d3d4d36b80f1b16d
30923119
RCTTypeSafety: 397515ea9a8122b62a7a310adf30205f0a5e3bfc
@@ -3177,6 +3204,7 @@ SPEC CHECKSUMS:
31773204
RNReanimated: f52ccd5ceea2bae48d7421eec89b3f0c10d7b642
31783205
RNScreens: b13e4c45f0406f33986a39c0d8da0324bff94435
31793206
RNSVG: 680e961f640e381aab730a04b2371969686ed9f7
3207+
RNTrueSheet: c32094ce4b285bf7f5208c10d9a84ab867fe2207
31803208
SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868
31813209
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
31823210
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c

jest.setup.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,22 @@ jest.mock('expo-device', () => ({
155155
isDevice: true
156156
}));
157157

158-
jest.mock('@discord/bottom-sheet', () => {
159-
const react = require('react-native');
158+
jest.mock('@lodev09/react-native-true-sheet', () => {
159+
const React = require('react');
160+
const { View } = require('react-native');
161+
const TrueSheet = React.forwardRef((props, ref) => {
162+
React.useImperativeHandle(ref, () => ({
163+
present: () => Promise.resolve(),
164+
dismiss: () => Promise.resolve(),
165+
resize: () => Promise.resolve()
166+
}));
167+
return <View {...props} />;
168+
});
169+
TrueSheet.displayName = 'TrueSheet';
160170
return {
161171
__esModule: true,
162-
default: react.View,
163-
BottomSheetScrollView: react.ScrollView
172+
TrueSheet,
173+
TrueSheetProvider: ({ children }) => children
164174
};
165175
});
166176

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
},
2626
"dependencies": {
2727
"@bugsnag/react-native": "8.4.0",
28-
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
28+
"@lodev09/react-native-true-sheet": "~3.6.0",
2929
"@expo/vector-icons": "^14.1.0",
3030
"@hookform/resolvers": "^2.9.10",
3131
"@nozbe/watermelondb": "^0.28.1-0",

0 commit comments

Comments
 (0)