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.

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#

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#

import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet'

// In your app root:
function App() {
  return (
    <BottomSheetProvider>
      <YourApp />
      <BottomSheetOutlet />
    </BottomSheetProvider>
  )
}

// In a component:
function MyComponent() {
  const sheetRef = useRef<BottomSheet>(null)
  
  const openSheet = () => {
    sheetRef.current?.present()
  }
  
  const closeSheet = () => {
    sheetRef.current?.dismiss()
  }
  
  return (
    <>
      <Button onPress={openSheet} title="Open Sheet" />
      
      <BottomSheet
        ref={sheetRef}
        cornerRadius={16}
        backgroundColor="white"
        onStateChange={(e) => console.log(e.nativeEvent.state)}
      >
        <View style={{padding: 20}}>
          <Text>Sheet content</Text>
          <Button onPress={closeSheet} title="Close" />
        </View>
      </BottomSheet>
    </>
  )
}

Nested Sheets#

The module supports nesting sheets by using BottomSheetPortalProvider within sheet content:

<BottomSheet ref={outerSheetRef}>
  <BottomSheetPortalProvider>
    <Button onPress={() => innerSheetRef.current?.present()} />
    <BottomSheet ref={innerSheetRef}>
      <Text>Inner sheet content</Text>
    </BottomSheet>
  </BottomSheetPortalProvider>
</BottomSheet>

Dismiss All Sheets#

import {BottomSheetNativeComponent} from '@modules/bottom-sheet'

BottomSheetNativeComponent.dismissAll()

Key Implementation Details#

iOS Specific#

  1. 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.

  2. iOS 26+ Zoom Transitions: When sourceViewTag is provided on iOS 26+, the sheet zooms from the specified view.

  3. Detent Selection: The module automatically chooses between custom detents, .medium(), and .large() based on content height and screen size.

Android Specific#

  1. Edge-to-Edge: The module handles edge-to-edge display correctly across API levels:

    • API 35+: Mandatory edge-to-edge
    • API 30-34: Uses currentWindowMetrics
    • API <30: Uses deprecated getRealSize()
  2. Status/Nav Bar Appearance: Preserves light/dark appearance from the host activity and reapplies it to the sheet dialog.

  3. 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).

  4. Layout Updates During Gestures: Content height changes are deferred during drag gestures to prevent fighting the user's input.

Platform Differences#

  • cornerRadius: Applied to sheet on iOS, to content wrapper on Android (Android clips with overflow: hidden)
  • disableDrag: Android-only prop (iOS drag behavior is controlled via preventDismiss + preventExpansion)
  • sourceViewTag: iOS 26+ only (ignored on Android)

Files Reference#

TypeScript#

  • index.ts - Public API exports
  • src/BottomSheet.types.ts - TypeScript type definitions
  • src/BottomSheet.tsx - Native component (re-export)
  • src/BottomSheet.web.tsx - Web stub
  • src/BottomSheetNativeComponent.tsx - Native wrapper with portal integration
  • src/BottomSheetNativeComponent.web.tsx - Web stub for native component
  • src/BottomSheetPortal.tsx - Portal context and providers
  • src/lib/Portal.tsx - Generic portal implementation

iOS#

  • ios/BottomSheetModule.swift - Module definition
  • ios/SheetView.swift - Main view implementation
  • ios/SheetViewController.swift - View controller for sheet presentation
  • ios/SheetManager.swift - Singleton for tracking active sheets
  • ios/Util.swift - Screen height utility

Android#

  • android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt - Module definition
  • android/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt - Main view implementation
  • android/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt - Dialog root view group
  • android/src/main/java/expo/modules/bottomsheet/SheetManager.kt - Sheet tracking singleton

Configuration#

  • expo-module.config.json - Expo module configuration