Skip to content
Merged
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
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ jobs:
- name: Setup
uses: ./.github/actions/setup

- name: Run unit tests
run: yarn test --maxWorkers=2 --coverage

build-library:
runs-on: ubuntu-latest
steps:
Expand Down
818 changes: 818 additions & 0 deletions CLAUDE.md

Large diffs are not rendered by default.

16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@

A stack manager for [@gorhom/bottom-sheet](https://github.com/gorhom/react-native-bottom-sheet) with navigation modes, iOS-style scale animations, and React context preservation.

## Documentation
## 📚 Documentation

**[View Full Documentation](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/)**

- [Getting Started](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/getting-started)
- [Navigation Modes](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/navigation-modes)
- [Scale Animation](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/scale-animation)
- [Portal API (Context Preservation)](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/context-preservation)
- [Persistent Sheets](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/persistent-sheets)
- [Type-Safe IDs & Params](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/type-safe-ids)
- [API Reference](https://arekkubaczkowski.github.io/react-native-bottom-sheet-stack/api/components)

## Features
## Features

- **Stack Navigation** - `push`, `switch`, and `replace` modes for managing multiple sheets
- **Scale Animation** - iOS-style background scaling effect when sheets are stacked
- **Context Preservation** - Portal-based API that preserves React context in bottom sheets
- **Type-Safe** - Full TypeScript support with type-safe portal IDs and params
- **Group Support** - Isolated stacks for different parts of your app
- 📚 **Stack Navigation** - `push`, `switch`, and `replace` modes for managing multiple sheets
- 🎭 **Scale Animation** - iOS-style background scaling effect when sheets are stacked
- 🔗 **Context Preservation** - Portal-based API that preserves React context in bottom sheets
- ⚡ **Persistent Sheets** - Pre-mounted sheets that open instantly and preserve state
- 🔒 **Type-Safe** - Full TypeScript support with type-safe portal IDs and params
- 📦 **Group Support** - Isolated stacks for different parts of your app

## Installation

Expand Down
49 changes: 49 additions & 0 deletions docs/docs/api/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,52 @@ Declares a portal-based bottom sheet that preserves React context.
| `children` | `ReactElement` | Yes | The bottom sheet component to render |

See [Type-Safe Portal IDs](/type-safe-ids) for type-safe ID configuration.

---

## BottomSheetPersistent

Declares a persistent bottom sheet that stays mounted even when closed. Opens instantly and preserves internal state between open/close cycles.

```tsx
<BottomSheetPersistent id="scanner">
<ScannerSheet />
</BottomSheetPersistent>
```

### Props

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `id` | `BottomSheetPortalId` | Yes | Unique identifier for this persistent sheet |
| `children` | `ReactElement` | Yes | The bottom sheet component to render |

### Placement

Can be placed anywhere inside `BottomSheetManagerProvider`. Must stay mounted to be accessible.

```tsx
// At app root - always available
<BottomSheetManagerProvider id="main">
<BottomSheetScaleView>
<App />
</BottomSheetScaleView>
<BottomSheetHost />
<BottomSheetPersistent id="scanner">
<ScannerSheet />
</BottomSheetPersistent>
</BottomSheetManagerProvider>

// Or on a specific screen
function HomeScreen() {
return (
<View>
<BottomSheetPersistent id="quick-actions">
<QuickActionsSheet />
</BottomSheetPersistent>
</View>
);
}
```

See [Persistent Sheets](/persistent-sheets) for detailed usage.
13 changes: 10 additions & 3 deletions docs/docs/api/hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,19 +155,26 @@ open({

Subscribe to any sheet's status from anywhere in your app. Pass the sheet ID to identify which sheet to observe.

:::tip When to Use
Use this when you need to show UI based on whether a sheet is open, or react to status changes. Separate from `useBottomSheetControl` to avoid unnecessary re-renders in components that only need to trigger sheets.
:::tip Works with All Sheet Types
This hook accepts any string ID, so it works with portal sheets, persistent sheets, and inline sheets (using the ID returned from `useBottomSheetManager().open()`).
:::

```tsx
// Portal/persistent sheet
const { status, isOpen } = useBottomSheetStatus('my-sheet');

// Inline sheet (using ID from open())
const { open } = useBottomSheetManager();
const sheetId = open(<MySheet />);
// ...
const { status, isOpen } = useBottomSheetStatus(sheetId);
```

### Parameters

| Parameter | Type | Description |
|-----------|------|-------------|
| `id` | `BottomSheetPortalId` | The sheet ID to observe |
| `id` | `string` | The sheet ID to observe (portal ID or inline sheet ID) |

### Returns

Expand Down
172 changes: 172 additions & 0 deletions docs/docs/persistent-sheets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
---
sidebar_position: 7
---

# Persistent Sheets

Persistent sheets are **pre-mounted** bottom sheets that stay in memory even when closed. They open instantly without mount delay and preserve their internal state between open/close cycles.

## When to Use

- **Frequently accessed sheets** - Scanner, camera, quick actions
- **State preservation** - Forms, multi-step wizards, media players
- **Instant open** - When mount delay is noticeable

## Basic Usage

```tsx
import {
BottomSheetPersistent,
useBottomSheetControl,
} from 'react-native-bottom-sheet-stack';

function App() {
return (
<BottomSheetManagerProvider id="main">
<BottomSheetScaleView>
<HomeScreen />
</BottomSheetScaleView>
<BottomSheetHost />

{/* Persistent sheet - always mounted */}
<BottomSheetPersistent id="scanner">
<ScannerSheet />
</BottomSheetPersistent>
</BottomSheetManagerProvider>
);
}

function HomeScreen() {
const scanner = useBottomSheetControl('scanner');

return (
<Button
title="Open Scanner"
onPress={() => scanner.open({ scaleBackground: true })}
/>
);
}
```

## State Preservation

Internal component state is preserved between open/close cycles:

```tsx
const ScannerSheet = forwardRef((props, ref) => {
const { close } = useBottomSheetContext();
// State is preserved when sheet is closed and reopened
const [scanResult, setScanResult] = useState<string | null>(null);
const [isScanning, setIsScanning] = useState(false);

const handleScan = () => {
setIsScanning(true);
// ... scanning logic
setScanResult('QR-ABC123');
setIsScanning(false);
};

return (
<BottomSheetManaged ref={ref} enableDynamicSizing>
<View>
{scanResult ? (
<Text>Result: {scanResult}</Text>
) : (
<Button title="Start Scan" onPress={handleScan} />
)}
<Button title="Close" onPress={close} />
</View>
</BottomSheetManaged>
);
});
```

Close the sheet and reopen it - `scanResult` is still there.


## Comparison with Portal API

| Feature | `BottomSheetPortal` | `BottomSheetPersistent` |
|---------|--------------------|-----------------------|
| Context preservation | ✅ | ✅ |
| State preservation | ❌ (remounts) | ✅ |
| Instant open | ❌ | ✅ |
| Memory usage | Lower | Higher |
| Best for | Regular sheets | Frequently used sheets |

## Passing Params

You can pass params when opening:

```tsx
const scanner = useBottomSheetControl('scanner');

scanner.open({
scaleBackground: true,
params: { source: 'home', title: 'Scan QR Code' },
});
```

Access them in the sheet:

```tsx
const ScannerSheet = forwardRef((props, ref) => {
const { params } = useBottomSheetContext<'scanner'>();

return (
<BottomSheetManaged ref={ref}>
<Text>{params?.title ?? 'Scanner'}</Text>
</BottomSheetManaged>
);
});
```

## Type Safety

Register your persistent sheet IDs for type-safe params:

```tsx
// bottom-sheet.d.ts
declare module 'react-native-bottom-sheet-stack' {
interface BottomSheetPortalRegistry {
scanner: { source: 'home' | 'navigation'; title?: string };
}
}
```

Now `useBottomSheetControl('scanner')` has typed `open()` and `params`.

## Placement

`BottomSheetPersistent` can be placed **anywhere** inside `BottomSheetManagerProvider` - at the app root, on a specific screen, or nested deep in your component tree.

The only requirement: **it must stay mounted** to be accessible. If you unmount it, the sheet won't be available until it mounts again.

```tsx
// Option 1: At app root - always available
<BottomSheetManagerProvider id="main">
<BottomSheetScaleView>
<App />
</BottomSheetScaleView>
<BottomSheetHost />
<BottomSheetPersistent id="scanner">
<ScannerSheet />
</BottomSheetPersistent>
</BottomSheetManagerProvider>

// Option 2: On a specific screen - available only when screen is mounted
function HomeScreen() {
return (
<View>
<BottomSheetPersistent id="quick-actions">
<QuickActionsSheet />
</BottomSheetPersistent>
{/* ... screen content */}
</View>
);
}
```

:::tip
For sheets that should be accessible from anywhere in your app, place them at the root level to ensure they're always mounted.
:::
1 change: 1 addition & 0 deletions docs/sidebars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const sidebars: SidebarsConfig = {
'navigation-modes',
'scale-animation',
'context-preservation',
'persistent-sheets',
'type-safe-ids',
{
type: 'category',
Expand Down
6 changes: 6 additions & 0 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import {
BottomSheetHost,
BottomSheetManagerProvider,
BottomSheetPersistent,
BottomSheetScaleView,
} from 'react-native-bottom-sheet-stack';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import { UserProvider } from './context/UserContext';
import { HomeScreen } from './screens';
import { ScannerSheet } from './sheets';
import { sharedStyles } from './styles/theme';

export default function App() {
Expand All @@ -24,6 +26,10 @@ export default function App() {
</UserProvider>
</BottomSheetScaleView>
<BottomSheetHost />
{/* Persistent sheet - always mounted, opens instantly */}
<BottomSheetPersistent id="scanner-sheet">
<ScannerSheet />
</BottomSheetPersistent>
</BottomSheetManagerProvider>
</GestureHandlerRootView>
</SafeAreaProvider>
Expand Down
4 changes: 4 additions & 0 deletions example/src/bottom-sheet.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,9 @@ declare module 'react-native-bottom-sheet-stack' {
};
'portal-mode-sheet-a': true;
'portal-mode-sheet-b': true;
'scanner-sheet': {
source: 'home' | 'navigation';
title?: string;
};
}
}
15 changes: 14 additions & 1 deletion example/src/screens/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { StyleSheet, ScrollView, Text, View } from 'react-native';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import {
BottomSheetPortal,
useBottomSheetControl,
Expand All @@ -23,6 +23,7 @@ export function HomeScreen() {
const { openBottomSheet } = useBottomSheetManager();
const portalSheetControl = useBottomSheetControl('context-portal-sheet');
const portalModeSheetA = useBottomSheetControl('portal-mode-sheet-a');
const scannerControl = useBottomSheetControl('scanner-sheet');

return (
<View style={sharedStyles.container}>
Expand Down Expand Up @@ -112,6 +113,18 @@ export function HomeScreen() {
openBottomSheet(<HeavySheet />, { scaleBackground: true })
}
/>

<DemoCard
title="Persistent Scanner"
description="Pre-mounted sheet with keepMounted - opens instantly"
color={colors.cyan}
onPress={() =>
scannerControl.open({
scaleBackground: true,
params: { source: 'home', title: 'Scanner from Home' },
})
}
/>
</View>

{/* Features */}
Expand Down
Loading
Loading