Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at cope-settings-sync 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