The Great Unjanking of the Sheets (#9973)

This commit is contained in:
Samuel Newman
2026-03-09 22:53:32 +02:00
committed by GitHub
parent 18d7e775f6
commit aa897f55a0
27 changed files with 318 additions and 196 deletions
@@ -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 ->
@@ -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,8 +70,15 @@ 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
var minHeight = 0f var minHeight = 0f
@@ -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,31 +207,40 @@ 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.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt() behavior.expandedOffset = getStatusBarHeight()
} else {
behavior.maxHeight = (screenHeight - getStatusBarHeight()).toInt()
}
val targetHeight = this.getTargetHeight()
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
val shouldBeExpanded = targetHeight >= availableHeight
if (shouldBeExpanded) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2 this.selectedSnapPoint = 2
} else { } else if (preventExpansion) {
behavior.isFitToContents = true
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
this.selectedSnapPoint = 1 this.selectedSnapPoint = 1
} else {
behavior.isFitToContents = false
behavior.halfExpandedRatio = getHalfExpandedRatio(contentHeight)
behavior.expandedOffset = getStatusBarHeight()
val targetHeight = this.getTargetHeight()
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
val shouldBeExpanded = targetHeight >= availableHeight
if (shouldBeExpanded) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
this.selectedSnapPoint = 2
} else {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
this.selectedSnapPoint = 1
}
} }
behavior.addBottomSheetCallback( behavior.addBottomSheetCallback(
@@ -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
}
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
} }
} else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) { 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()
index++ if (h > maxChildHeight) maxChildHeight = h
} }
return 0f return maxChildHeight
} }
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
+32 -9
View File
@@ -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
const {height} = e.nativeEvent.layout ? e => {
this.setState({viewHeight: height}) const {height} = e.nativeEvent.layout
} this.setState({viewHeight: height})
if (Platform.OS === 'android') { }
// TEMP HACKFIX: I had to timebox this, but this is Bad. : undefined
// 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
+50 -34
View File
@@ -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),
+3 -10
View File
@@ -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>
+1 -1
View File
@@ -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}
+1
View File
@@ -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
+2 -6
View File
@@ -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}
+1 -1
View File
@@ -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>
+4 -1
View File
@@ -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>
+13 -6
View File
@@ -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>
) )
+2 -6
View File
@@ -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>
) )
} }