Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 248 lines 9.2 kB view raw view rendered
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