# Bottom Sheet Expo Module A 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). ## Overview This 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. Key features: - Native bottom sheet presentation on iOS and Android - Automatic content height detection (no JS bridge round-trip) - Configurable snap points (hidden, partial, full) - Drag-to-dismiss with prevention controls - Portal-based rendering for proper z-index layering - Edge-to-edge support on modern Android versions - iOS 26+ zoom transition support ## Platform Support - **iOS**: Uses `UISheetPresentationController` (iOS 15+) - **Android**: Uses Material Design `BottomSheetDialog` with `BottomSheetBehavior` - **Web**: Not supported (throws error) ## Architecture ### TypeScript Layer The module exposes a React component that handles rendering and state management: - **BottomSheet.tsx** (Native): Main component wrapping the native view - **BottomSheet.web.tsx** (Web): Stub that throws an error - **BottomSheetNativeComponent.tsx**: React wrapper with portal integration - **BottomSheetPortal.tsx**: Portal system for rendering sheets above app content - **Portal.tsx**: Generic portal implementation for managing component hierarchy The component uses a class-based approach to expose imperative methods (`present()`, `dismiss()`, `dismissAll()`). ### Native Layer #### iOS Implementation - **BottomSheetModule.swift**: Expo module definition with event handlers and prop bindings - **SheetView.swift**: Main view component that creates and manages `SheetViewController` - Observes content height via KVO (Key-Value Observing) on bounds - Manages sheet lifecycle and state transitions - Implements `UISheetPresentationControllerDelegate` for drag events - **SheetViewController.swift**: UIViewController subclass with sheet presentation - Configures detents (snap points) based on content height - Handles iOS 26+ safe area adjustments for floating sheet style - Animates detent changes when content resizes - **SheetManager.swift**: Singleton that tracks all active sheets with weak references - **Util.swift**: Helper for calculating screen height minus safe area insets #### Android Implementation - **BottomSheetModule.kt**: Expo module definition mirroring iOS functionality - **BottomSheetView.kt**: Main view component managing Material BottomSheetDialog - Uses `OnLayoutChangeListener` to observe content height natively - Configures `BottomSheetBehavior` for drag and snap behavior - Handles edge-to-edge display across Android versions (API 29-35+) - Preserves status/nav bar appearance from host activity - **DialogRootViewGroup.kt**: Custom ViewGroup acting as RootView for the dialog - Forwards touch events to React Native event system - Updates shadow node size to match window dimensions - Based on React Native's ReactModalHostView pattern - **SheetManager.kt**: Singleton for tracking sheets (same pattern as iOS) ### Content Height Detection Both platforms detect content height changes natively without JS bridge round-trips: - **iOS**: KVO observation on the content view's `bounds` property - **Android**: `OnLayoutChangeListener` on child views (catches React Native's direct `layout()` calls) This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading). ## Props ```typescript interface BottomSheetViewProps { children: React.ReactNode // Appearance cornerRadius?: number backgroundColor?: ColorValue containerBackgroundColor?: ColorValue // Behavior preventDismiss?: boolean // Disable swipe-to-dismiss preventExpansion?: boolean // Lock to initial height (no full-screen) disableDrag?: boolean // Disable drag handle (Android only) fullHeight?: boolean // Start at full screen height // Height constraints minHeight?: number // Minimum height in dp maxHeight?: number // Maximum height in dp // iOS 26+ transition sourceViewTag?: number // View tag for zoom transition origin // Events onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void onStateChange?: (event: BottomSheetStateChangeEvent) => void } ``` ## States and Snap Points ### States - `closed`: Sheet is dismissed - `closing`: Sheet is animating closed - `open`: Sheet is fully visible - `opening`: Sheet is animating open ### Snap Points - `Hidden` (0): Dismissed - `Partial` (1): Half-expanded / content height - `Full` (2): Expanded to screen height ## Usage ### Basic Example ```tsx import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet' // In your app root: function App() { return ( ) } // In a component: function MyComponent() { const sheetRef = useRef(null) const openSheet = () => { sheetRef.current?.present() } const closeSheet = () => { sheetRef.current?.dismiss() } return ( <>