Bluesky app fork with some witchin' additions 馃挮
witchsky.app
bluesky
fork
client
1# Bottom Sheet Expo Module
2
3A custom Expo module that provides native bottom sheet functionality for iOS and Android, using platform-specific native bottom sheet implementations (UISheetPresentationController on iOS, Material BottomSheetDialog on Android).
4
5## Overview
6
7This module wraps native bottom sheet components to provide a React Native interface with cross-platform consistency. It uses native presentation APIs rather than JavaScript-based animations for better performance and native behavior.
8
9Key features:
10- Native bottom sheet presentation on iOS and Android
11- Automatic content height detection (no JS bridge round-trip)
12- Configurable snap points (hidden, partial, full)
13- Drag-to-dismiss with prevention controls
14- Portal-based rendering for proper z-index layering
15- Edge-to-edge support on modern Android versions
16- iOS 26+ zoom transition support
17
18## Platform Support
19
20- **iOS**: Uses `UISheetPresentationController` (iOS 15+)
21- **Android**: Uses Material Design `BottomSheetDialog` with `BottomSheetBehavior`
22- **Web**: Not supported (throws error)
23
24## Architecture
25
26### TypeScript Layer
27
28The module exposes a React component that handles rendering and state management:
29
30- **BottomSheet.tsx** (Native): Main component wrapping the native view
31- **BottomSheet.web.tsx** (Web): Stub that throws an error
32- **BottomSheetNativeComponent.tsx**: React wrapper with portal integration
33- **BottomSheetPortal.tsx**: Portal system for rendering sheets above app content
34- **Portal.tsx**: Generic portal implementation for managing component hierarchy
35
36The component uses a class-based approach to expose imperative methods (`present()`, `dismiss()`, `dismissAll()`).
37
38### Native Layer
39
40#### iOS Implementation
41
42- **BottomSheetModule.swift**: Expo module definition with event handlers and prop bindings
43- **SheetView.swift**: Main view component that creates and manages `SheetViewController`
44 - Observes content height via KVO (Key-Value Observing) on bounds
45 - Manages sheet lifecycle and state transitions
46 - Implements `UISheetPresentationControllerDelegate` for drag events
47- **SheetViewController.swift**: UIViewController subclass with sheet presentation
48 - Configures detents (snap points) based on content height
49 - Handles iOS 26+ safe area adjustments for floating sheet style
50 - Animates detent changes when content resizes
51- **SheetManager.swift**: Singleton that tracks all active sheets with weak references
52- **Util.swift**: Helper for calculating screen height minus safe area insets
53
54#### Android Implementation
55
56- **BottomSheetModule.kt**: Expo module definition mirroring iOS functionality
57- **BottomSheetView.kt**: Main view component managing Material BottomSheetDialog
58 - Uses `OnLayoutChangeListener` to observe content height natively
59 - Configures `BottomSheetBehavior` for drag and snap behavior
60 - Handles edge-to-edge display across Android versions (API 29-35+)
61 - Preserves status/nav bar appearance from host activity
62- **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog
63 - Forwards touch events to React Native event system
64 - Updates shadow node size to match window dimensions
65 - Based on React Native's ReactModalHostView pattern
66- **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS)
67
68### Content Height Detection
69
70Both platforms detect content height changes natively without JS bridge round-trips:
71
72- **iOS**: KVO observation on the content view's `bounds` property
73- **Android**: `OnLayoutChangeListener` on child views (catches React Native's direct `layout()` calls)
74
75This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading).
76
77## Props
78
79```typescript
80interface BottomSheetViewProps {
81 children: React.ReactNode
82
83 // Appearance
84 cornerRadius?: number
85 backgroundColor?: ColorValue
86 containerBackgroundColor?: ColorValue
87
88 // Behavior
89 preventDismiss?: boolean // Disable swipe-to-dismiss
90 preventExpansion?: boolean // Lock to initial height (no full-screen)
91 disableDrag?: boolean // Disable drag handle (Android only)
92 fullHeight?: boolean // Start at full screen height
93
94 // Height constraints
95 minHeight?: number // Minimum height in dp
96 maxHeight?: number // Maximum height in dp
97
98 // iOS 26+ transition
99 sourceViewTag?: number // View tag for zoom transition origin
100
101 // Events
102 onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void
103 onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void
104 onStateChange?: (event: BottomSheetStateChangeEvent) => void
105}
106```
107
108## States and Snap Points
109
110### States
111- `closed`: Sheet is dismissed
112- `closing`: Sheet is animating closed
113- `open`: Sheet is fully visible
114- `opening`: Sheet is animating open
115
116### Snap Points
117- `Hidden` (0): Dismissed
118- `Partial` (1): Half-expanded / content height
119- `Full` (2): Expanded to screen height
120
121## Usage
122
123### Basic Example
124
125```tsx
126import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet'
127
128// In your app root:
129function App() {
130 return (
131 <BottomSheetProvider>
132 <YourApp />
133 <BottomSheetOutlet />
134 </BottomSheetProvider>
135 )
136}
137
138// In a component:
139function MyComponent() {
140 const sheetRef = useRef<BottomSheet>(null)
141
142 const openSheet = () => {
143 sheetRef.current?.present()
144 }
145
146 const closeSheet = () => {
147 sheetRef.current?.dismiss()
148 }
149
150 return (
151 <>
152 <Button onPress={openSheet} title="Open Sheet" />
153
154 <BottomSheet
155 ref={sheetRef}
156 cornerRadius={16}
157 backgroundColor="white"
158 onStateChange={(e) => console.log(e.nativeEvent.state)}
159 >
160 <View style={{padding: 20}}>
161 <Text>Sheet content</Text>
162 <Button onPress={closeSheet} title="Close" />
163 </View>
164 </BottomSheet>
165 </>
166 )
167}
168```
169
170### Nested Sheets
171
172The module supports nesting sheets by using `BottomSheetPortalProvider` within sheet content:
173
174```tsx
175<BottomSheet ref={outerSheetRef}>
176 <BottomSheetPortalProvider>
177 <Button onPress={() => innerSheetRef.current?.present()} />
178 <BottomSheet ref={innerSheetRef}>
179 <Text>Inner sheet content</Text>
180 </BottomSheet>
181 </BottomSheetPortalProvider>
182</BottomSheet>
183```
184
185### Dismiss All Sheets
186
187```tsx
188import {BottomSheetNativeComponent} from '@modules/bottom-sheet'
189
190BottomSheetNativeComponent.dismissAll()
191```
192
193## Key Implementation Details
194
195### iOS Specific
196
1971. **iOS 15 Compatibility**: On iOS 15, custom detents are not available, so the module uses `.medium()` detent and applies extra styling to prevent visual issues.
198
1992. **iOS 26+ Zoom Transitions**: When `sourceViewTag` is provided on iOS 26+, the sheet zooms from the specified view.
200
2013. **Detent Selection**: The module automatically chooses between custom detents, `.medium()`, and `.large()` based on content height and screen size.
202
203### Android Specific
204
2051. **Edge-to-Edge**: The module handles edge-to-edge display correctly across API levels:
206 - API 35+: Mandatory edge-to-edge
207 - API 30-34: Uses `currentWindowMetrics`
208 - API <30: Uses deprecated `getRealSize()`
209
2102. **Status/Nav Bar Appearance**: Preserves light/dark appearance from the host activity and reapplies it to the sheet dialog.
211
2123. **Drag Handling**: On full-height sheets with `preventDismiss`, dragging is disabled to prevent accidental dismissal (since there's no half-expanded snap point to land on).
213
2144. **Layout Updates During Gestures**: Content height changes are deferred during drag gestures to prevent fighting the user's input.
215
216### Platform Differences
217
218- **cornerRadius**: Applied to sheet on iOS, to content wrapper on Android (Android clips with `overflow: hidden`)
219- **disableDrag**: Android-only prop (iOS drag behavior is controlled via `preventDismiss` + `preventExpansion`)
220- **sourceViewTag**: iOS 26+ only (ignored on Android)
221
222## Files Reference
223
224### TypeScript
225- `index.ts` - Public API exports
226- `src/BottomSheet.types.ts` - TypeScript type definitions
227- `src/BottomSheet.tsx` - Native component (re-export)
228- `src/BottomSheet.web.tsx` - Web stub
229- `src/BottomSheetNativeComponent.tsx` - Native wrapper with portal integration
230- `src/BottomSheetNativeComponent.web.tsx` - Web stub for native component
231- `src/BottomSheetPortal.tsx` - Portal context and providers
232- `src/lib/Portal.tsx` - Generic portal implementation
233
234### iOS
235- `ios/BottomSheetModule.swift` - Module definition
236- `ios/SheetView.swift` - Main view implementation
237- `ios/SheetViewController.swift` - View controller for sheet presentation
238- `ios/SheetManager.swift` - Singleton for tracking active sheets
239- `ios/Util.swift` - Screen height utility
240
241### Android
242- `android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt` - Module definition
243- `android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt` - Main view implementation
244- `android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt` - Dialog root view group
245- `android/src/main/java/expo/modules/bottomsheet/SheetManager.kt` - Sheet tracking singleton
246
247### Configuration
248- `expo-module.config.json` - Expo module configuration