Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 43 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export default function Example() {
/>
<RBSheet
ref={refRBSheet}
snapPoints={['20%', '40%', '90%']}
useNativeDriver={true}
customStyles={{
wrapper: {
Expand Down Expand Up @@ -111,11 +112,46 @@ const renderItem = ({item, index}) => {
};
```

#### Bottom Sheet with snap points

```jsx
import React, {useRef} from 'react';
import {View, Button, Text} from 'react-native';
import RBSheet from 'react-native-raw-bottom-sheet';

export default function Example() {
const refRBSheet = useRef();

return (
<View style={{flex: 1, justifyContent: 'center', alignItems: 'center'}}>
<Button title="OPEN AT 40%" onPress={() => refRBSheet.current.open(1)} />
<Button title="OPEN FULL (90%)" onPress={() => refRBSheet.current.open(2)} />

<RBSheet
ref={refRBSheet}
snapPoints={['20%', '40%', '90%']} // 👈 multiple snap points
draggable={true}
customStyles={{
container: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
},
}}>
<View style={{padding: 20}}>
<Text style={{fontSize: 18}}>Hello from snap points 👋</Text>
</View>
</RBSheet>
</View>
);
}
```

## Props

| Props | Type | Description | Default |
| ----------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| height | number | The height of bottom sheet. | 260 |
| snapPoints | array | Array of snap points (`['20%', '50%', '90%']` or `[200, 400, 600]`). Overrides `height`. | {} |
| openDuration | number | Duration of the animation when opening bottom sheet. | 300 (ms) |
| closeDuration | number | Duration of the animation when closing bottom sheet. | 200 (ms) |
| closeOnPressMask | boolean | Press the outside area (mask) to close bottom sheet. | true |
Expand All @@ -141,10 +177,13 @@ customStyles: {

## Methods

| Method Name | Description | Usage |
| ----------- | --------------------------------- | ---------------------------- |
| open | The method to open bottom sheet. | `refRBSheet.current.open()` |
| close | The method to close bottom sheet. | `refRBSheet.current.close()` |
| Method Name | Description | Usage |
| ----------- | ---------------------------------------------------------------------------| ---------------------------- |
| open | The method to open bottom sheet. | `refRBSheet.current.open()` |
| close | The method to close bottom sheet. | `refRBSheet.current.close()` |
| open | Opens the bottom sheet at a snap point. Pass index (default = last point). | `refRBSheet.current.open(1)` |

👉 This way, people can use either height (old way) or snapPoints (new way). No breaking changes.

## CONTRIBUTING

Expand Down
13 changes: 11 additions & 2 deletions example/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ const App = () => {
</View>

{/* List Menu */}
<RBSheet ref={refStandard} draggable dragOnContent height={330}>
<RBSheet
snapPoints={['20%', '50%', '80%']}
ref={refStandard}
draggable
dragOnContent
height={330}>
<View style={styles.listContainer}>
<Text style={styles.listTitle}>Create</Text>
{data.lists.map(list => (
Expand All @@ -75,6 +80,7 @@ const App = () => {
{/* Grid Menu */}
<RBSheet
ref={refScrollable}
snapPoints={['20%', '50%', '80%']}
draggable
customModalProps={{
animationType: 'slide',
Expand Down Expand Up @@ -110,6 +116,7 @@ const App = () => {
{/* Date Picker IOS */}
<RBSheet
ref={refDatePicker}
snapPoints={['20%', '50%', '80%']}
onOpen={() => console.log('RBSheet is Opened')}
onClose={() => console.log('RBSheet is Closed')}>
<View style={styles.dateHeaderContainer}>
Expand All @@ -130,7 +137,8 @@ const App = () => {
{/* TextInput */}
<RBSheet
ref={refInput}
height={60}
snapPoints={['20%', '50%', '80%']}
// height={60}
closeOnPressMask={true}
closeOnPressBack={true}
customStyles={{
Expand All @@ -155,6 +163,7 @@ const App = () => {
{/* Alert */}
<RBSheet
ref={refMessage}
snapPoints={['20%', '50%', '80%']}
openDuration={150}
closeDuration={100}
customStyles={{
Expand Down
17 changes: 15 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,19 @@ import {
interface RBSheetProps {
/**
* The height of bottom sheet.
* Used as fallback when `snapPoints` is not provided.
*/
height?: number;

/**
* Array of snap points for the bottom sheet.
* Accepts either percentages (e.g. "50%") or absolute numbers in px.
* Example: snapPoints={['20%', '50%', '90%']} or snapPoints={[200, 400, 600]}
*
* If not provided, the sheet will use `height` for a single snap point.
*/
snapPoints?: (string | number)[];

/**
* Duration of the animation when opening bottom sheet.
*/
Expand Down Expand Up @@ -94,16 +104,19 @@ interface RBSheetProps {
onClose?: () => void;

/**
* Your own compoent.
* Your own component.
*/
children?: React.ReactNode;
}

interface RBSheetRef {
/**
* The method to open bottom sheet.
* You can specify which snap point index to open.
*
* Example: ref.current.open(1) // opens at snapPoints[1]
*/
open: () => void;
open: (index?: number) => void;

/**
* The method to close bottom sheet.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-raw-bottom-sheet",
"version": "3.0.0",
"version": "3.1.0",
"description": "Add Your Own Component To Bottom Sheet Whatever You Want (Android & iOS)",
"main": "index.js",
"typings": "index.d.ts",
Expand Down
128 changes: 79 additions & 49 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// Importing necessary packages and components
import React, {useState, useRef, forwardRef, useImperativeHandle} from 'react';
import React, {
useState,
useRef,
forwardRef,
useImperativeHandle,
useEffect,
} from 'react';
import {
Animated,
PanResponder,
Expand All @@ -8,14 +14,19 @@ import {
KeyboardAvoidingView,
Platform,
View,
Dimensions,
} from 'react-native';
import styles from './style';

// Get device screen height (for % based snap points)
const {height: SCREEN_HEIGHT} = Dimensions.get('window');

// Creating the RBSheet component
const RBSheet = forwardRef((props, ref) => {
// Props destructuring
const {
height = 260,
snapPoints = null, // NEW: array of snap points
openDuration = 300,
closeDuration = 200,
closeOnPressMask = true,
Expand All @@ -36,11 +47,28 @@ const RBSheet = forwardRef((props, ref) => {

// Using useRef hook to reference animated values
const animatedHeight = useRef(new Animated.Value(0)).current;
const pan = useRef(new Animated.ValueXY()).current;
const pan = useRef(new Animated.Value(0)).current; // NEW: single value for vertical drag

// Helper: convert snapPoints (percentages or numbers) into pixels
const normalizedSnapPoints = snapPoints
? snapPoints.map(p =>
typeof p === 'string' && p.includes('%')
? (parseFloat(p) / 100) * SCREEN_HEIGHT
: Number(p),
)
: [height]; // fallback to single height

const maxSnapPoint = Math.max(...normalizedSnapPoints);

// Keep track of current snap index
const currentSnapIndex = useRef(normalizedSnapPoints.length - 1);

// Exposing component methods to parent via useImperativeHandle hook
useImperativeHandle(ref, () => ({
open: () => handleSetVisible(true),
open: (index = normalizedSnapPoints.length - 1) => {
currentSnapIndex.current = index;
handleSetVisible(true, normalizedSnapPoints[index]);
},
close: () => handleSetVisible(false),
}));

Expand All @@ -50,31 +78,43 @@ const RBSheet = forwardRef((props, ref) => {
// Respond only if draggable is true
onStartShouldSetPanResponder: () => draggable,

// Respond only if draggable, dragOnContent is true, and vertical movement is positive
// Respond if draggable and dragging downward or upward
onMoveShouldSetPanResponder: (e, gestureState) =>
draggable && dragOnContent && gestureState.dy > 0,
draggable && Math.abs(gestureState.dy) > 2,

// Update pan.y value on vertical move if gestureState.dy is positive
// Update pan value (drag sheet with finger)
onPanResponderMove: (e, gestureState) => {
gestureState.dy > 0 &&
Animated.event([null, {dy: pan.y}], {useNativeDriver})(
e,
gestureState,
);
const newHeight =
normalizedSnapPoints[currentSnapIndex.current] - gestureState.dy;

// Prevent dragging beyond screen or below 0
if (newHeight >= 0 && newHeight <= maxSnapPoint) {
pan.setValue(newHeight);
}
},

// Handle when the user has released the touche
// Handle when the user releases finger
onPanResponderRelease: (e, gestureState) => {
// Close modal if swipe down distance is more than 100
if (gestureState.dy > 100) {
handleSetVisible(false);
} else {
// Reset pan to original position on release
Animated.spring(pan, {
toValue: {x: 0, y: 0},
useNativeDriver,
}).start();
}
const releaseHeight =
normalizedSnapPoints[currentSnapIndex.current] - gestureState.dy;

// Find closest snap point
let closest = normalizedSnapPoints[0];
let closestIndex = 0;
normalizedSnapPoints.forEach((p, i) => {
if (Math.abs(p - releaseHeight) < Math.abs(closest - releaseHeight)) {
closest = p;
closestIndex = i;
}
});

currentSnapIndex.current = closestIndex;

// Animate to closest snap point
Animated.spring(pan, {
toValue: closest,
useNativeDriver: false,
}).start();
},
});
};
Expand All @@ -83,33 +123,24 @@ const RBSheet = forwardRef((props, ref) => {
const panResponder = useRef(createPanResponder()).current;

// Function to handle the visibility of the modal
const handleSetVisible = visible => {
const handleSetVisible = (visible, toValue = height) => {
if (visible) {
setModalVisible(visible);
// Call onOpen callback if provided
if (typeof onOpen === 'function') {
onOpen();
}
// Animate height on open
Animated.timing(animatedHeight, {
useNativeDriver,
toValue: height,
setModalVisible(true);
if (typeof onOpen === 'function') onOpen();

Animated.timing(pan, {
toValue,
duration: openDuration,
useNativeDriver: false,
}).start();
} else {
// Animate height on close
Animated.timing(animatedHeight, {
useNativeDriver,
Animated.timing(pan, {
toValue: 0,
duration: closeDuration,
useNativeDriver: false,
}).start(() => {
setModalVisible(visible);
// Reset pan value
pan.setValue({x: 0, y: 0});
// Call onClose callback if provided
if (typeof onClose === 'function') {
onClose();
}
setModalVisible(false);
if (typeof onClose === 'function') onClose();
});
}
};
Expand All @@ -120,7 +151,7 @@ const RBSheet = forwardRef((props, ref) => {
testID="Modal"
transparent
visible={modalVisible}
onRequestClose={closeOnPressBack ? () => handleSetVisible(false) : null} // Close on hardware button press (Android) if enabled
onRequestClose={closeOnPressBack ? () => handleSetVisible(false) : null}
{...customModalProps}>
<KeyboardAvoidingView
testID="KeyboardAvoidingView"
Expand All @@ -131,21 +162,20 @@ const RBSheet = forwardRef((props, ref) => {
testID="TouchableOpacity"
style={styles.mask}
activeOpacity={1}
onPress={closeOnPressMask ? () => handleSetVisible(false) : null} // Close on mask press if enabled
onPress={closeOnPressMask ? () => handleSetVisible(false) : null}
/>
<Animated.View
testID="AnimatedView"
{...(dragOnContent && panResponder.panHandlers)} // Attach pan handlers to content if dragOnContent is true
{...(dragOnContent && panResponder.panHandlers)}
style={[
styles.container,
{transform: pan.getTranslateTransform()},
{height: animatedHeight},
{height: pan}, // height follows pan value
customStyles.container,
]}>
{draggable && ( // Show draggable icon if set it to true
{draggable && (
<View
testID="DraggableView"
{...(!dragOnContent && panResponder.panHandlers)} // Attach pan handlers to draggable icon if dragOnContent is false
{...(!dragOnContent && panResponder.panHandlers)}
style={styles.draggableContainer}>
<View
testID="DraggableIcon"
Expand Down