The Great Unjanking of the Sheets (#9973)
This commit is contained in:
+2
-2
@@ -25,8 +25,8 @@ class BottomSheetModule : Module() {
|
|||||||
view.dismiss()
|
view.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("updateLayout") { view: BottomSheetView ->
|
Prop("fullHeight") { view: BottomSheetView, prop: Boolean ->
|
||||||
view.updateLayout()
|
view.fullHeight = prop
|
||||||
}
|
}
|
||||||
|
|
||||||
Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
|
Prop("disableDrag") { view: BottomSheetView, prop: Boolean ->
|
||||||
|
|||||||
+138
-46
@@ -8,10 +8,7 @@ import android.view.ViewStructure
|
|||||||
import android.view.Window
|
import android.view.Window
|
||||||
import android.view.accessibility.AccessibilityEvent
|
import android.view.accessibility.AccessibilityEvent
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.allViews
|
|
||||||
import com.facebook.react.bridge.LifecycleEventListener
|
import com.facebook.react.bridge.LifecycleEventListener
|
||||||
import com.facebook.react.bridge.ReactContext
|
import com.facebook.react.bridge.ReactContext
|
||||||
import com.facebook.react.bridge.UiThreadUtil
|
import com.facebook.react.bridge.UiThreadUtil
|
||||||
@@ -34,11 +31,20 @@ class BottomSheetView(
|
|||||||
|
|
||||||
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
private lateinit var dialogRootViewGroup: DialogRootViewGroup
|
||||||
private var eventDispatcher: EventDispatcher? = null
|
private var eventDispatcher: EventDispatcher? = null
|
||||||
private var isKeyboardVisible: Boolean = false
|
|
||||||
|
|
||||||
private val screenHeight =
|
// Native content height observation (eliminates JS bridge round-trip)
|
||||||
context.resources.displayMetrics.heightPixels
|
private var contentLayoutListener: View.OnLayoutChangeListener? = null
|
||||||
.toFloat()
|
private var observedChildren: List<View> = emptyList()
|
||||||
|
private var lastObservedContentHeight: Float = 0f
|
||||||
|
private var pendingLayoutUpdate: Boolean = false
|
||||||
|
|
||||||
|
private val screenHeight: Float =
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
|
context.resources.displayMetrics.heightPixels.toFloat()
|
||||||
|
} else {
|
||||||
|
val wm = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager
|
||||||
|
wm.currentWindowMetrics.bounds.height().toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getNavigationBarHeight(): Int {
|
private fun getNavigationBarHeight(): Int {
|
||||||
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
val resourceId = resources.getIdentifier("navigation_bar_height", "dimen", "android")
|
||||||
@@ -64,7 +70,14 @@ class BottomSheetView(
|
|||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
field = value
|
||||||
this.dialog?.setCancelable(!value)
|
this.dialog?.setCancelable(!value)
|
||||||
|
// Full-height sheets have no half-expanded snap point, so any drag
|
||||||
|
// would dismiss. Disable dragging when dismiss is prevented.
|
||||||
|
if (fullHeight) {
|
||||||
|
this.setDraggable(!value && !disableDrag)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var fullHeight = false
|
||||||
|
|
||||||
var preventExpansion = false
|
var preventExpansion = false
|
||||||
|
|
||||||
@@ -129,6 +142,7 @@ class BottomSheetView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun destroy() {
|
private fun destroy() {
|
||||||
|
this.stopObservingContentHeight()
|
||||||
this.isClosing = false
|
this.isClosing = false
|
||||||
this.isOpen = false
|
this.isOpen = false
|
||||||
this.dialog = null
|
this.dialog = null
|
||||||
@@ -193,20 +207,28 @@ class BottomSheetView(
|
|||||||
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
||||||
bottomSheet?.let {
|
bottomSheet?.let {
|
||||||
it.setBackgroundColor(0)
|
it.setBackgroundColor(0)
|
||||||
|
it.elevation = 0f
|
||||||
|
|
||||||
val behavior = BottomSheetBehavior.from(it)
|
val behavior = BottomSheetBehavior.from(it)
|
||||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
behavior.isFitToContents = true
|
|
||||||
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
|
||||||
behavior.skipCollapsed = true
|
behavior.skipCollapsed = true
|
||||||
behavior.isDraggable = true
|
behavior.isDraggable = true
|
||||||
behavior.isHideable = true
|
behavior.isHideable = true
|
||||||
|
if (fullHeight) {
|
||||||
if (preventExpansion) {
|
behavior.isFitToContents = false
|
||||||
|
behavior.expandedOffset = getStatusBarHeight()
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
this.selectedSnapPoint = 2
|
||||||
|
} else if (preventExpansion) {
|
||||||
|
behavior.isFitToContents = true
|
||||||
|
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||||
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
this.selectedSnapPoint = 1
|
||||||
} else {
|
} else {
|
||||||
behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt()
|
behavior.isFitToContents = false
|
||||||
}
|
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
|
||||||
|
behavior.expandedOffset = getStatusBarHeight()
|
||||||
|
|
||||||
val targetHeight = this.getTargetHeight()
|
val targetHeight = this.getTargetHeight()
|
||||||
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
||||||
@@ -219,6 +241,7 @@ class BottomSheetView(
|
|||||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
this.selectedSnapPoint = 1
|
this.selectedSnapPoint = 1
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
behavior.addBottomSheetCallback(
|
behavior.addBottomSheetCallback(
|
||||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
@@ -226,12 +249,23 @@ class BottomSheetView(
|
|||||||
bottomSheet: View,
|
bottomSheet: View,
|
||||||
newState: Int,
|
newState: Int,
|
||||||
) {
|
) {
|
||||||
|
if (newState == BottomSheetBehavior.STATE_EXPANDED && preventExpansion) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
return
|
||||||
|
}
|
||||||
when (newState) {
|
when (newState) {
|
||||||
BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2
|
BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2
|
||||||
BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1
|
BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1
|
||||||
BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1
|
BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1
|
||||||
BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0
|
BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0
|
||||||
}
|
}
|
||||||
|
// Apply deferred layout update after gesture completes
|
||||||
|
if (newState != BottomSheetBehavior.STATE_DRAGGING &&
|
||||||
|
newState != BottomSheetBehavior.STATE_SETTLING &&
|
||||||
|
pendingLayoutUpdate) {
|
||||||
|
pendingLayoutUpdate = false
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSlide(
|
override fun onSlide(
|
||||||
@@ -245,25 +279,14 @@ class BottomSheetView(
|
|||||||
this.isOpening = true
|
this.isOpening = true
|
||||||
dialog.show()
|
dialog.show()
|
||||||
this.dialog = dialog
|
this.dialog = dialog
|
||||||
|
if (!fullHeight) {
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets ->
|
this.startObservingContentHeight()
|
||||||
val imeVisible = insets.isVisible(WindowInsetsCompat.Type.ime())
|
|
||||||
val bottomSheet = dialog.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
|
|
||||||
val behavior = bottomSheet?.let { BottomSheetBehavior.from(it) }
|
|
||||||
|
|
||||||
val wasKeyboardVisible = isKeyboardVisible
|
|
||||||
isKeyboardVisible = imeVisible
|
|
||||||
|
|
||||||
if (imeVisible && behavior?.state == BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
|
||||||
} else if (!imeVisible && wasKeyboardVisible) {
|
|
||||||
updateLayout()
|
|
||||||
}
|
|
||||||
insets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLayout() {
|
fun updateLayout() {
|
||||||
|
if (fullHeight) return
|
||||||
val dialog = this.dialog ?: return
|
val dialog = this.dialog ?: return
|
||||||
val contentHeight = this.getContentHeight()
|
val contentHeight = this.getContentHeight()
|
||||||
|
|
||||||
@@ -274,21 +297,34 @@ class BottomSheetView(
|
|||||||
|
|
||||||
val oldRatio = behavior.halfExpandedRatio
|
val oldRatio = behavior.halfExpandedRatio
|
||||||
val newRatio = getHalfExpandedRatio(contentHeight)
|
val newRatio = getHalfExpandedRatio(contentHeight)
|
||||||
behavior.halfExpandedRatio = newRatio
|
|
||||||
|
|
||||||
if (preventExpansion) {
|
|
||||||
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
val targetHeight = this.getTargetHeight()
|
val targetHeight = this.getTargetHeight()
|
||||||
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
|
||||||
val shouldBeExpanded = targetHeight >= availableHeight
|
val shouldBeExpanded = targetHeight >= availableHeight
|
||||||
|
|
||||||
if (isKeyboardVisible) {
|
// Don't update during user gestures — defer until the gesture completes.
|
||||||
if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
|
if (currentState == BottomSheetBehavior.STATE_DRAGGING) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
pendingLayoutUpdate = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
|
|
||||||
|
behavior.halfExpandedRatio = newRatio
|
||||||
|
|
||||||
|
if (preventExpansion) {
|
||||||
|
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
|
||||||
|
it.requestLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// During settling (programmatic animation from our own state change),
|
||||||
|
// redirect the animation to the new position if the ratio changed.
|
||||||
|
if (currentState == BottomSheetBehavior.STATE_SETTLING) {
|
||||||
|
if (oldRatio != newRatio) {
|
||||||
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
} else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
} else if (!shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_HALF_EXPANDED) {
|
||||||
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
|
||||||
@@ -299,21 +335,77 @@ class BottomSheetView(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun dismiss() {
|
fun dismiss() {
|
||||||
this.dialog?.dismiss()
|
val dialog = this.dialog ?: return
|
||||||
|
// Mark as closing so the content observer doesn't fight the dismiss
|
||||||
|
// animation by calling updateLayout() mid-hide.
|
||||||
|
this.isClosing = true
|
||||||
|
// Temporarily make cancelable so cancel() works — cancel() gives the
|
||||||
|
// slide-out animation, while dismiss() does a plain fade.
|
||||||
|
dialog.setCancelable(true)
|
||||||
|
dialog.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Observe each direct child of innerView via OnLayoutChangeListener so that
|
||||||
|
// height updates are detected purely on the native side. We use OnLayoutChangeListener
|
||||||
|
// (not OnGlobalLayoutListener) because React Native calls view.layout() directly
|
||||||
|
// via Yoga, bypassing requestLayout()/performTraversals(). OnLayoutChangeListener
|
||||||
|
// fires from setFrame() which IS called by layout(), so it catches RN updates.
|
||||||
|
private fun startObservingContentHeight() {
|
||||||
|
stopObservingContentHeight()
|
||||||
|
|
||||||
|
val innerViewGroup = this.innerView as? ViewGroup ?: return
|
||||||
|
|
||||||
|
val listener = View.OnLayoutChangeListener { _, _, top, _, bottom, _, _, oldTop, oldBottom ->
|
||||||
|
val newHeight = bottom - top
|
||||||
|
val oldHeight = oldBottom - oldTop
|
||||||
|
if (newHeight != oldHeight) {
|
||||||
|
val contentHeight = getContentHeight()
|
||||||
|
if (contentHeight != lastObservedContentHeight && contentHeight > 0 && (isOpen || isOpening) && !isClosing) {
|
||||||
|
lastObservedContentHeight = contentHeight
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val children = mutableListOf<View>()
|
||||||
|
for (i in 0 until innerViewGroup.childCount) {
|
||||||
|
val child = innerViewGroup.getChildAt(i)
|
||||||
|
child.addOnLayoutChangeListener(listener)
|
||||||
|
children.add(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.contentLayoutListener = listener
|
||||||
|
this.observedChildren = children
|
||||||
|
|
||||||
|
// Pick up current height if content is already laid out
|
||||||
|
val contentHeight = getContentHeight()
|
||||||
|
if (contentHeight > 0 && contentHeight != lastObservedContentHeight) {
|
||||||
|
lastObservedContentHeight = contentHeight
|
||||||
|
updateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopObservingContentHeight() {
|
||||||
|
contentLayoutListener?.let { listener ->
|
||||||
|
observedChildren.forEach { it.removeOnLayoutChangeListener(listener) }
|
||||||
|
}
|
||||||
|
contentLayoutListener = null
|
||||||
|
observedChildren = emptyList()
|
||||||
|
lastObservedContentHeight = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
// Util
|
// Util
|
||||||
|
|
||||||
private fun getContentHeight(): Float {
|
private fun getContentHeight(): Float {
|
||||||
val innerView = this.innerView ?: return 0f
|
val innerView = this.innerView as? ViewGroup ?: return 0f
|
||||||
var index = 0
|
// Use the tallest direct child's height. The handle is absolutely positioned
|
||||||
innerView.allViews.forEach {
|
// (overlaps the content), so summing would double-count its height as padding.
|
||||||
if (index == 1) {
|
var maxChildHeight = 0f
|
||||||
return it.height.toFloat()
|
for (i in 0 until innerView.childCount) {
|
||||||
|
val h = innerView.getChildAt(i).height.toFloat()
|
||||||
|
if (h > maxChildHeight) maxChildHeight = h
|
||||||
}
|
}
|
||||||
index++
|
return maxChildHeight
|
||||||
}
|
|
||||||
return 0f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getTargetHeight(): Float {
|
private fun getTargetHeight(): Float {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<style name="EdgeToEdgeBottomSheetDialogTheme" parent="Theme.Material3.DayNight.BottomSheetDialog">
|
<style name="EdgeToEdgeBottomSheetDialogTheme" parent="ThemeOverlay.Material3.DayNight.BottomSheetDialog">
|
||||||
<!-- Enable edge-to-edge -->
|
<!-- Enable edge-to-edge, matching react-native-edge-to-edge's setup -->
|
||||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
<item name="android:windowIsFloating">false</item>
|
<item name="android:windowIsFloating">false</item>
|
||||||
|
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||||
|
<item name="android:fitsSystemWindows">false</item>
|
||||||
<item name="enableEdgeToEdge">true</item>
|
<item name="enableEdgeToEdge">true</item>
|
||||||
|
|
||||||
<!-- Configure bottom sheet to respect system window insets -->
|
<!-- Configure bottom sheet to respect system window insets -->
|
||||||
@@ -16,5 +18,6 @@
|
|||||||
<item name="paddingLeftSystemWindowInsets">true</item>
|
<item name="paddingLeftSystemWindowInsets">true</item>
|
||||||
<item name="paddingRightSystemWindowInsets">true</item>
|
<item name="paddingRightSystemWindowInsets">true</item>
|
||||||
<item name="paddingTopSystemWindowInsets">false</item>
|
<item name="paddingTopSystemWindowInsets">false</item>
|
||||||
|
<item name="backgroundTint">@android:color/transparent</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ public class BottomSheetModule: Module {
|
|||||||
view.dismiss()
|
view.dismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncFunction("updateLayout") { (view: SheetView) in
|
Prop("fullHeight") { (view: SheetView, prop: Bool) in
|
||||||
view.updateLayout()
|
view.fullHeight = prop
|
||||||
}
|
}
|
||||||
|
|
||||||
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
Prop("cornerRadius") { (view: SheetView, prop: Float) in
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
private var innerView: UIView?
|
private var innerView: UIView?
|
||||||
private var touchHandler: RCTTouchHandler?
|
private var touchHandler: RCTTouchHandler?
|
||||||
|
|
||||||
|
// Native content height observation (eliminates JS bridge round-trip)
|
||||||
|
private var contentHeightObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
// Events
|
// Events
|
||||||
private let onAttemptDismiss = EventDispatcher()
|
private let onAttemptDismiss = EventDispatcher()
|
||||||
private let onSnapPointChange = EventDispatcher()
|
private let onSnapPointChange = EventDispatcher()
|
||||||
@@ -23,6 +26,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// React view props
|
// React view props
|
||||||
|
var fullHeight = false
|
||||||
var preventDismiss = false
|
var preventDismiss = false
|
||||||
var preventExpansion = false
|
var preventExpansion = false
|
||||||
var cornerRadius: CGFloat?
|
var cornerRadius: CGFloat?
|
||||||
@@ -68,7 +72,6 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private var prevLayoutDetentIdentifier: UISheetPresentationController.Detent.Identifier?
|
|
||||||
|
|
||||||
// MARK: - Lifecycle
|
// MARK: - Lifecycle
|
||||||
|
|
||||||
@@ -106,6 +109,8 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func destroy() {
|
private func destroy() {
|
||||||
|
self.contentHeightObservation?.invalidate()
|
||||||
|
self.contentHeightObservation = nil
|
||||||
self.isClosing = false
|
self.isClosing = false
|
||||||
self.isOpen = false
|
self.isOpen = false
|
||||||
self.sheetVc = nil
|
self.sheetVc = nil
|
||||||
@@ -128,7 +133,7 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let sheetVc = SheetViewController()
|
let sheetVc = SheetViewController()
|
||||||
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion)
|
sheetVc.setDetents(contentHeight: self.clampHeight(contentHeight), preventExpansion: self.preventExpansion, fullHeight: self.fullHeight)
|
||||||
if let sheet = sheetVc.sheetPresentationController {
|
if let sheet = sheetVc.sheetPresentationController {
|
||||||
sheet.delegate = self
|
sheet.delegate = self
|
||||||
sheet.preferredCornerRadius = self.cornerRadius
|
sheet.preferredCornerRadius = self.cornerRadius
|
||||||
@@ -147,6 +152,9 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
|
|
||||||
self.sheetVc = sheetVc
|
self.sheetVc = sheetVc
|
||||||
self.isOpening = true
|
self.isOpening = true
|
||||||
|
if !self.fullHeight {
|
||||||
|
self.startObservingContentHeight()
|
||||||
|
}
|
||||||
|
|
||||||
rvc.present(sheetVc, animated: true) { [weak self] in
|
rvc.present(sheetVc, animated: true) { [weak self] in
|
||||||
self?.isOpening = false
|
self?.isOpening = false
|
||||||
@@ -154,15 +162,30 @@ class SheetView: ExpoView, UISheetPresentationControllerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateLayout() {
|
// Observe the content view's bounds via KVO so that height changes are detected
|
||||||
// Allow updates either when identifiers match OR when prevLayoutDetentIdentifier is nil (first real content update)
|
// purely on the native side, without a JS bridge round-trip through onLayout.
|
||||||
if self.prevLayoutDetentIdentifier == self.selectedDetentIdentifier || self.prevLayoutDetentIdentifier == nil,
|
// Calls updateDetents directly with the observed height rather than going through
|
||||||
let contentHeight = self.innerView?.subviews.first?.frame.size.height {
|
// updateLayout(), which has a prevLayoutDetentIdentifier guard that can block
|
||||||
self.sheetVc?.updateDetents(contentHeight: self.clampHeight(contentHeight),
|
// legitimate content-driven updates when detent identifiers drift during animations.
|
||||||
preventExpansion: self.preventExpansion)
|
private func startObservingContentHeight() {
|
||||||
|
self.contentHeightObservation?.invalidate()
|
||||||
|
|
||||||
|
guard let contentView = self.innerView?.subviews.first else { return }
|
||||||
|
|
||||||
|
self.contentHeightObservation = contentView.observe(
|
||||||
|
\.bounds,
|
||||||
|
options: [.old, .new]
|
||||||
|
) { [weak self] _, change in
|
||||||
|
guard let self = self,
|
||||||
|
(self.isOpen || self.isOpening) && !self.isClosing,
|
||||||
|
let oldBounds = change.oldValue,
|
||||||
|
let newBounds = change.newValue,
|
||||||
|
oldBounds.height != newBounds.height,
|
||||||
|
newBounds.height > 0 else { return }
|
||||||
|
let clampedHeight = self.clampHeight(newBounds.height)
|
||||||
|
self.sheetVc?.updateDetents(contentHeight: clampedHeight, preventExpansion: self.preventExpansion)
|
||||||
self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
|
self.selectedDetentIdentifier = self.sheetVc?.getCurrentDetentIdentifier()
|
||||||
}
|
}
|
||||||
self.prevLayoutDetentIdentifier = self.selectedDetentIdentifier
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func dismiss() {
|
func dismiss() {
|
||||||
|
|||||||
@@ -20,13 +20,19 @@ class SheetViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setDetents(contentHeight: CGFloat, preventExpansion: Bool) {
|
func setDetents(contentHeight: CGFloat, preventExpansion: Bool, fullHeight: Bool = false) {
|
||||||
guard let sheet = self.sheetPresentationController,
|
guard let sheet = self.sheetPresentationController,
|
||||||
let screenHeight = Util.getScreenHeight()
|
let screenHeight = Util.getScreenHeight()
|
||||||
else {
|
else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fullHeight {
|
||||||
|
sheet.detents = [.large()]
|
||||||
|
sheet.selectedDetentIdentifier = .large
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// On iOS 26, the floaty sheet presentation adds the device bottom safe area
|
// On iOS 26, the floaty sheet presentation adds the device bottom safe area
|
||||||
// on top of the custom detent value, creating visible padding inside the pill.
|
// on top of the custom detent value, creating visible padding inside the pill.
|
||||||
// Subtract it so the pill height matches our actual content.
|
// Subtract it so the pill height matches our actual content.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface BottomSheetViewProps {
|
|||||||
disableDrag?: boolean
|
disableDrag?: boolean
|
||||||
sourceViewTag?: number
|
sourceViewTag?: number
|
||||||
|
|
||||||
|
fullHeight?: boolean
|
||||||
minHeight?: number
|
minHeight?: number
|
||||||
maxHeight?: number
|
maxHeight?: number
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
import {requireNativeModule, requireNativeViewManager} from 'expo-modules-core'
|
||||||
|
|
||||||
import {IS_IOS} from '#/env'
|
|
||||||
import {
|
import {
|
||||||
type BottomSheetState,
|
type BottomSheetState,
|
||||||
type BottomSheetViewProps,
|
type BottomSheetViewProps,
|
||||||
@@ -35,6 +34,10 @@ const IS_IOS15 =
|
|||||||
Platform.OS === 'ios' &&
|
Platform.OS === 'ios' &&
|
||||||
// semvar - can be 3 segments, so can't use Number(Platform.Version)
|
// semvar - can be 3 segments, so can't use Number(Platform.Version)
|
||||||
Number(Platform.Version.split('.').at(0)) < 16
|
Number(Platform.Version.split('.').at(0)) < 16
|
||||||
|
// older android versions (15 and below) aren't naturally edge-to-edge
|
||||||
|
// and behave a little differently
|
||||||
|
const IS_NON_E2E_ANDROID =
|
||||||
|
Platform.OS === 'android' && Number(Platform.Version) < 35
|
||||||
|
|
||||||
export class BottomSheetNativeComponent extends React.Component<
|
export class BottomSheetNativeComponent extends React.Component<
|
||||||
BottomSheetViewProps,
|
BottomSheetViewProps,
|
||||||
@@ -71,10 +74,6 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
this.props.onStateChange?.(event)
|
this.props.onStateChange?.(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateLayout = () => {
|
|
||||||
this.ref.current?.updateLayout()
|
|
||||||
}
|
|
||||||
|
|
||||||
static dismissAll = async () => {
|
static dismissAll = async () => {
|
||||||
await NativeModule.dismissAll()
|
await NativeModule.dismissAll()
|
||||||
}
|
}
|
||||||
@@ -113,23 +112,14 @@ export class BottomSheetNativeComponent extends React.Component<
|
|||||||
nativeViewRef={this.ref}
|
nativeViewRef={this.ref}
|
||||||
onStateChange={this.onStateChange}
|
onStateChange={this.onStateChange}
|
||||||
extraStyles={extraStyles}
|
extraStyles={extraStyles}
|
||||||
onLayout={e => {
|
onLayout={
|
||||||
if (IS_IOS15) {
|
IS_IOS15
|
||||||
|
? e => {
|
||||||
const {height} = e.nativeEvent.layout
|
const {height} = e.nativeEvent.layout
|
||||||
this.setState({viewHeight: height})
|
this.setState({viewHeight: height})
|
||||||
}
|
}
|
||||||
if (Platform.OS === 'android') {
|
: undefined
|
||||||
// TEMP HACKFIX: I had to timebox this, but this is Bad.
|
|
||||||
// On Android, if you run updateLayout() immediately,
|
|
||||||
// it will take ages to actually run on the native side.
|
|
||||||
// However, adding literally any delay will fix this, including
|
|
||||||
// a console.log() - just sending the log to the CLI is enough.
|
|
||||||
// TODO: Get to the bottom of this and fix it properly! -sfn
|
|
||||||
setTimeout(() => this.updateLayout())
|
|
||||||
} else {
|
|
||||||
this.updateLayout()
|
|
||||||
}
|
}
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
@@ -150,13 +140,18 @@ function BottomSheetNativeComponentInner({
|
|||||||
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
event: NativeSyntheticEvent<{state: BottomSheetState}>,
|
||||||
) => void
|
) => void
|
||||||
nativeViewRef: React.RefObject<View>
|
nativeViewRef: React.RefObject<View>
|
||||||
onLayout: (event: LayoutChangeEvent) => void
|
onLayout?: (event: LayoutChangeEvent) => void
|
||||||
}) {
|
}) {
|
||||||
const insets = useSafeAreaInsets()
|
const insets = useSafeAreaInsets()
|
||||||
const cornerRadius = rest.cornerRadius ?? 0
|
const cornerRadius = rest.cornerRadius ?? 0
|
||||||
const {height: screenHeight} = useWindowDimensions()
|
const {height: screenHeight} = useWindowDimensions()
|
||||||
|
|
||||||
const sheetHeight = IS_IOS ? screenHeight - insets.top : screenHeight
|
// sigh... on older Android versions, screenHeight does not include safe area insets
|
||||||
|
// on newer Androids + iOS, it does. we need to find the inner bit + the bottom inset
|
||||||
|
// for the sheet content
|
||||||
|
const sheetHeight = IS_NON_E2E_ANDROID
|
||||||
|
? screenHeight + insets.bottom
|
||||||
|
: screenHeight - insets.top
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NativeView
|
<NativeView
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
import React, {useImperativeHandle} from 'react'
|
|
||||||
import {
|
import {
|
||||||
|
forwardRef,
|
||||||
|
useCallback,
|
||||||
|
useImperativeHandle,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import {
|
||||||
|
Keyboard,
|
||||||
|
type KeyboardEventListener,
|
||||||
type LayoutChangeEvent,
|
type LayoutChangeEvent,
|
||||||
type NativeScrollEvent,
|
type NativeScrollEvent,
|
||||||
type NativeSyntheticEvent,
|
type NativeSyntheticEvent,
|
||||||
@@ -34,6 +43,7 @@ import {
|
|||||||
type DialogOuterProps,
|
type DialogOuterProps,
|
||||||
} from '#/components/Dialog/types'
|
} from '#/components/Dialog/types'
|
||||||
import {createInput} from '#/components/forms/TextField'
|
import {createInput} from '#/components/forms/TextField'
|
||||||
|
import {useOnKeyboard} from '#/components/hooks/useOnKeyboard'
|
||||||
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env'
|
import {IS_ANDROID, IS_IOS, IS_LIQUID_GLASS} from '#/env'
|
||||||
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
|
import {BottomSheet, BottomSheetSnapPoint} from '../../../modules/bottom-sheet'
|
||||||
import {
|
import {
|
||||||
@@ -58,21 +68,21 @@ export function Outer({
|
|||||||
}: React.PropsWithChildren<DialogOuterProps>) {
|
}: React.PropsWithChildren<DialogOuterProps>) {
|
||||||
const themeName = useThemeName()
|
const themeName = useThemeName()
|
||||||
const t = useTheme(themeName)
|
const t = useTheme(themeName)
|
||||||
const ref = React.useRef<BottomSheetNativeComponent>(null)
|
const ref = useRef<BottomSheetNativeComponent>(null)
|
||||||
const closeCallbacks = React.useRef<(() => void)[]>([])
|
const closeCallbacks = useRef<(() => void)[]>([])
|
||||||
const {setDialogIsOpen, setFullyExpandedCount} =
|
const {setDialogIsOpen, setFullyExpandedCount} =
|
||||||
useDialogStateControlContext()
|
useDialogStateControlContext()
|
||||||
|
|
||||||
const prevSnapPoint = React.useRef<BottomSheetSnapPoint>(
|
const prevSnapPoint = useRef<BottomSheetSnapPoint>(
|
||||||
BottomSheetSnapPoint.Hidden,
|
BottomSheetSnapPoint.Hidden,
|
||||||
)
|
)
|
||||||
|
|
||||||
const [disableDrag, setDisableDrag] = React.useState(false)
|
const [disableDrag, setDisableDrag] = useState(false)
|
||||||
const [snapPoint, setSnapPoint] = React.useState<BottomSheetSnapPoint>(
|
const [snapPoint, setSnapPoint] = useState<BottomSheetSnapPoint>(
|
||||||
BottomSheetSnapPoint.Partial,
|
BottomSheetSnapPoint.Partial,
|
||||||
)
|
)
|
||||||
|
|
||||||
const callQueuedCallbacks = React.useCallback(() => {
|
const callQueuedCallbacks = useCallback(() => {
|
||||||
for (const cb of closeCallbacks.current) {
|
for (const cb of closeCallbacks.current) {
|
||||||
try {
|
try {
|
||||||
cb()
|
cb()
|
||||||
@@ -84,7 +94,7 @@ export function Outer({
|
|||||||
closeCallbacks.current = []
|
closeCallbacks.current = []
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const open = React.useCallback<DialogControlProps['open']>(() => {
|
const open = useCallback<DialogControlProps['open']>(() => {
|
||||||
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
// Run any leftover callbacks that might have been queued up before calling `.open()`
|
||||||
callQueuedCallbacks()
|
callQueuedCallbacks()
|
||||||
setDialogIsOpen(control.id, true)
|
setDialogIsOpen(control.id, true)
|
||||||
@@ -92,7 +102,7 @@ export function Outer({
|
|||||||
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
|
}, [setDialogIsOpen, control.id, callQueuedCallbacks])
|
||||||
|
|
||||||
// This is the function that we call when we want to dismiss the dialog.
|
// This is the function that we call when we want to dismiss the dialog.
|
||||||
const close = React.useCallback<DialogControlProps['close']>(cb => {
|
const close = useCallback<DialogControlProps['close']>(cb => {
|
||||||
if (typeof cb === 'function') {
|
if (typeof cb === 'function') {
|
||||||
closeCallbacks.current.push(cb)
|
closeCallbacks.current.push(cb)
|
||||||
}
|
}
|
||||||
@@ -101,7 +111,7 @@ export function Outer({
|
|||||||
|
|
||||||
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
// This is the actual thing we are doing once we "confirm" the dialog. We want the dialog's close animation to
|
||||||
// happen before we run this. It is passed to the `BottomSheet` component.
|
// happen before we run this. It is passed to the `BottomSheet` component.
|
||||||
const onCloseAnimationComplete = React.useCallback(() => {
|
const onCloseAnimationComplete = useCallback(() => {
|
||||||
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
// This removes the dialog from our list of stored dialogs. Not super necessary on iOS, but on Android this
|
||||||
// tells us that we need to toggle the accessibility overlay setting
|
// tells us that we need to toggle the accessibility overlay setting
|
||||||
setDialogIsOpen(control.id, false)
|
setDialogIsOpen(control.id, false)
|
||||||
@@ -147,7 +157,7 @@ export function Outer({
|
|||||||
[open, close],
|
[open, close],
|
||||||
)
|
)
|
||||||
|
|
||||||
const context = React.useMemo(
|
const context = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
close,
|
close,
|
||||||
isNativeDialog: true,
|
isNativeDialog: true,
|
||||||
@@ -201,25 +211,23 @@ export function Inner({children, style, header}: DialogInnerProps) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
export const ScrollableInner = forwardRef<ScrollView, DialogInnerProps>(
|
||||||
function ScrollableInner(
|
function ScrollableInner(
|
||||||
{children, contentContainerStyle, header, ...props},
|
{children, contentContainerStyle, header, ...props},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
const isAtMaxSnapPoint = nativeSnapPoint === BottomSheetSnapPoint.Full
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [keyboardHeight, setKeyboardHeight] = useState(() =>
|
||||||
|
IS_ANDROID ? (Keyboard.metrics()?.height ?? 0) : 0,
|
||||||
|
)
|
||||||
|
|
||||||
let paddingBottom = 0
|
const keyboardEventHandler = useCallback<KeyboardEventListener>(e => {
|
||||||
if (IS_IOS) {
|
setKeyboardHeight(e.endCoordinates.height)
|
||||||
paddingBottom = tokens.space._2xl
|
}, [])
|
||||||
} else {
|
useOnKeyboard('keyboardDidShow', keyboardEventHandler)
|
||||||
paddingBottom =
|
useOnKeyboard('keyboardDidHide', keyboardEventHandler)
|
||||||
Math.max(insets.bottom, tokens.space._5xl) + tokens.space._2xl
|
|
||||||
if (isAtMaxSnapPoint) {
|
|
||||||
paddingBottom += insets.top
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
const onScroll = (e: NativeSyntheticEvent<NativeScrollEvent>) => {
|
||||||
if (!IS_ANDROID) {
|
if (!IS_ANDROID) {
|
||||||
@@ -238,7 +246,12 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
a.pt_2xl,
|
a.pt_2xl,
|
||||||
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
IS_LIQUID_GLASS ? a.px_2xl : a.px_xl,
|
||||||
{paddingBottom},
|
platform({
|
||||||
|
ios: a.pb_2xl,
|
||||||
|
android: {
|
||||||
|
paddingBottom: keyboardHeight + insets.bottom + tokens.space.xl,
|
||||||
|
},
|
||||||
|
}),
|
||||||
contentContainerStyle,
|
contentContainerStyle,
|
||||||
]}
|
]}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -250,7 +263,12 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
{...props}
|
{...props}
|
||||||
bounces={isAtMaxSnapPoint}
|
bounces={isAtMaxSnapPoint}
|
||||||
scrollEventThrottle={50}
|
scrollEventThrottle={50}
|
||||||
onScroll={IS_ANDROID ? onScroll : undefined}
|
// set drag state based on scroll on android.
|
||||||
|
// we want to detect if it's at the top or not, so watch
|
||||||
|
// scrollEndDrag and momentumScrollEnd as well
|
||||||
|
onScroll={android(onScroll)}
|
||||||
|
onScrollEndDrag={android(onScroll)}
|
||||||
|
onMomentumScrollEnd={android(onScroll)}
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
// TODO: figure out why this positions the header absolutely (rather than stickily)
|
||||||
// on Android. fine to disable for now, because we don't have any
|
// on Android. fine to disable for now, because we don't have any
|
||||||
@@ -263,7 +281,7 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
export const InnerFlatList = React.forwardRef<
|
export const InnerFlatList = forwardRef<
|
||||||
ListMethods,
|
ListMethods,
|
||||||
ListProps<any> & {
|
ListProps<any> & {
|
||||||
webInnerStyle?: StyleProp<ViewStyle>
|
webInnerStyle?: StyleProp<ViewStyle>
|
||||||
@@ -293,7 +311,10 @@ export const InnerFlatList = React.forwardRef<
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollProvider onScroll={onScroll}>
|
<ScrollProvider
|
||||||
|
onScroll={onScroll}
|
||||||
|
onEndDrag={onScroll}
|
||||||
|
onMomentumEnd={onScroll}>
|
||||||
<List
|
<List
|
||||||
keyboardShouldPersistTaps="handled"
|
keyboardShouldPersistTaps="handled"
|
||||||
contentInsetAdjustmentBehavior={
|
contentInsetAdjustmentBehavior={
|
||||||
@@ -327,7 +348,7 @@ export function FlatListFooter({
|
|||||||
onLayout?: (event: LayoutChangeEvent) => void
|
onLayout?: (event: LayoutChangeEvent) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {top, bottom} = useSafeAreaInsets()
|
const {bottom} = useSafeAreaInsets()
|
||||||
const {height} = useReanimatedKeyboardAnimation()
|
const {height} = useReanimatedKeyboardAnimation()
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => {
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
@@ -350,12 +371,7 @@ export function FlatListFooter({
|
|||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.pt_md,
|
a.pt_md,
|
||||||
{
|
{paddingBottom: bottom + tokens.space.md},
|
||||||
paddingBottom: platform({
|
|
||||||
ios: tokens.space.md + bottom + (IS_LIQUID_GLASS ? top : 0),
|
|
||||||
android: tokens.space.md + bottom + top,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
// TODO: had to admit defeat here, but we should
|
// TODO: had to admit defeat here, but we should
|
||||||
// try and get this to work for Android as well -sfn
|
// try and get this to work for Android as well -sfn
|
||||||
ios(animatedStyle),
|
ios(animatedStyle),
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
|
||||||
import {
|
import {TextInput, View, type ViewToken} from 'react-native'
|
||||||
TextInput,
|
|
||||||
useWindowDimensions,
|
|
||||||
View,
|
|
||||||
type ViewToken,
|
|
||||||
} from 'react-native'
|
|
||||||
import {type ModerationOpts} from '@atproto/api'
|
import {type ModerationOpts} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -72,7 +67,6 @@ export function FollowDialog({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const control = Dialog.useDialogControl()
|
const control = Dialog.useDialogControl()
|
||||||
const {gtPhone} = useBreakpoints()
|
const {gtPhone} = useBreakpoints()
|
||||||
const {height: minHeight} = useWindowDimensions()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -89,7 +83,7 @@ export function FollowDialog({
|
|||||||
</ButtonText>
|
</ButtonText>
|
||||||
{showArrow && <ButtonIcon icon={ArrowRightIcon} />}
|
{showArrow && <ButtonIcon icon={ArrowRightIcon} />}
|
||||||
</Button>
|
</Button>
|
||||||
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<DialogInner guide={guide} />
|
<DialogInner guide={guide} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
@@ -105,9 +99,8 @@ export function FollowDialogWithoutGuide({
|
|||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
}) {
|
}) {
|
||||||
const {height: minHeight} = useWindowDimensions()
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{minHeight}}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<DialogInner />
|
<DialogInner />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ export function Content<T>({
|
|||||||
}, [items, context.value, valueExtractor, setValue])
|
}, [items, context.value, valueExtractor, setValue])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<ContentInner
|
<ContentInner
|
||||||
control={control}
|
control={control}
|
||||||
items={items}
|
items={items}
|
||||||
|
|||||||
@@ -78,7 +78,10 @@ export function WizardEditListDialog({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} testID="newChatDialog">
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
testID="newChatDialog"
|
||||||
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.InnerFlatList
|
<Dialog.InnerFlatList
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export function GifSelectDialog({
|
|||||||
bottomInset: 0,
|
bottomInset: 0,
|
||||||
// use system corner radius on iOS
|
// use system corner radius on iOS
|
||||||
...ios({cornerRadius: undefined}),
|
...ios({cornerRadius: undefined}),
|
||||||
|
fullHeight: true,
|
||||||
}}>
|
}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useCallback, useMemo, useState} from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
@@ -17,7 +16,7 @@ import {SearchInput} from '#/components/forms/SearchInput'
|
|||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
type FlatListItem =
|
type FlatListItem =
|
||||||
| {
|
| {
|
||||||
@@ -51,20 +50,13 @@ export function LanguageSelectDialog({
|
|||||||
onSelectLanguages: (languages: string[]) => void
|
onSelectLanguages: (languages: string[]) => void
|
||||||
maxLanguages?: number
|
maxLanguages?: number
|
||||||
}) {
|
}) {
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
const insets = useSafeAreaInsets()
|
|
||||||
|
|
||||||
const renderErrorBoundary = useCallback(
|
const renderErrorBoundary = useCallback(
|
||||||
(error: any) => <DialogError details={String(error)} />,
|
(error: any) => <DialogError details={String(error)} />,
|
||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
control={control}
|
|
||||||
nativeOptions={{
|
|
||||||
minHeight: IS_LIQUID_GLASS ? height : height - insets.top,
|
|
||||||
}}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<ErrorBoundary renderError={renderErrorBoundary}>
|
<ErrorBoundary renderError={renderErrorBoundary}>
|
||||||
<DialogInner
|
<DialogInner
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
|
import {useCallback, useImperativeHandle, useRef, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Trans} from '@lingui/react/macro'
|
import {Trans} from '@lingui/react/macro'
|
||||||
@@ -28,7 +28,6 @@ export function ServerInputDialog({
|
|||||||
onSelect: (url: string) => void
|
onSelect: (url: string) => void
|
||||||
}) {
|
}) {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
const formRef = useRef<DialogInnerRef>(null)
|
const formRef = useRef<DialogInnerRef>(null)
|
||||||
|
|
||||||
// persist these options between dialog open/close
|
// persist these options between dialog open/close
|
||||||
@@ -53,10 +52,7 @@ export function ServerInputDialog({
|
|||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
nativeOptions={platform({
|
nativeOptions={{preventExpansion: true}}>
|
||||||
android: {minHeight: height / 2},
|
|
||||||
ios: {preventExpansion: true},
|
|
||||||
})}>
|
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<DialogInner
|
<DialogInner
|
||||||
formRef={formRef}
|
formRef={formRef}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export function StarterPackDialog({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<StarterPackList
|
<StarterPackList
|
||||||
onStartWizard={wrappedNavToWizard}
|
onStartWizard={wrappedNavToWizard}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback, useEffect, useMemo, useState} from 'react'
|
import {useCallback, useEffect, useMemo, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
|
import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -53,7 +53,6 @@ export function CreateOrEditListDialog({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const cancelControl = Dialog.useDialogControl()
|
const cancelControl = Dialog.useDialogControl()
|
||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
|
|
||||||
// 'You might lose unsaved changes' warning
|
// 'You might lose unsaved changes' warning
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -82,7 +81,7 @@ export function CreateOrEditListDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
nativeOptions={{
|
nativeOptions={{
|
||||||
preventDismiss: dirty,
|
preventDismiss: dirty,
|
||||||
minHeight: height,
|
fullHeight: true,
|
||||||
}}
|
}}
|
||||||
testID="createOrEditListDialog">
|
testID="createOrEditListDialog">
|
||||||
<DialogInner
|
<DialogInner
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ export function ListAddRemoveUsersDialog({
|
|||||||
) => void | undefined
|
) => void | undefined
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} testID="listAddRemoveUsersDialog">
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
testID="listAddRemoveUsersDialog"
|
||||||
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<DialogInner list={list} onChange={onChange} />
|
<DialogInner list={list} onChange={onChange} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -70,7 +70,10 @@ export function NewChat({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Dialog.Outer control={control} testID="newChatDialog">
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
testID="newChatDialog"
|
||||||
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<SearchablePeopleList
|
<SearchablePeopleList
|
||||||
title={_(msg`Start a new chat`)}
|
title={_(msg`Start a new chat`)}
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ export function SendViaChatDialog({
|
|||||||
onSelectChat: (chatId: string) => void
|
onSelectChat: (chatId: string) => void
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} testID="sendViaChatChatDialog">
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
testID="sendViaChatChatDialog"
|
||||||
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
<SendViaChatDialogInner control={control} onSelectChat={onSelectChat} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import React from 'react'
|
import {useEffect} from 'react'
|
||||||
import {Keyboard} from 'react-native'
|
import {
|
||||||
|
Keyboard,
|
||||||
|
type KeyboardEventListener,
|
||||||
|
type KeyboardEventName,
|
||||||
|
} from 'react-native'
|
||||||
|
|
||||||
export function useOnKeyboardDidShow(cb: () => unknown) {
|
export function useOnKeyboard(
|
||||||
React.useEffect(() => {
|
eventName: KeyboardEventName,
|
||||||
const subscription = Keyboard.addListener('keyboardDidShow', cb)
|
cb: KeyboardEventListener,
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = Keyboard.addListener(eventName, cb)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
subscription.remove()
|
subscription.remove()
|
||||||
}
|
}
|
||||||
}, [cb])
|
}, [eventName, cb])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback, useEffect, useState} from 'react'
|
import {useCallback, useEffect, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {type AppBskyActorDefs} from '@atproto/api'
|
import {type AppBskyActorDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -41,7 +41,6 @@ export function EditProfileDialog({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const cancelControl = Dialog.useDialogControl()
|
const cancelControl = Dialog.useDialogControl()
|
||||||
const [dirty, setDirty] = useState(false)
|
const [dirty, setDirty] = useState(false)
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
|
|
||||||
const onPressCancel = useCallback(() => {
|
const onPressCancel = useCallback(() => {
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
@@ -56,7 +55,7 @@ export function EditProfileDialog({
|
|||||||
control={control}
|
control={control}
|
||||||
nativeOptions={{
|
nativeOptions={{
|
||||||
preventDismiss: dirty,
|
preventDismiss: dirty,
|
||||||
minHeight: height,
|
fullHeight: true,
|
||||||
}}
|
}}
|
||||||
webOptions={{
|
webOptions={{
|
||||||
onBackgroundPress: () => {
|
onBackgroundPress: () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useEffect, useMemo, useState} from 'react'
|
import {useEffect, useMemo, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
@@ -34,9 +34,8 @@ export function AddAppPasswordDialog({
|
|||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
passwords: string[]
|
passwords: string[]
|
||||||
}) {
|
}) {
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<CreateDialogInner passwords={passwords} />
|
<CreateDialogInner passwords={passwords} />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useCallback, useMemo, useState} from 'react'
|
import {useCallback, useMemo, useState} from 'react'
|
||||||
import {useWindowDimensions, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
FadeIn,
|
FadeIn,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
@@ -53,10 +53,8 @@ export function ChangeHandleDialog({
|
|||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
}) {
|
}) {
|
||||||
const {height} = useWindowDimensions()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{minHeight: height}}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
<ChangeHandleDialogInner />
|
<ChangeHandleDialogInner />
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
import {TouchableOpacity, useWindowDimensions, View} from 'react-native'
|
import {TouchableOpacity, View} from 'react-native'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Plural, Trans} from '@lingui/react/macro'
|
import {Plural, Trans} from '@lingui/react/macro'
|
||||||
@@ -23,7 +23,6 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico
|
|||||||
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
|
||||||
import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
|
import {GifEmbed} from '#/components/Post/Embed/ExternalEmbed/Gif'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_ANDROID} from '#/env'
|
|
||||||
import {AltTextReminder} from './photos/Gallery'
|
import {AltTextReminder} from './photos/Gallery'
|
||||||
|
|
||||||
export function GifAltTextDialog({
|
export function GifAltTextDialog({
|
||||||
@@ -69,7 +68,6 @@ export function GifAltTextDialogLoaded({
|
|||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
|
const [altTextDraft, setAltTextDraft] = useState(altText || vendorAltText)
|
||||||
const {height: minHeight} = useWindowDimensions()
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
@@ -110,7 +108,7 @@ export function GifAltTextDialogLoaded({
|
|||||||
onClose={() => {
|
onClose={() => {
|
||||||
onSubmit(altTextDraft)
|
onSubmit(altTextDraft)
|
||||||
}}
|
}}
|
||||||
nativeOptions={{minHeight}}>
|
nativeOptions={{fullHeight: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<AltTextInner
|
<AltTextInner
|
||||||
vendorAltText={vendorAltText}
|
vendorAltText={vendorAltText}
|
||||||
@@ -226,8 +224,6 @@ function AltTextInner({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
<Dialog.Close />
|
<Dialog.Close />
|
||||||
{/* Maybe fix this later -h */}
|
|
||||||
{IS_ANDROID ? <View style={{height: 300}} /> : null}
|
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export function DraftsListDialog({
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control} nativeOptions={{fullHeight: true}}>
|
||||||
{/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */}
|
{/* We really really need to figure out a nice, consistent API for doing a header cross-platform -sfn */}
|
||||||
{IS_NATIVE && header}
|
{IS_NATIVE && header}
|
||||||
<Dialog.InnerFlatList
|
<Dialog.InnerFlatList
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {Plural, Trans} from '@lingui/react/macro'
|
import {Plural, Trans} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {MAX_ALT_TEXT} from '#/lib/constants'
|
import {MAX_ALT_TEXT} from '#/lib/constants'
|
||||||
import {useIsKeyboardVisible} from '#/lib/hooks/useIsKeyboardVisible'
|
|
||||||
import {enforceLen} from '#/lib/strings/helpers'
|
import {enforceLen} from '#/lib/strings/helpers'
|
||||||
import {type ComposerImage} from '#/state/gallery'
|
import {type ComposerImage} from '#/state/gallery'
|
||||||
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
import {AltTextCounterWrapper} from '#/view/com/composer/AltTextCounterWrapper'
|
||||||
@@ -17,7 +16,7 @@ import {type DialogControlProps} from '#/components/Dialog'
|
|||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {IS_ANDROID, IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
import {IS_LIQUID_GLASS, IS_WEB} from '#/env'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
@@ -32,7 +31,6 @@ export const ImageAltTextDialog = ({
|
|||||||
onChange,
|
onChange,
|
||||||
sourceViewTag,
|
sourceViewTag,
|
||||||
}: Props): React.ReactNode => {
|
}: Props): React.ReactNode => {
|
||||||
const {height: minHeight} = useWindowDimensions()
|
|
||||||
const [altText, setAltText] = useState(image.alt)
|
const [altText, setAltText] = useState(image.alt)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,7 +42,7 @@ export const ImageAltTextDialog = ({
|
|||||||
alt: enforceLen(altText, MAX_ALT_TEXT, true),
|
alt: enforceLen(altText, MAX_ALT_TEXT, true),
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
nativeOptions={{minHeight, sourceViewTag}}>
|
nativeOptions={{fullHeight: true, sourceViewTag}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<ImageAltTextInner
|
<ImageAltTextInner
|
||||||
control={control}
|
control={control}
|
||||||
@@ -71,8 +69,6 @@ const ImageAltTextInner = ({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {width: screenWidth} = useWindowDimensions()
|
const {width: screenWidth} = useWindowDimensions()
|
||||||
|
|
||||||
const [isKeyboardVisible] = useIsKeyboardVisible()
|
|
||||||
|
|
||||||
const imageStyle = useMemo<ImageStyle>(() => {
|
const imageStyle = useMemo<ImageStyle>(() => {
|
||||||
const maxWidth = IS_WEB
|
const maxWidth = IS_WEB
|
||||||
? 450
|
? 450
|
||||||
@@ -179,8 +175,6 @@ const ImageAltTextInner = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</AltTextCounterWrapper>
|
</AltTextCounterWrapper>
|
||||||
</View>
|
</View>
|
||||||
{/* Maybe fix this later -h */}
|
|
||||||
{IS_ANDROID && isKeyboardVisible ? <View style={{height: 300}} /> : null}
|
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user