move into repo
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
group = 'expo.modules.bottomsheet'
|
||||
version = '0.1.0'
|
||||
|
||||
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
||||
apply from: expoModulesCorePlugin
|
||||
applyKotlinExpoModulesCorePlugin()
|
||||
useCoreDependencies()
|
||||
useExpoPublishing()
|
||||
|
||||
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
||||
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
||||
// Most of the time, you may like to manage the Android SDK versions yourself.
|
||||
def useManagedAndroidSdkVersions = false
|
||||
if (useManagedAndroidSdkVersions) {
|
||||
useDefaultAndroidSdkVersions()
|
||||
} else {
|
||||
buildscript {
|
||||
// Simple helper that allows the root project to override versions declared by this library.
|
||||
ext.safeExtGet = { prop, fallback ->
|
||||
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
||||
}
|
||||
}
|
||||
project.android {
|
||||
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
||||
defaultConfig {
|
||||
minSdkVersion safeExtGet("minSdkVersion", 21)
|
||||
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "expo.modules.bottomsheet"
|
||||
defaultConfig {
|
||||
versionCode 1
|
||||
versionName "0.1.0"
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
buildFeatures {
|
||||
compose true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.8"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation project(':expo-modules-core')
|
||||
implementation "androidx.compose.material3:material3:1.3.0"
|
||||
implementation "androidx.compose.material:material:1.7.2"
|
||||
implementation "com.facebook.react:react-native:+"
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
<manifest>
|
||||
</manifest>
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.core.view.allViews
|
||||
import com.facebook.react.ReactRootView
|
||||
import expo.modules.kotlin.AppContext
|
||||
import expo.modules.kotlin.viewevent.EventDispatcher
|
||||
import expo.modules.kotlin.views.ExpoView
|
||||
|
||||
class BottomSheetView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
||||
val sheetState = mutableStateOf(SheetState())
|
||||
|
||||
private var reactRootView: ReactRootView? = null
|
||||
private var innerView: View? = null
|
||||
|
||||
private var sheetView: ComposeView? = null
|
||||
|
||||
private val onStateChange by EventDispatcher()
|
||||
private val onAttemptDismiss by EventDispatcher()
|
||||
|
||||
// Props
|
||||
var preventDismiss = false
|
||||
var minHeight = 0f
|
||||
var maxHeight = 0f
|
||||
|
||||
private var isOpen: Boolean = false
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
|
||||
field = value
|
||||
this.sheetState.value.isOpen = value
|
||||
onStateChange(mapOf(
|
||||
"state" to if (value) "open" else "closed"
|
||||
))
|
||||
}
|
||||
|
||||
private var isOpening: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
onStateChange(mapOf("state" to "opening"))
|
||||
}
|
||||
}
|
||||
|
||||
private var isClosing: Boolean = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
onStateChange(mapOf("state" to "closing"))
|
||||
}
|
||||
}
|
||||
|
||||
private var hasInitiallyOpened = false
|
||||
|
||||
// Lifecycle
|
||||
|
||||
override fun addView(child: View?, index: Int) {
|
||||
this.innerView = child
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
||||
this.innerView?.let {
|
||||
val height = it.allViews.last().measuredHeight.toFloat()
|
||||
this.present(height)
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroy() {
|
||||
this.isClosing = false
|
||||
this.isOpen = false
|
||||
|
||||
this.getRootLayout().removeView(this.sheetView)
|
||||
this.sheetView = null
|
||||
|
||||
this.reactRootView = null
|
||||
this.innerView = null
|
||||
}
|
||||
|
||||
// Presentation
|
||||
|
||||
private fun present(contentHeight: Float) {
|
||||
val innerView = this.innerView ?: return
|
||||
|
||||
// For GestureRootView to work, we need to create a ReactRootView for the innerView to be
|
||||
// contained inside of
|
||||
val reactRootView = ReactRootView(context)
|
||||
reactRootView.addView(innerView)
|
||||
this.reactRootView = reactRootView
|
||||
|
||||
this.isOpening = true
|
||||
this.sheetView = ComposeView(context).also {
|
||||
it.setContent {
|
||||
SheetView(
|
||||
state = sheetState,
|
||||
innerView = reactRootView,
|
||||
contentHeight = innerView.height.toFloat(),
|
||||
onDismissRequest = {
|
||||
onAttemptDismiss(mapOf())
|
||||
if (!preventDismiss) {
|
||||
dismiss()
|
||||
}
|
||||
},
|
||||
onExpanded = {
|
||||
isOpening = false
|
||||
isOpen = true
|
||||
hasInitiallyOpened = true
|
||||
},
|
||||
onHidden = {
|
||||
if (hasInitiallyOpened) {
|
||||
destroy()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
getRootLayout().addView(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismiss() {
|
||||
this.isClosing = true
|
||||
this.destroy()
|
||||
}
|
||||
|
||||
// Utils
|
||||
|
||||
private fun getRootLayout(): FrameLayout {
|
||||
return appContext.currentActivity!!.findViewById(android.R.id.content)
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.graphics.Color
|
||||
import expo.modules.kotlin.modules.Module
|
||||
import expo.modules.kotlin.modules.ModuleDefinition
|
||||
|
||||
class BottomSheetModule : Module() {
|
||||
override fun definition() = ModuleDefinition {
|
||||
Name("BlueskyBottomSheet")
|
||||
|
||||
Function("getSafeAreaInset") {
|
||||
return@Function 10 // @TODO
|
||||
}
|
||||
|
||||
View(BottomSheetView::class) {
|
||||
Events(arrayOf(
|
||||
"onStateChange",
|
||||
"onAttemptDismiss",
|
||||
))
|
||||
|
||||
AsyncFunction("dismiss") { view: BottomSheetView ->
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
Prop("preventDismiss") { view: BottomSheetView, prop: Boolean ->
|
||||
view.preventDismiss = prop
|
||||
}
|
||||
|
||||
Prop("minHeight") { view: BottomSheetView, prop: Float ->
|
||||
view.minHeight = prop
|
||||
}
|
||||
|
||||
Prop("maxHeight") { view: BottomSheetView, prop: Float ->
|
||||
view.maxHeight = prop
|
||||
}
|
||||
|
||||
Prop("cornerRadius") { view: BottomSheetView, prop: Float ->
|
||||
view.sheetState.value.cornerRadius = prop
|
||||
}
|
||||
|
||||
Prop("containerBackgroundColor") { view: BottomSheetView, prop: String ->
|
||||
view.sheetState.value.containerBackgroundColor = Color.parseColor(prop)
|
||||
}
|
||||
|
||||
Prop("preventExpansion") { view: BottomSheetView, prop: Boolean ->
|
||||
view.sheetState.value.preventExpansion = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package expo.modules.bottomsheet
|
||||
|
||||
import android.view.View
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
|
||||
data class SheetState(
|
||||
var isOpen: Boolean = false,
|
||||
var cornerRadius: Float? = null,
|
||||
var containerBackgroundColor: Int ? = null,
|
||||
var preventExpansion: Boolean = false,
|
||||
)
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SheetView(
|
||||
state: MutableState<SheetState>,
|
||||
innerView: View,
|
||||
contentHeight: Float,
|
||||
onDismissRequest: () -> Unit,
|
||||
onExpanded: () -> Unit,
|
||||
onHidden: () -> Unit,
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false)
|
||||
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
onDismissRequest = onDismissRequest,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = state.value.cornerRadius ?: 0f,
|
||||
topEnd = state.value.cornerRadius ?: 0f,
|
||||
),
|
||||
containerColor = Color(state.value.containerBackgroundColor ?: android.graphics.Color.TRANSPARENT),
|
||||
) {
|
||||
Column(
|
||||
Modifier.fillMaxWidth()
|
||||
.height(contentHeight.dp)
|
||||
// Prevent covering up the handle
|
||||
.padding(top = 34.dp)
|
||||
) {
|
||||
AndroidView(
|
||||
factory = { innerView }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(sheetState.currentValue) {
|
||||
if (sheetState.currentValue == SheetValue.PartiallyExpanded || sheetState.currentValue == SheetValue.Expanded) {
|
||||
onExpanded()
|
||||
} else if (sheetState.currentValue == SheetValue.Hidden) {
|
||||
onHidden()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
Pod::Spec.new do |s|
|
||||
s.name = 'BottomSheet'
|
||||
s.version = '1.0.0'
|
||||
s.summary = 'A bottom sheet for use in Bluesky'
|
||||
s.description = 'A bottom sheet for use in Bluesky'
|
||||
s.author = ''
|
||||
s.homepage = 'https://github.com/bluesky-social/social-app'
|
||||
s.platforms = { :ios => '15.0', :tvos => '15.0' }
|
||||
s.source = { git: '' }
|
||||
s.static_framework = true
|
||||
|
||||
s.dependency 'ExpoModulesCore'
|
||||
|
||||
# Swift/Objective-C compatibility
|
||||
s.pod_target_xcconfig = {
|
||||
'DEFINES_MODULE' => 'YES',
|
||||
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
||||
}
|
||||
|
||||
s.source_files = "**/*.{h,m,swift}"
|
||||
end
|
||||
@@ -0,0 +1,46 @@
|
||||
import ExpoModulesCore
|
||||
|
||||
public class BottomSheetModule: Module {
|
||||
public func definition() -> ModuleDefinition {
|
||||
Name("BottomSheet")
|
||||
|
||||
AsyncFunction("dismissAll") {
|
||||
SheetManager.shared.dismissAll()
|
||||
}
|
||||
|
||||
View(SheetView.self) {
|
||||
Events([
|
||||
"onStateChange",
|
||||
"onAttemptDismiss"
|
||||
])
|
||||
|
||||
AsyncFunction("dismiss") { (view: SheetView) in
|
||||
view.dismiss()
|
||||
}
|
||||
|
||||
AsyncFunction("updateLayout") { (view: SheetView) in
|
||||
view.updateLayout()
|
||||
}
|
||||
|
||||
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||
view.cornerRadius = CGFloat(prop)
|
||||
}
|
||||
|
||||
Prop("preventDismiss") { (view: SheetView, prop: Bool) in
|
||||
view.preventDismiss = prop
|
||||
}
|
||||
|
||||
Prop("minHeight") { (view: SheetView, prop: Double) in
|
||||
view.minHeight = prop
|
||||
}
|
||||
|
||||
Prop("maxHeight") { (view: SheetView, prop: Double) in
|
||||
view.maxHeight = prop
|
||||
}
|
||||
|
||||
Prop("preventExpansion") { (view: SheetView, prop: Bool) in
|
||||
view.preventExpansion = prop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// SheetManager.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 10/1/24.
|
||||
//
|
||||
|
||||
import ExpoModulesCore
|
||||
|
||||
class SheetManager {
|
||||
static let shared = SheetManager()
|
||||
|
||||
private var sheetViews = NSHashTable<SheetView>(options: .weakMemory)
|
||||
|
||||
func add(_ view: SheetView) {
|
||||
sheetViews.add(view)
|
||||
}
|
||||
|
||||
func remove(_ view: SheetView) {
|
||||
sheetViews.remove(view)
|
||||
}
|
||||
|
||||
func dismissAll() {
|
||||
sheetViews.allObjects.forEach { sheetView in
|
||||
sheetView.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import ExpoModulesCore
|
||||
import UIKit
|
||||
|
||||
class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
||||
// Views
|
||||
private var sheetVc: SheetViewController?
|
||||
private var innerView: UIView?
|
||||
|
||||
// Events
|
||||
private let onStateChange = EventDispatcher()
|
||||
private let onAttemptDismiss = EventDispatcher()
|
||||
|
||||
// Open event firing
|
||||
private var isOpen: Bool = false {
|
||||
didSet {
|
||||
onStateChange([
|
||||
"state": isOpen ? "open" : "closed"
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
// React view props
|
||||
var preventDismiss = false
|
||||
var preventExpansion = false
|
||||
var cornerRadius: CGFloat?
|
||||
var minHeight = 0.0
|
||||
var maxHeight: CGFloat! {
|
||||
didSet {
|
||||
let screenHeight = Util.getScreenHeight() ?? 0
|
||||
if maxHeight > screenHeight {
|
||||
maxHeight = screenHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isOpening = false {
|
||||
didSet {
|
||||
if isOpening {
|
||||
onStateChange([
|
||||
"state": "opening"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
private var isClosing = false {
|
||||
didSet {
|
||||
if isClosing {
|
||||
onStateChange([
|
||||
"state": "closing"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
required init (appContext: AppContext? = nil) {
|
||||
super.init(appContext: appContext)
|
||||
self.maxHeight = Util.getScreenHeight()
|
||||
SheetManager.shared.add(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.destroy()
|
||||
}
|
||||
|
||||
// We don't want this view to actually get added to the tree, so we'll simply store it for adding
|
||||
// to the SheetViewController
|
||||
override func insertReactSubview(_ subview: UIView!, at atIndex: Int) {
|
||||
self.innerView = subview
|
||||
}
|
||||
|
||||
// We'll grab the content height from here so we know the initial detent to set
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
guard let innerView = self.innerView else {
|
||||
return
|
||||
}
|
||||
|
||||
if innerView.subviews.count != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
self.present(contentHeight: innerView.subviews[0].frame.size.height)
|
||||
}
|
||||
|
||||
private func destroy() {
|
||||
self.isClosing = false
|
||||
self.isOpen = false
|
||||
self.sheetVc = nil
|
||||
self.innerView = nil
|
||||
SheetManager.shared.remove(self)
|
||||
}
|
||||
|
||||
// MARK: - Presentation
|
||||
|
||||
func present(contentHeight: CGFloat) {
|
||||
guard !self.isOpen,
|
||||
let innerView = self.innerView,
|
||||
let rvc = self.reactViewController() else {
|
||||
return
|
||||
}
|
||||
|
||||
let sheetVc = SheetViewController()
|
||||
if let sheet = sheetVc.sheetPresentationController {
|
||||
sheet.delegate = self
|
||||
sheet.preferredCornerRadius = self.cornerRadius
|
||||
}
|
||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
||||
sheetVc.view.addSubview(innerView)
|
||||
|
||||
self.sheetVc = sheetVc
|
||||
self.isOpening = true
|
||||
|
||||
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||
self?.isOpening = false
|
||||
self?.isOpen = true
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout() {
|
||||
if let contentHeight = self.innerView?.subviews[0].frame.size.height {
|
||||
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
||||
preventExpansion: self.preventExpansion)
|
||||
}
|
||||
}
|
||||
|
||||
func dismiss() {
|
||||
self.isClosing = true
|
||||
self.sheetVc?.dismiss(animated: true) { [weak self] in
|
||||
self?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Utils
|
||||
|
||||
private func clampHeight(_ height: CGFloat) -> CGFloat {
|
||||
if height < self.minHeight {
|
||||
return self.minHeight
|
||||
} else if height > self.maxHeight {
|
||||
return self.maxHeight
|
||||
}
|
||||
return height
|
||||
}
|
||||
|
||||
// MARK: - UISheetPresentationControllerDelegate
|
||||
|
||||
func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
|
||||
self.onAttemptDismiss()
|
||||
return !self.preventDismiss
|
||||
}
|
||||
|
||||
func presentationControllerWillDismiss(_ presentationController: UIPresentationController) {
|
||||
self.isClosing = true
|
||||
}
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// SheetViewController.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 9/30/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
class SheetViewController: UIViewController {
|
||||
init() {
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.modalPresentationStyle = .formSheet
|
||||
self.isModalInPresentation = false
|
||||
|
||||
if let sheet = self.sheetPresentationController {
|
||||
sheet.prefersGrabberVisible = true
|
||||
}
|
||||
}
|
||||
|
||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
guard let sheet = self.sheetPresentationController,
|
||||
let screenHeight = Util.getScreenHeight()
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
if contentHeight > screenHeight {
|
||||
sheet.detents = [
|
||||
.large()
|
||||
]
|
||||
} else {
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.detents = [
|
||||
.custom { _ in
|
||||
return contentHeight
|
||||
}
|
||||
]
|
||||
} else {
|
||||
sheet.detents = [
|
||||
.medium()
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if !preventExpansion {
|
||||
sheet.detents.append(.large())
|
||||
}
|
||||
}
|
||||
|
||||
func updateDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
||||
if let sheet = self.sheetPresentationController {
|
||||
sheet.animateChanges {
|
||||
self.setDetents(contentHeight: contentHeight, preventExpansion: preventExpansion)
|
||||
if #available(iOS 16.0, *) {
|
||||
sheet.invalidateDetents()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Util.swift
|
||||
// Pods
|
||||
//
|
||||
// Created by Hailey on 10/2/24.
|
||||
//
|
||||
|
||||
class Util {
|
||||
static func getScreenHeight() -> CGFloat? {
|
||||
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
|
||||
let window = windowScene.windows.first {
|
||||
let safeAreaInsets = window.safeAreaInsets
|
||||
let fullScreenHeight = UIScreen.main.bounds.height
|
||||
return fullScreenHeight - (safeAreaInsets.top + safeAreaInsets.bottom)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react'
|
||||
import {ColorValue, NativeSyntheticEvent} from 'react-native'
|
||||
|
||||
export type BottomSheetState = 'closed' | 'closing' | 'open' | 'opening'
|
||||
|
||||
export interface BottomSheetViewProps {
|
||||
children: React.ReactNode
|
||||
cornerRadius?: number
|
||||
preventDismiss?: boolean
|
||||
preventExpansion?: boolean
|
||||
containerBackgroundColor?: ColorValue
|
||||
topInset?: number
|
||||
bottomInset?: number
|
||||
|
||||
minHeight?: number
|
||||
maxHeight?: number
|
||||
|
||||
onStateChange?: (
|
||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||
) => void
|
||||
onAttemptDismiss?: (event: NativeSyntheticEvent<object>) => void
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react'
|
||||
import {
|
||||
ColorValue,
|
||||
Dimensions,
|
||||
NativeSyntheticEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native'
|
||||
import {requireNativeViewManager} from 'expo-modules-core'
|
||||
|
||||
import {BottomSheetState, BottomSheetViewProps} from './BottomSheet.types'
|
||||
|
||||
const screenHeight = Dimensions.get('screen').height
|
||||
|
||||
const NativeView: React.ComponentType<
|
||||
BottomSheetViewProps & {
|
||||
ref: React.RefObject<any>
|
||||
style: StyleProp<ViewStyle>
|
||||
}
|
||||
> = requireNativeViewManager('BlueskyBottomSheet')
|
||||
|
||||
export class BottomSheetView extends React.Component<
|
||||
BottomSheetViewProps,
|
||||
{
|
||||
open: boolean
|
||||
}
|
||||
> {
|
||||
ref = React.createRef<any>()
|
||||
|
||||
constructor(props: BottomSheetViewProps) {
|
||||
super(props)
|
||||
this.state = {
|
||||
open: false,
|
||||
}
|
||||
}
|
||||
|
||||
present() {
|
||||
this.setState({open: true})
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.ref.current?.dismiss()
|
||||
}
|
||||
|
||||
private onStateChange = (
|
||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||
) => {
|
||||
const {state} = event.nativeEvent
|
||||
const isOpen = state !== 'closed'
|
||||
this.setState({open: isOpen})
|
||||
this.props.onStateChange?.(event)
|
||||
}
|
||||
|
||||
private getBackgroundColor = (): ColorValue | undefined => {
|
||||
const parent = React.Children.toArray(
|
||||
this.props.children,
|
||||
)[0] as React.ReactElement
|
||||
if (parent?.props?.style) {
|
||||
const parentStyle = StyleSheet.flatten(parent.props.style) as ViewStyle
|
||||
return parentStyle.backgroundColor ?? 'transparent'
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
private updateLayout = () => {
|
||||
this.ref.current?.updateLayout()
|
||||
}
|
||||
|
||||
render() {
|
||||
const {children, ...rest} = this.props
|
||||
const topInset = rest.topInset ?? 0
|
||||
const bottomInset = rest.bottomInset ?? 0
|
||||
|
||||
if (!this.state.open) {
|
||||
return null
|
||||
}
|
||||
|
||||
const backgroundColor = this.getBackgroundColor()
|
||||
return (
|
||||
<NativeView
|
||||
{...rest}
|
||||
onStateChange={this.onStateChange}
|
||||
ref={this.ref}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
height: screenHeight - topInset - bottomInset,
|
||||
width: '100%',
|
||||
}}
|
||||
containerBackgroundColor={backgroundColor}>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor,
|
||||
paddingTop: topInset,
|
||||
paddingBottom: bottomInset,
|
||||
}}>
|
||||
<View onLayout={this.updateLayout}>{children}</View>
|
||||
</View>
|
||||
</NativeView>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import {BottomSheetViewProps} from './BottomSheet.types'
|
||||
|
||||
export function BottomSheetView(_: BottomSheetViewProps) {
|
||||
throw new Error('BottomSheetView is not available on web')
|
||||
}
|
||||
@@ -68,7 +68,6 @@
|
||||
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.1.1",
|
||||
"@fortawesome/react-native-fontawesome": "^0.3.2",
|
||||
"@haileyok/bluesky-bottom-sheet": "^0.1.1-alpha.11",
|
||||
"@haileyok/bluesky-video": "0.1.10",
|
||||
"@lingui/react": "^4.5.0",
|
||||
"@mattermost/react-native-paste-input": "^0.7.1",
|
||||
|
||||
@@ -4120,11 +4120,6 @@
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
|
||||
"@haileyok/bluesky-bottom-sheet@^0.1.1-alpha.11":
|
||||
version "0.1.1-alpha.11"
|
||||
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-bottom-sheet/-/bluesky-bottom-sheet-0.1.1-alpha.11.tgz#a9c0e1c1903ba066c185481958f4ea9519cd5ba3"
|
||||
integrity sha512-RtiEpEIXtejBzCtspFO7oEw6n87LnVQuSz+FGGEQZ6ywnRUBy6HZ5bpE3IFI7mmyF2YjAPg2nLzsqR9NXRMlhQ==
|
||||
|
||||
"@haileyok/bluesky-video@0.1.10":
|
||||
version "0.1.10"
|
||||
resolved "https://registry.yarnpkg.com/@haileyok/bluesky-video/-/bluesky-video-0.1.10.tgz#2756e8c83a78caeb6b120a175578eac1eb6889a9"
|
||||
|
||||
Reference in New Issue
Block a user