Android sheets edge to edge (#8342)

This commit is contained in:
Samuel Newman
2026-02-10 21:01:58 +00:00
committed by GitHub
parent e2058c8ecc
commit 338016ed5c
19 changed files with 161 additions and 228 deletions
+1 -1
View File
@@ -44,6 +44,6 @@ android {
dependencies { dependencies {
implementation project(':expo-modules-core') implementation project(':expo-modules-core')
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.13.0'
implementation "com.facebook.react:react-native:+" implementation "com.facebook.react:react-native:+"
} }
@@ -5,8 +5,12 @@ import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewStructure import android.view.ViewStructure
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.allViews 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
@@ -15,6 +19,7 @@ import com.facebook.react.uimanager.UIManagerHelper
import com.facebook.react.uimanager.events.EventDispatcher import com.facebook.react.uimanager.events.EventDispatcher
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.internal.EdgeToEdgeUtils
import expo.modules.kotlin.AppContext import expo.modules.kotlin.AppContext
import expo.modules.kotlin.viewevent.EventDispatcher import expo.modules.kotlin.viewevent.EventDispatcher
import expo.modules.kotlin.views.ExpoView import expo.modules.kotlin.views.ExpoView
@@ -29,22 +34,26 @@ 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 rawScreenHeight = private val screenHeight =
context.resources.displayMetrics.heightPixels context.resources.displayMetrics.heightPixels
.toFloat() .toFloat()
private val safeScreenHeight = (rawScreenHeight - getNavigationBarHeight()).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")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0 return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
} }
private fun getStatusBarHeight(): Int {
val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android")
return if (resourceId > 0) resources.getDimensionPixelSize(resourceId) else 0
}
private val onAttemptDismiss by EventDispatcher() private val onAttemptDismiss by EventDispatcher()
private val onSnapPointChange by EventDispatcher() private val onSnapPointChange by EventDispatcher()
private val onStateChange by EventDispatcher() private val onStateChange by EventDispatcher()
// Props
var disableDrag = false var disableDrag = false
set(value) { set(value) {
field = value field = value
@@ -56,48 +65,31 @@ class BottomSheetView(
field = value field = value
this.dialog?.setCancelable(!value) this.dialog?.setCancelable(!value)
} }
var preventExpansion = false var preventExpansion = false
var minHeight = 0f var minHeight = 0f
set(value) { set(value) {
field = field = if (value < 0) 0f else dpToPx(value)
if (value < 0) {
0f
} else {
dpToPx(value)
}
} }
var maxHeight = this.safeScreenHeight var maxHeight = this.screenHeight
set(value) { set(value) {
val px = dpToPx(value) val px = dpToPx(value)
field = field = if (px > this.screenHeight) this.screenHeight else px
if (px > this.safeScreenHeight) {
this.safeScreenHeight
} else {
px
}
} }
private var isOpen: Boolean = false private var isOpen: Boolean = false
set(value) { set(value) {
field = value field = value
onStateChange( onStateChange(mapOf("state" to if (value) "open" else "closed"))
mapOf(
"state" to if (value) "open" else "closed",
),
)
} }
private var isOpening: Boolean = false private var isOpening: Boolean = false
set(value) { set(value) {
field = value field = value
if (value) { if (value) {
onStateChange( onStateChange(mapOf("state" to "opening"))
mapOf(
"state" to "opening",
),
)
} }
} }
@@ -105,33 +97,21 @@ class BottomSheetView(
set(value) { set(value) {
field = value field = value
if (value) { if (value) {
onStateChange( onStateChange(mapOf("state" to "closing"))
mapOf(
"state" to "closing",
),
)
} }
} }
private var selectedSnapPoint = 0 private var selectedSnapPoint = 0
set(value) { set(value) {
if (field == value) return if (field == value) return
field = value field = value
onSnapPointChange( onSnapPointChange(mapOf("snapPoint" to value))
mapOf(
"snapPoint" to value,
),
)
} }
// Lifecycle
init { init {
(appContext.reactContext as? ReactContext)?.let { (appContext.reactContext as? ReactContext)?.let {
it.addLifecycleEventListener(this) it.addLifecycleEventListener(this)
this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id) this.eventDispatcher = UIManagerHelper.getEventDispatcherForReactTag(it, this.id)
this.dialogRootViewGroup = DialogRootViewGroup(context) this.dialogRootViewGroup = DialogRootViewGroup(context)
this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher this.dialogRootViewGroup.eventDispatcher = this.eventDispatcher
} }
@@ -161,27 +141,55 @@ class BottomSheetView(
private fun getHalfExpandedRatio(contentHeight: Float): Float = private fun getHalfExpandedRatio(contentHeight: Float): Float =
when { when {
// Full height sheets // Full height sheets
contentHeight >= safeScreenHeight -> 0.99f contentHeight >= screenHeight -> 0.99f
// Medium height sheets (>50% but <100%) else -> this.clampRatio(this.getTargetHeight() / screenHeight)
contentHeight >= safeScreenHeight / 2 ->
this.clampRatio(this.getTargetHeight() / safeScreenHeight)
// Small height sheets (<50%)
else ->
this.clampRatio(this.getTargetHeight() / rawScreenHeight)
} }
private fun present() { private fun present() {
if (this.isOpen || this.isOpening || this.isClosing) return if (this.isOpen || this.isOpening || this.isClosing) return
val contentHeight = this.getContentHeight() val contentHeight = this.getContentHeight()
val dialog = BottomSheetDialog(context)
var activityWindow: Window? = null
var currentContext = context
while (currentContext != null) {
if (currentContext is android.app.Activity) {
activityWindow = currentContext.window
break
}
currentContext = (currentContext as? android.content.ContextWrapper)?.baseContext
}
val originalStatusBarAppearance =
activityWindow?.let { window ->
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightStatusBars
}
val originalNavBarAppearance =
activityWindow?.let { window ->
WindowInsetsControllerCompat(window, window.decorView).isAppearanceLightNavigationBars
}
val dialog = BottomSheetDialog(context, R.style.EdgeToEdgeBottomSheetDialogTheme)
dialog.setContentView(dialogRootViewGroup) dialog.setContentView(dialogRootViewGroup)
dialog.setCancelable(!preventDismiss) dialog.setCancelable(!preventDismiss)
dialog.setDismissWithAnimation(true)
dialog.setOnDismissListener { dialog.setOnDismissListener {
this.isClosing = true this.isClosing = true
this.destroy() this.destroy()
} }
dialog.setOnShowListener {
dialog.window?.let { window ->
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
if (originalNavBarAppearance != null) {
insetsController.isAppearanceLightNavigationBars = originalNavBarAppearance
}
if (originalStatusBarAppearance != null) {
EdgeToEdgeUtils.setLightStatusBar(window, originalStatusBarAppearance)
}
}
}
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)
@@ -194,7 +202,17 @@ class BottomSheetView(
behavior.isDraggable = true behavior.isDraggable = true
behavior.isHideable = true behavior.isHideable = true
if (contentHeight >= this.safeScreenHeight || this.minHeight >= this.safeScreenHeight) { if (preventExpansion) {
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
} 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 {
@@ -209,18 +227,10 @@ class BottomSheetView(
newState: Int, newState: Int,
) { ) {
when (newState) { when (newState) {
BottomSheetBehavior.STATE_EXPANDED -> { BottomSheetBehavior.STATE_EXPANDED -> selectedSnapPoint = 2
selectedSnapPoint = 2 BottomSheetBehavior.STATE_COLLAPSED -> selectedSnapPoint = 1
} BottomSheetBehavior.STATE_HALF_EXPANDED -> selectedSnapPoint = 1
BottomSheetBehavior.STATE_COLLAPSED -> { BottomSheetBehavior.STATE_HIDDEN -> selectedSnapPoint = 0
selectedSnapPoint = 1
}
BottomSheetBehavior.STATE_HALF_EXPANDED -> {
selectedSnapPoint = 1
}
BottomSheetBehavior.STATE_HIDDEN -> {
selectedSnapPoint = 0
}
} }
} }
@@ -231,9 +241,26 @@ class BottomSheetView(
}, },
) )
} }
this.isOpening = true this.isOpening = true
dialog.show() dialog.show()
this.dialog = dialog this.dialog = dialog
ViewCompat.setOnApplyWindowInsetsListener(dialogRootViewGroup) { view, insets ->
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() {
@@ -246,12 +273,24 @@ class BottomSheetView(
val currentState = behavior.state val currentState = behavior.state
val oldRatio = behavior.halfExpandedRatio val oldRatio = behavior.halfExpandedRatio
var newRatio = getHalfExpandedRatio(contentHeight) val newRatio = getHalfExpandedRatio(contentHeight)
behavior.halfExpandedRatio = newRatio behavior.halfExpandedRatio = newRatio
if (contentHeight > this.safeScreenHeight && behavior.state != BottomSheetBehavior.STATE_EXPANDED) { if (preventExpansion) {
behavior.maxHeight = (behavior.halfExpandedRatio * screenHeight).toInt()
}
val targetHeight = this.getTargetHeight()
val availableHeight = screenHeight - getStatusBarHeight() - getNavigationBarHeight()
val shouldBeExpanded = targetHeight >= availableHeight
if (isKeyboardVisible) {
if (behavior.state != BottomSheetBehavior.STATE_EXPANDED) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
} else if (shouldBeExpanded && behavior.state != BottomSheetBehavior.STATE_EXPANDED && !preventExpansion) {
behavior.state = BottomSheetBehavior.STATE_EXPANDED behavior.state = BottomSheetBehavior.STATE_EXPANDED
} else if (contentHeight < this.safeScreenHeight && 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
} else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) { } else if (currentState == BottomSheetBehavior.STATE_HALF_EXPANDED && oldRatio != newRatio) {
behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED behavior.state = BottomSheetBehavior.STATE_HALF_EXPANDED
@@ -279,25 +318,19 @@ class BottomSheetView(
private fun getTargetHeight(): Float { private fun getTargetHeight(): Float {
val contentHeight = this.getContentHeight() val contentHeight = this.getContentHeight()
val height = return when {
if (contentHeight > maxHeight) { contentHeight > maxHeight -> maxHeight
maxHeight contentHeight < minHeight -> minHeight
} else if (contentHeight < minHeight) { else -> contentHeight
minHeight }
} else {
contentHeight
}
return height
} }
private fun clampRatio(ratio: Float): Float { private fun clampRatio(ratio: Float): Float =
if (ratio < 0.01) { when {
return 0.01f ratio < 0.01 -> 0.01f
} else if (ratio > 0.99) { ratio > 0.99 -> 0.99f
return 0.99f else -> ratio
} }
return ratio
}
private fun setDraggable(draggable: Boolean) { private fun setDraggable(draggable: Boolean) {
val dialog = this.dialog ?: return val dialog = this.dialog ?: return
@@ -322,9 +355,7 @@ class BottomSheetView(
// View overrides to pass to DialogRootViewGroup instead // View overrides to pass to DialogRootViewGroup instead
override fun dispatchProvideStructure(structure: ViewStructure?) { override fun dispatchProvideStructure(structure: ViewStructure?) {
if (structure == null) { if (structure == null) return
return
}
dialogRootViewGroup.dispatchProvideStructure(structure) dialogRootViewGroup.dispatchProvideStructure(structure)
} }
@@ -363,7 +394,6 @@ class BottomSheetView(
// https://stackoverflow.com/questions/11862391/getheight-px-or-dpi // https://stackoverflow.com/questions/11862391/getheight-px-or-dpi
fun dpToPx(dp: Float): Float { fun dpToPx(dp: Float): Float {
val displayMetrics = context.resources.displayMetrics val displayMetrics = context.resources.displayMetrics
val px = dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT) return dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT)
return px
} }
} }
@@ -52,6 +52,8 @@ class DialogRootViewGroup(
if (ReactFeatureFlags.dispatchPointerEvents) { if (ReactFeatureFlags.dispatchPointerEvents) {
jSPointerDispatcher = JSPointerDispatcher(this) jSPointerDispatcher = JSPointerDispatcher(this)
} }
fitsSystemWindows = false
} }
override fun onSizeChanged( override fun onSizeChanged(
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="EdgeToEdgeBottomSheetDialogTheme" parent="Theme.Material3.DayNight.BottomSheetDialog">
<!-- Enable edge-to-edge -->
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:windowIsFloating">false</item>
<item name="enableEdgeToEdge">true</item>
<!-- Configure bottom sheet to respect system window insets -->
<item name="bottomSheetStyle">@style/EdgeToEdgeBottomSheet</item>
</style>
<style name="EdgeToEdgeBottomSheet" parent="Widget.Material3.BottomSheet">
<item name="paddingBottomSystemWindowInsets">false</item>
<item name="paddingLeftSystemWindowInsets">true</item>
<item name="paddingRightSystemWindowInsets">true</item>
<item name="paddingTopSystemWindowInsets">false</item>
</style>
</resources>
@@ -175,6 +175,7 @@ function BottomSheetNativeComponentInner({
Platform.OS === 'android' && { Platform.OS === 'android' && {
borderTopLeftRadius: cornerRadius, borderTopLeftRadius: cornerRadius,
borderTopRightRadius: cornerRadius, borderTopRightRadius: cornerRadius,
overflow: 'hidden',
}, },
extraStyles, extraStyles,
]}> ]}>
@@ -34,12 +34,15 @@ class NotificationPrefs(
is Boolean -> { is Boolean -> {
putBoolean(key, value) putBoolean(key, value)
} }
is String -> { is String -> {
putString(key, value) putString(key, value)
} }
is Array<*> -> { is Array<*> -> {
putStringSet(key, value.map { it.toString() }.toSet()) putStringSet(key, value.map { it.toString() }.toSet())
} }
is Map<*, *> -> { is Map<*, *> -> {
putStringSet(key, value.map { it.toString() }.toSet()) putStringSet(key, value.map { it.toString() }.toSet())
} }
@@ -117,7 +117,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
private fun handleImageIntents( private fun handleImageIntents(
uris: List<Uri>, uris: List<Uri>,
text: String? text: String?,
) { ) {
var allParams = "" var allParams = ""
@@ -145,7 +145,7 @@ class ExpoReceiveAndroidIntentsModule : Module() {
private fun handleVideoIntents( private fun handleVideoIntents(
uris: List<Uri>, uris: List<Uri>,
text: String? text: String?,
) { ) {
val uri = uris[0] val uri = uris[0]
// If there is no extension for the file, substringAfterLast returns the original string - not // If there is no extension for the file, substringAfterLast returns the original string - not
+1 -1
View File
@@ -202,7 +202,7 @@
"react-native-edge-to-edge": "^1.6.0", "react-native-edge-to-edge": "^1.6.0",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-get-random-values": "~1.11.0", "react-native-get-random-values": "~1.11.0",
"react-native-keyboard-controller": "1.18.5", "react-native-keyboard-controller": "^1.20.7",
"react-native-pager-view": "6.8.0", "react-native-pager-view": "6.8.0",
"react-native-progress": "bluesky-social/react-native-progress", "react-native-progress": "bluesky-social/react-native-progress",
"react-native-qrcode-styled": "^0.3.3", "react-native-qrcode-styled": "^0.3.3",
+1 -1
View File
@@ -3,6 +3,7 @@ import '#/view/icons'
import React, {useEffect, useState} from 'react' import React, {useEffect, useState} from 'react'
import {GestureHandlerRootView} from 'react-native-gesture-handler' import {GestureHandlerRootView} from 'react-native-gesture-handler'
import {KeyboardProvider as KeyboardControllerProvider} from 'react-native-keyboard-controller'
import { import {
initialWindowMetrics, initialWindowMetrics,
SafeAreaProvider, SafeAreaProvider,
@@ -14,7 +15,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import * as Sentry from '@sentry/react-native' import * as Sentry from '@sentry/react-native'
import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController'
import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder'
import {QueryProvider} from '#/lib/react-query' import {QueryProvider} from '#/lib/react-query'
import {s} from '#/lib/styles' import {s} from '#/lib/styles'
+3 -7
View File
@@ -11,6 +11,7 @@ import {
} from 'react-native' } from 'react-native'
import { import {
KeyboardAwareScrollView, KeyboardAwareScrollView,
type KeyboardAwareScrollViewRef,
useKeyboardHandler, useKeyboardHandler,
useReanimatedKeyboardAnimation, useReanimatedKeyboardAnimation,
} from 'react-native-keyboard-controller' } from 'react-native-keyboard-controller'
@@ -23,7 +24,6 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
import {ScrollProvider} from '#/lib/ScrollContext' import {ScrollProvider} from '#/lib/ScrollContext'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useA11y} from '#/state/a11y' import {useA11y} from '#/state/a11y'
@@ -209,10 +209,9 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
useEnableKeyboardController(IS_IOS)
const [keyboardHeight, setKeyboardHeight] = React.useState(0) const [keyboardHeight, setKeyboardHeight] = React.useState(0)
// note: iOS-only. keyboard-controller doesn't seem to work inside the sheets on Android
useKeyboardHandler( useKeyboardHandler(
{ {
onEnd: e => { onEnd: e => {
@@ -231,7 +230,6 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
} }
paddingBottom = Math.max(paddingBottom, tokens.space._2xl) paddingBottom = Math.max(paddingBottom, tokens.space._2xl)
} else { } else {
paddingBottom += keyboardHeight
if (nativeSnapPoint === BottomSheetSnapPoint.Full) { if (nativeSnapPoint === BottomSheetSnapPoint.Full) {
paddingBottom += insets.top paddingBottom += insets.top
} }
@@ -259,7 +257,7 @@ export const ScrollableInner = React.forwardRef<ScrollView, DialogInnerProps>(
{paddingBottom}, {paddingBottom},
contentContainerStyle, contentContainerStyle,
]} ]}
ref={ref} ref={ref as React.Ref<KeyboardAwareScrollViewRef>}
showsVerticalScrollIndicator={IS_ANDROID ? false : undefined} showsVerticalScrollIndicator={IS_ANDROID ? false : undefined}
{...props} {...props}
bounces={nativeSnapPoint === BottomSheetSnapPoint.Full} bounces={nativeSnapPoint === BottomSheetSnapPoint.Full}
@@ -289,8 +287,6 @@ export const InnerFlatList = React.forwardRef<
const insets = useSafeAreaInsets() const insets = useSafeAreaInsets()
const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext() const {nativeSnapPoint, disableDrag, setDisableDrag} = useDialogContext()
useEnableKeyboardController(IS_IOS)
const onScroll = (e: ScrollEvent) => { const onScroll = (e: ScrollEvent) => {
'worklet' 'worklet'
if (!IS_ANDROID) { if (!IS_ANDROID) {
@@ -28,7 +28,7 @@ export function VerifierDialog({
verificationState: FullVerificationState verificationState: FullVerificationState
}) { }) {
return ( return (
<Dialog.Outer control={control}> <Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
<Dialog.Handle /> <Dialog.Handle />
<Inner <Inner
control={control} control={control}
@@ -123,7 +123,6 @@ function Inner({
}), }),
)} )}
size="small" size="small"
variant="solid"
color="primary" color="primary"
style={[a.justify_center]} style={[a.justify_center]}
onPress={() => { onPress={() => {
@@ -138,7 +137,6 @@ function Inner({
<Button <Button
label={_(msg`Close dialog`)} label={_(msg`Close dialog`)}
size="small" size="small"
variant="solid"
color="secondary" color="secondary"
onPress={() => { onPress={() => {
control.close() control.close()
@@ -1,107 +0,0 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
} from 'react'
import {
KeyboardProvider,
useKeyboardController,
} from 'react-native-keyboard-controller'
import {useFocusEffect} from '@react-navigation/native'
const KeyboardControllerRefCountContext = createContext<{
incrementRefCount: () => void
decrementRefCount: () => void
}>({
incrementRefCount: () => {},
decrementRefCount: () => {},
})
KeyboardControllerRefCountContext.displayName =
'KeyboardControllerRefCountContext'
export function KeyboardControllerProvider({
children,
}: {
children: React.ReactNode
}) {
return (
<KeyboardProvider enabled={false} preload={false}>
<KeyboardControllerProviderInner>
{children}
</KeyboardControllerProviderInner>
</KeyboardProvider>
)
}
function KeyboardControllerProviderInner({
children,
}: {
children: React.ReactNode
}) {
const {setEnabled} = useKeyboardController()
const refCount = useRef(0)
const value = useMemo(
() => ({
incrementRefCount: () => {
refCount.current++
setEnabled(refCount.current > 0)
},
decrementRefCount: () => {
refCount.current--
setEnabled(refCount.current > 0)
if (__DEV__ && refCount.current < 0) {
console.error('KeyboardController ref count < 0')
}
},
}),
[setEnabled],
)
return (
<KeyboardControllerRefCountContext.Provider value={value}>
{children}
</KeyboardControllerRefCountContext.Provider>
)
}
export function useEnableKeyboardController(shouldEnable: boolean) {
const {incrementRefCount, decrementRefCount} = useContext(
KeyboardControllerRefCountContext,
)
useEffect(() => {
if (!shouldEnable) {
return
}
incrementRefCount()
return () => {
decrementRefCount()
}
}, [shouldEnable, incrementRefCount, decrementRefCount])
}
/**
* Like `useEnableKeyboardController`, but using `useFocusEffect`
*/
export function useEnableKeyboardControllerScreen(shouldEnable: boolean) {
const {incrementRefCount, decrementRefCount} = useContext(
KeyboardControllerRefCountContext,
)
useFocusEffect(
useCallback(() => {
if (!shouldEnable) {
return
}
incrementRefCount()
return () => {
decrementRefCount()
}
}, [shouldEnable, incrementRefCount, decrementRefCount]),
)
}
-3
View File
@@ -4,7 +4,6 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {usePreventRemove} from '@react-navigation/native' import {usePreventRemove} from '@react-navigation/native'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import { import {
type AllNavigatorParams, type AllNavigatorParams,
type NativeStackScreenProps, type NativeStackScreenProps,
@@ -37,8 +36,6 @@ export function FindContactsFlowScreen({navigation}: Props) {
}) })
}) })
useEnableKeyboardControllerScreen(true)
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const effect = useCallback(() => { const effect = useCallback(() => {
setMinimalShellMode(true) setMinimalShellMode(true)
-3
View File
@@ -15,7 +15,6 @@ import {
} from '@react-navigation/native' } from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
@@ -68,8 +67,6 @@ export function MessagesConversationScreenInner({route}: Props) {
const convoId = route.params.conversation const convoId = route.params.conversation
const {setCurrentConvoId} = useCurrentConvoId() const {setCurrentConvoId} = useCurrentConvoId()
useEnableKeyboardControllerScreen(true)
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
setCurrentConvoId(convoId) setCurrentConvoId(convoId)
-3
View File
@@ -2,7 +2,6 @@ import {useMemo, useReducer} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import * as bcp47Match from 'bcp-47-match' import * as bcp47Match from 'bcp-47-match'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {useLanguagePrefs} from '#/state/preferences' import {useLanguagePrefs} from '#/state/preferences'
import { import {
Layout, Layout,
@@ -60,8 +59,6 @@ export function Onboarding() {
) )
const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState() const [contactsFlowState, contactsFlowDispatch] = useFindContactsFlowState()
useEnableKeyboardControllerScreen(true)
return ( return (
<Portal> <Portal>
<View style={[a.absolute, a.inset_0, t.atoms.bg]}> <View style={[a.absolute, a.inset_0, t.atoms.bg]}>
-3
View File
@@ -16,7 +16,6 @@ import {useFocusEffect, useNavigation} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {STARTER_PACK_MAX_SIZE} from '#/lib/constants' import {STARTER_PACK_MAX_SIZE} from '#/lib/constants'
import {useEnableKeyboardControllerScreen} from '#/lib/hooks/useEnableKeyboardController'
import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name'
import { import {
type CommonNavigatorParams, type CommonNavigatorParams,
@@ -185,8 +184,6 @@ function WizardInner({
}) })
}, [navigation]) }, [navigation])
useEnableKeyboardControllerScreen(true)
useFocusEffect( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
setMinimalShellMode(true) setMinimalShellMode(true)
-3
View File
@@ -12,7 +12,6 @@ import {
BLUESKY_MOD_SERVICE_HEADERS, BLUESKY_MOD_SERVICE_HEADERS,
MAX_REPORT_REASON_GRAPHEME_LENGTH, MAX_REPORT_REASON_GRAPHEME_LENGTH,
} from '#/lib/constants' } from '#/lib/constants'
import {useEnableKeyboardController} from '#/lib/hooks/useEnableKeyboardController'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {useAgent, useSession, useSessionApi} from '#/state/session' import {useAgent, useSession, useSessionApi} from '#/state/session'
import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
@@ -121,8 +120,6 @@ export function Takendown() {
const webLayout = IS_WEB && gtMobile const webLayout = IS_WEB && gtMobile
useEnableKeyboardController(true)
return ( return (
<View style={[a.util_screen_outer, a.flex_1]}> <View style={[a.util_screen_outer, a.flex_1]}>
<KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent> <KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent>
@@ -5,6 +5,7 @@ import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
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'
@@ -28,6 +29,7 @@ export const ImageAltTextDialog = ({
image, image,
onChange, onChange,
}: Props): React.ReactNode => { }: Props): React.ReactNode => {
const {height: minHeight} = useWindowDimensions()
const [altText, setAltText] = React.useState(image.alt) const [altText, setAltText] = React.useState(image.alt)
return ( return (
@@ -38,7 +40,8 @@ export const ImageAltTextDialog = ({
...image, ...image,
alt: enforceLen(altText, MAX_ALT_TEXT, true), alt: enforceLen(altText, MAX_ALT_TEXT, true),
}) })
}}> }}
nativeOptions={{minHeight}}>
<Dialog.Handle /> <Dialog.Handle />
<ImageAltTextInner <ImageAltTextInner
control={control} control={control}
@@ -65,6 +68,8 @@ const ImageAltTextInner = ({
const t = useTheme() const t = useTheme()
const windim = useWindowDimensions() const windim = useWindowDimensions()
const [isKeyboardVisible] = useIsKeyboardVisible()
const imageStyle = React.useMemo<ImageStyle>(() => { const imageStyle = React.useMemo<ImageStyle>(() => {
const maxWidth = IS_WEB ? 450 : windim.width const maxWidth = IS_WEB ? 450 : windim.width
const source = image.transformed ?? image.source const source = image.transformed ?? image.source
@@ -165,7 +170,7 @@ const ImageAltTextInner = ({
</AltTextCounterWrapper> </AltTextCounterWrapper>
</View> </View>
{/* Maybe fix this later -h */} {/* Maybe fix this later -h */}
{IS_ANDROID ? <View style={{height: 300}} /> : null} {IS_ANDROID && isKeyboardVisible ? <View style={{height: 300}} /> : null}
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
) )
} }
+4 -4
View File
@@ -17163,10 +17163,10 @@ react-native-is-edge-to-edge@^1.2.1:
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358" resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358"
integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q== integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==
react-native-keyboard-controller@1.18.5: react-native-keyboard-controller@^1.20.7:
version "1.18.5" version "1.20.7"
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.18.5.tgz#ae12131f2019c574178479d2c55784f55e08bb68" resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.20.7.tgz#e1be1c15a5eb10b96a40a0812d8472e6e4bd8f29"
integrity sha512-wbYN6Tcu3G5a05dhRYBgjgd74KqoYWuUmroLpigRg9cXy5uYo7prTMIvMgvLtARQtUF7BOtFggUnzgoBOgk0TQ== integrity sha512-G8S5jz1FufPrcL1vPtReATx+jJhT/j+sTqxMIb30b1z7cYEfMlkIzOCyaHgf6IMB2KA9uBmnA5M6ve2A9Ou4kw==
dependencies: dependencies:
react-native-is-edge-to-edge "^1.2.1" react-native-is-edge-to-edge "^1.2.1"